30.12.2013

Freeduino wireless (2)

Сегодня я хочу описать очередной способ беспроводного общения для Arduino. В предыдущий статье я рассказывал о простейших аналоговых модулях с амплитудной (ASK) модуляцией и библиотеке VirtualWire. Из плюсов - спартанская простота, из минусов - необходимость добавления антенн для устойчивой работы даже в пределах комнаты.

Довольно давно на рынке существует микросхема nRF24L01+, выпускаемая фирмой Nordic Semiconductors. Она обеспечивает возможность приема или передачи информации на несущей 2,4 ГГц (полоса ISM), методом частотной манипуляции GFSK. Для Arduino это решение удобно не только в силу низкой стоимости чипа, но и по причине подключения через SPI.

Для экспериментов нам понадобится какой-либо беспроводной модуль на основе nRF24L01+, например такой:



Его можно купить практически где угодно, стоить больше 160 рублей он не должен. Следует заметить, что 90% таких модулей ничем друг от друга не отличаются - схема скопирована из реф-дизайна evolution-кита к nRF24L01+, опубликованному производителем. Антенна расположена прямо на печатной плате, что при такой длине волны вполне допустимо и обещает максимальное расстояние до 100 метров (разумеется, с оговоркой про "идеальные условия", в реальности оно будет меньше - сколько именно, придется проверять экспериментально). К слову, существует две версии чипа - nRF24L01 и nRF24L01+, отличающиеся совсем ненамного и совместимые друг с другом (просто имейте ввиду, что если ваш модуль с nRF24L01 - к нему эта статья применима в полном объеме).

Назначение контактов на вилке должно быть следующим (вид сверху):



У модулей с вилкой 2x4 есть одно существенное неудобство - невозможно воткнуть в беспаечную макетку: расположенные в два ряда контакты, увы, будут закорочены попарно. Остается только позавидовать счастливым обладателям модуля от SparkFun, не поскупившимся отдать за это чудо 19.95 USD:

Посмотрим, какие выводы и куда подключать:
  • GND - земля, соединяем с одним из пинов GND на Arduino;
  • VCC - питание модуля,  внимательно - соединяем с пином 3.3В - модуль питается от напряжений 1,9..3,6В, хотя при этом отлично умеет работать с логическими уровнями +5В;
  • CE - расшифровывается как Chip Enable, но речь не про работу всего чипа, а лишь про его радиоинтерфейс - в режиме приема заставляет модуль "прослушивать" эфир в поисках предназначенного ему пакета, в режиме передачи кратковременный импульс CE передает в эфир содержимое буфера FIFO.  Подключим его к пину D8;
  • CSN - SPI: Chip select not, выбор Slave-устройства на шине SPI низким уровнем, подключаем к пину D7;
  • SCK - SPI: Clock, подключаем к пину D13;
  • MOSI - SPI: Master Out, Slave In - подключаем к пину D11;
  • MISO - SPI: Master In, Slave Out - подключаем к пину D12;
  • IRQ - прерывание, пока никуда не подключаем :(
Пины с аппаратной поддержкой SPI (SCK, MOSI, MISO) гарантированно можно найти на шестиконтактной ISP-вилке Arduino:


Если же будете подключаться к боковым гребенкам, учитывайте, что аппаратная поддержка SPI у Mega находится на пинах 50 (MISO), 51 (MOSI), 52 (SCK) и 53 (SS).

Переходник для установки в беспаечную макетку можно соорудить из разъемов PBD08 и PLS8, на кусочке обрезка обычной макетной платы (под пайку):



Итак, паяльник можно откладывать в сторону. Однако прежде чем приступать к написанию скетчей, познакомимся с документацией на чип и поподробней разберемся в том, что он умеет (и что не умеет - разумеется, тоже).

nRF24L01+ способен работать приемником или передатчиком на частотах 2400..2525 МГц, конкретная частота определяется номером частотного канала от 1 до 126, задаваемым через регистр RF_CH. Чтобы приемник и передатчик смогли обмениваться между собой, они обязательно должны быть сконфигурированы для одного и того же канала. Скорость обмена по умолчанию - 2 Мбит/с, но при необходимости ее можно снизить до 1 Мбит/с - в основном это делается для совместимости с ранними версиями чипов Nordec (вот одно из отличий nRF24L01 от nRF24L01+: последний умеет работать еще и на пониженной скорости 250 Кбит/с).

Данные передаются пакетами. Пользовательская часть (в документации называется "Payload") может занимать до 32 байт, остальные поля - служебные:


В одном частотном канале могут обмениваться довольно много устройств - для их идентификации предусмотрено поле адреса (от 3 до 5 байт). Таким образом, каждый передатчик отправляет сообщение конкретному приемнику, указывая адрес получателя; приемник, в свою очередь, прослушивает эфир и отбрасывает все сообщения, адрес которых не совпадает с его собственным.

В какой-то конкретный момент времени чип может быть либо передатчиком, либо приемником - либо передавать, либо принимать: по-очереди, но не одновременно. Для контроля можно организовать смену режимов - одной стороне даете команду передать сообщение и перейти в режим приема, другой - принять сообщение, проверить его корректность с помощью поля CRC, затем перейти в режим передачи и "отстучать" передавшему подтверждение.

Но, к счастью, существует специальный встроенный протокол Enhanced Shockburst (tm), который берет на себя все заботы о гарантированной доставке сообщения между двумя nRF24L01+. Это включает в себя не только прием, проверку правильности и отсылку подтверждающего пакета приемником, но и повторную перепосылку пакета передатчиком. Не правда ли, инженеры Nordic Semiconductors поработали на славу? ;) Для работы протокол использует поле управления пакетом.

В довершение существует специальный режим Multiceiver, в котором приемник может быть настроен на получение данных от шести передатчиков, и опять же, с возможностью работы по Enhanced Shockburst. Иными словами - один приемник может "тянуть" данные из шести передатчиков с аппаратным подтверждением и перепосылкой. Правда, есть некоторые ограничения - все устройства надо сконфигурировать на работу в одном частотном канале, с одинаковой скоростью,  длиной адреса и способом формирования CRC. Один адрес передатчика можно выбрать произвольно, а вот остальные пять должны совпадать во всех байтах адреса, кроме последнего (например, 0x1234567801, 0x12345678D5, 0x12345678AA, 0x1234567855, и так далее).

Адрес задает пользователь, и, строго говоря, он может быть произвольным. Но располагается он в самом начале пакета, сразу после преамбулы, которая выглядит в двоичном представлении как 01010101 или 10101010 - такая комбинация не случайна и нужна для правильного пробуждения приемника и его подстройки к частоте передатчика. Чтобы было меньше ложных "отбраковываний" пакетов приемной частью, надо стараться избегать как адресов, похожих на преамбулу (например - 0xAAAAAA или 0x555555), так и адресов, состоящих из вообще одних нулей и единиц (еще два плохих примера - 0xFFFFFF или 0x000000).

Пример схемы (допустим, Freeduino Nano):



Какие библиотеки  для Arduino существуют для работы с nRF24L01+?


Самый простой вариант - библиотека Mirf, которая предоставляет минимальную обвязку (её даже адаптировали для ATtiny). Скачайте последнюю версию библиотеки:
  • на странице Arduino playground, посвященной nRF24L01+;
  • непосредственно из gitHub.
Перед началом работы надо задать пины, к которым подключены CE и CSN, собственный адрес как приемника, длину пользовательской части (payload) и вызывать пару функций. По умолчанию предполагается, что CE подключен к D8, CSN - к D7 (ровно так мы уже и сделали), работа будет идти по каналу 1, длина пакета - 16 байт. Если что-то из перечисленного не совпадает с вашими потребностями, необходимо установить соответствующие значения полей до вызова init() и config(), например: Mirf.cePIN = 10;  в случае, если CE подключен к D10.

В основном, последовательность инициализации выглядит так:
  1. Mirf.spi = &MirfHardwareSpi;
  2. Mirf.init();
  3. Mirf.setRADDR((byte *)"12345");
  4. Mirf.payload = 4;
  5. Mirf.config();

Первая строчка - установление "правильного" алгоритма работы с SPI, носит скорее исторический характер. Сейчас всё равно вся работа осуществляется через встроенную в ArduinoIDE библиотку SPI, но ведь не всегда было так ;) Второй строчкой происходит инициализация пинов и SPI, в третьей мы устанавливаем собственный адрес приемника, длиной 5 байт (символьная строка "12345" дана исключительно для наглядности, на самом деле это 0x3132333435 ;) В четвертой - мы устанавливаем длину пакета 4 байта, и, наконец-то, в пятой конфигурируем чип NRF24L01 и переводим его в режим приема.

Дальше можно принимать и передавать данные.

Прием:

 if(Mirf.dataReady()){
  Mirf.getData((byte *) &packet); 
}
Сначала дожидаемся прихода пакета в буфер, затем считываем его в переменную packet (ее размер должен соответствовать текущей длине payload).

Передача:

Mirf.setTADDR((byte *)"12345");
Mirf.send((byte *)&packet);
while(Mirf.isSending()){
}

Устанавливаем адрес получателя, затем отсылаем packet и при помощи isSending дожидаемся завершения отправки.

К библиотеке прилагаются примеры ping_client и ping_server, которые обмениваются пакетами друг с другом. Чуть интереснее изучить пример  ping_server_interrupt, демонстрирующий работу по прерыванию в режиме энергосбережения: пока нет данных, Arduino пребывает в режиме сна. Однако, как только фиксируется прием нового пакета, он просыпается, принимает пакет из nRF24L01+, отсылает ответный пакет и снова "засыпает". Для корректной работы потребуется подключить ранее неиспользованный пин IRQ на плате радиомодуля - к пину D2 Arduino.

В каких случаях генерируется прерывание от nRF24L01+?


Это происходит в трех ситуациях и отображается в регистре STATUS при помощи соответствующих битовых полей:
  1. TX_DS - передающая сторона получила от встречной стороны информацию (ACK-пакет), подтверждающую успешный прием;
  2. RX_DR - приемная сторона получила новый пакет данных (и с правильным CRC, и не дубль уже полученного ранее пакета);
  3. MAX_RT - передающая сторона попыталась передать пакет максимально разрешенное количество раз, но так и не получила подтверждения о приеме.  
И, самое важное - после того, как любой из перечисленных битов был установлен, для дальнейшей работы его необходимо сбросить - записать единицу. Обычно, об этом заботится библиотека ;)

Функция Mirf.isSending() как раз и проверяет факт завершения передачи по установке TX_DS или MAX_RT. Но имейте ввиду, что она не отвечает на вопрос о том, был ли пакет благополучно принят на противоположной стороне. После того, как Mirf.isSending() вернет true, чтение регистра STATUS становится бессмысленным - перед выходом функция сбрасывает биты TX_DS и MAX_RT. К счастью, при каждом возникновении MAX_RT увеличивается счетчик PLOS_CNT в специальном регистре OBSERVE_TX, поэтому проверку благополучной доставки можно организовать так:

int readPLOS_CNT() {
  byte reg;
  Mirf.readRegister(OBSERVE_TX,&reg,1);
  return (reg >> PLOS_CNT) & B1111;
}

void loop() {
  
  int prev_plos_cnt = readPLOS_CNT();

  /* ... */
  Mirf.send((byte *)&packet);
  while(Mirf.isSending()){
  }

  if (readPLOS_CNT() != prev_plos_cnt) {
   /* failed :( */else {
   /* success! */
  }
}

Еще можно вообще отказаться от использования Mirf.isSending(), и проверять биты TX_DS и MSX_RT непосредственно чтением регистра STATUS. Но тогда не забывайте сбрасывать их после установки, иначе процесс передачи встанет после первого же пакета.

Как я уже упоминал в самом начале - библиотека реализует действительно минимально необходимый набор функционала. За это приходится расплатиться следующим:
  • длина адресного поля - всегда 5 байт;
  • CRC всегда однобайтовый;
  • длина payload всегда фиксирована;
  • нет поддержки режима multiceiver (прием данных от шести передатчиков).
Что-то решается простейшим исправлением кода библиотеки, что-то можно сделать через запись и чтение управляющих регистров nRF24L01+, что-то (multiceiver или динамическая длина payload) добавить уже и вовсе непросто.

Однако, если у вас возникает слишком много претензий к функционалу Mirf, можно попробовать альтернативу - RF24. В ней конфигурируется буквально всё, в том числе автоматически учитываются различия между nRF24L01 и nRF24L01+. Идеологически библиотека ближе к стандартам Arduino и интуитивно понятнее, особенно начинающим. Например, по аналогии с Mirf есть функция RF24::startWrite, а в дополнение к ней - RF24::send, которая блокирует ход скетча до завершения процесса передачи, чтобы стало понятно - передан пакет успешно или нет, что и сообщается в возвращаемом значении типа bool (прямая замена вышеприведенному коду, который пришлось добавлять для аналогичного контроля в Mirf). 

В комплекте с библиотекой RF24 много полезных примеров, которые демонстрируют использование irq, отправку пользовательских данных в ACK-пакете и прочее. Есть даже примитивный сканер эфира ;)

Куда двигаться дальше? 


Например, по каким-то причинам не устраивает дальность приема: то ли расстояние велико, то ли стен многовато. В этом случае надо брать модуль nRF24L01+ с PA/LNA (Power Amplifier и Low Noise Amplifier):


Этот модуль будет стоить чуть дороже, около 10 USD. В его основе nRF24L01+ с точно такой же вилкой, но у него непосредственно к выходным каскадам вместо антенны подключен усилитель, повышающий мощность передатчика и чувствительность приемника. К SMA-коннектору необходимо подключить стандартную антенну для диапазона 2,4 ГГц (можно скрутить с нерабочего WiFi роутера) и правильно сориентировать - получите диапазон до 1000 метров. Правда, сразу вырастут запросы по питанию: модулю потребуются не десятки, а сотни mA, напряжение питания должно быть в диапазоне 3,0..3,6В. В этом случае может уже не получиться питать его от Arduino - если не помогает конденсатор 10 мкФ между 3,3В и землей, просто подключите модуль к отдельному источнику питания.

Да, и еще одно: не исключено пересечение по частотным каналам с другими беспроводными устройствами. Если у вас есть беспроводная клавиатура или мышь на 2,4 ГГц, с очень большой долей вероятности есть шанс разобрав ее увидеть нашего старого знакомого ;) 

Ссылки по теме: