Arduino: Связь двух плат


Соедините пару плат Arduino в два процессора или сделайте удаленный датчик с радиоканалом.
Большие связи Ника Вейча все это позволяют.


Есть куча причин, по которым вашему проекту может понадобиться больше одного Arduino -возможно, у вас есть массив удаленных датчиков или вы хотите управлять роботом на базе Arduino с другого Arduino. В любом случае, без взаимодействия тут не обойтись. К счастью, организовать его довольно просто. Способов передачи и приема данных много, и выбор зависит от ваших потребностей - в частности, от расстояния.

Экономим провода

Интерфейс SPI быстр и хорошо поддерживается в Arduino, но требует жуткого количества проводов - на него тратится по меньшей мере четыре вывода (или больше, для адресации). Тут еще есть смысл при беспроводном соединении, но если платы связаны локально, более чем достаточно двухпроводного интерфейса I2C. Библиотека Wire (см. наши более ранние эксперименты с ЕЕ-PR0M в LXF152/153) позволяет просто связать два Arduino, сэкономив множество дополнительных выводов. В ней надо задействовать аналоговые выводы 4 и 5, но это тоже просто.

Используется конфигурация «ведущий/ведомый», шиной управляет одно устройство, и никто не заговорит, пока к нему не обратятся первым. Теоретически на одной шине может быть множество плат Arduino или других устройств, управляет которыми одно главное. На практике, без применения специальных аппаратных повторителей длина создаваемой шины ограничена. Чтобы поговорить с устройством, выполните следующие шаги:

Wire.begin();
Wire.beginTransmission(Slave_address);
Wire.send(OxOI); Wire.endTransmission();

Первая строка инициализирует это устройство как главное. Затем ему нужно поговорить с определенным устройством, и мы загружаем его адрес. Данные обычно отправляются байтами, но в качестве параметров метода send() можно использовать указатель и длину. Наконец, всегда стоит приятно завершать разговор - endTransmission() на самом деле инициирует передачу данных, которые были помещены в буфер методом send(). Чтобы получить данные с ведомого устройства, главное инициирует разговор и говорит, сколько байтов оно хочет получить:

char bufter[8];
Wire.requestFrom(slave_address, 8);
int count=0;
while(Wire.available();
{
buffer[count] = Wire.receive();
cont++;
}

Функция Wire.available() возвращает количество принятых байт, находящихся в буфере в данный момент - для их получения используется метод Wire.receive(). Можно и ввернуть в эту передачу добавочный код. чтобы убедиться в получении нужного количества данных или получить фрагменты, потерявшиеся по дороге.

В порядке более полезного примера напишем короткую программу «пинга». Она установит одно устройство как главное, другое - как подчиненное. Можно использовать один и тот же код для обоих устройств и задать главное устройство на аппаратном уровне, просто подключив один из цифровых выходов к +5В и проверяя его внутри программы.

Вы не всегда предпочтете писать код таким образом, но для проверки гораздо проще иметь один блок кода - оказывается, ведомому устройству многое все равно не нужно. «Железа» тоже много не потребуется. На рис. 1а показано, как это делается физически (если у вас хорошие проводники, на макете можно обойтись без повышающих резисторов и подключить платы друг к другу напрямую).


Рис. 1а. Соединить Arduino через шину PC просто - иногда
даже не нужны повышающие резисторы.

Рис. 1b проясняет эту схему, и шину можно расширить для подключения других устройств.

Разобьем наш тестовый код на фрагменты (полностью он приведен на DVD в файле LXF152-arduino-code.zip):

#include <Wire.h>
const int configpin =7
bool config;
unsigned char buffer[8];
int Slave=8;

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

Код настройки по состоянию вывода определит, является ли устройство ведущим или ведомым, и установит все как надо:

void setup(void)
{
pinMode(configpin, INPUT); config = digitalRead(configpin);
Serial.begin(9600);
Serial, print(lnitialised as:");
if (config){ Serial.println("transmitter");
Wire.begin();
}
else{ Serial.println("receiver");
Wire.begin(Slave);
Wire.onReceive(slaveRX);
Wire.onRequest(slaveTX):
}
}

Код в общем понятен. Самое интересное происходит во фрагменте для ведомого устройства. Существует два метода установки callback-функции (обратного вызова) - в одном она вызывается, когда библиотека Wire фиксирует отправку данных ведущим устройством, а в другом - когда принимается запрос на возврат данных. Этим мы пока не занимались.

В главном цикле должен быть код только для главного ведущего, но в качестве обратной связи мы можем отправлять какие-то сообщения о состоянии по последовательному каналу при возникновении определенных событий. Основной код здесь передает байт данных, а затем просит передать его обратно. С помощью встроенного таймера (путем вызова функции millis() можно узнать, сколько времени занимают отправка и возврат:

void loop(void)
{
if (config) {
uint32_t time = millis();
uint32 t start:
bool timeout = false;
Serial.print("Now sending");
Wire.beginTransmission(Slave);
Wire.send(0xFF);
WireTransmissioin();

О методах для передачи данных мы говорили выше. Теперь, когда данные отправлены, можно попросить прислать их обратно:

Serial.printlnf"
Waiting for response");
delay(20);
Wire.requestFrom(Slave,1);
delay(20);
Serial.println("Datarequested");
buffer[0]=Wire.receive():
Serial, print(response received");
Serial.println(buffer[0],HEX);
Serial.print("round-trip:");
Serial.println(millis()-time):
delay(2000);
}
else
Serial.print("waiting");
delay(1000);
}
}

Наконец, нужно написать функции для работы на ведомом устройстве. Каждая из них вызывается с аргументом - целым числом, которое задает число принятых или запрошенных байт:

void slaveRX(int bytes){
buffer[0]=Wire.receive();
Serial.print("value received:"); ;
Serial.println(buffer[0]. HEX);
};
void slaveTX(){;
Serial.println("sending reply"); ;
Wire.send(buffer[0]);

Здесь есть несколько задержек, чтобы линии могли приходить в исходное состояние между передачами данных - на хороших шинах этого не потребуется: может, впрочем, оказаться, что нужно несколько повышающих резисторов, если напряжение на линиях будет с трудом достигать 5 В. Подключите к каждой линии на +5 В резистор сопротивлением 2 кОм. На нашем тестовом оборудовании на отправку и прием байта ушло около 90 мкс без задержек. Не так уж плохо.

Скорая помощь

А что у нас с однопроводными соединениями? Некоторые датчики используют именно такое соединение для передачи данных (см. www.arduino.cc/Dlavoround/Learning/OneWire). Но для обмена большими объемами данных между двумя Arduino это не лучший вариант.

Что нам надо? Две платы Arduino (любых). » Для схем с радиосвязью вам понадобятся две платы nRF2401+ (см. http://www.sparkfun.com/products/152. http://proto-pic.co.uk/transceiver-nrf2401a-with-chip-antenna/. или поищите на eBay). » Библиотека RF c LXFDVD.

Если вам нужен беспроводной канал связи, вариантов несколько. Из дорогих - стандарт «ZigBee», поддерживаемый схемами и модулями ХВее, предоставит вам все, о чем вы мечтаете. В большинстве схем ХВее применяется простой последовательный интерфейс, и они часто используются в библиотеках и примерах Arduino. Большой недостаток - стоимость: около 20 фунтов за устройство - это не то, что вы охотно приплюсуете к каждому проекту. Из более доступных - платы радиосвязи, работающие по принципу регенерации (для щеголяющих старомодностью, поясню: «автодины»). Они достаточно дешевы, и их легко собрать или переделать самим. Но им не хватает оснастки - придется писать собственные протоколы для отправки и приема данных; вдобавок эти устройства способны интерферировать друг с другом, и пользоваться несколькими устройствами по соседству может быть затруднительно. «Золотая середина» -серия трансиверов RF24XX. Производимые Nordic Semiconductors, эти чудесные маленькие схемы работают на частоте 2.4 ГГц. Немного не дойдя до полноценного сетевого протокола, они предлагают столь полезные возможности, как выбор канала, передача пакетов подтверждения и различные скорости передачи (чтобы выжать из сигнала наибольшее расстояние), и обойдутся вдесятеро дешевле ХВее.

Эти микросхемы можно купить уже смонтированными на оконечной плате [breakout board] вместе с антенной. Такие платы выпускает Sparkfun. Их довольно сложно пристроить к Arduino, но скоммутировать все на макетной плате не составит труда.

Схемы nRF24XX являются полудуплексными. Они могут отправлять или принимать данные, но не одновременно. Чтобы реализовать это программно, придется немного повозиться, но эта проблема свойственна и другим (не по радиоканалу) способам соединения. В любом случае, микросхемы ATmega для многозадачности не совсем пригодны.

Существует пара реализаций библиотеки и для этого устройства. Более сложная из двух, но и с большей функциональностью - библиотека RF24 от Джеймса Колиза-младшего [James Coliz, Jr]. Она поддерживает многие аппаратные функции микросхемы без лишних осложнений. Посмотрим, как с помощью этой библиотеки настроить соединение по радиоканалу и воспользоваться им:

#include<SPI.h>
#include"nRF24L01.h"
#include "RF24.h"

Эта библиотека использует библиотеку SPI для Arduino, поэтому ее тоже нужно подключить. Здесь также подключается класс RF24 - с его помощью легко создать радиоканал, и после подключения всего этого мы создаем экземпляр радиоканала:

RF24 radio(8,9);

Здесь инициализируется объект radio с использованием выводов 8 и 9 Arduino в качестве выводов СЕ (Chip Enable - микросхема активна) и CSN (Chip Select Not - микросхема не выбрана) соответственно. Название «Микросхема не выбрана» может показаться забавным, но по сути это инвертированный вывод «Микросхема выбрана» (Chip Select) - он активен в состоянии «нуля», и микросхема выбирается, когда этот вывод соединяется с «землей». Это обычное дело для интерфейсов SPI. поэтому даже если вывод отмечен как CS или SS, проверьте, активируется ли он «нулевым» уровнем. Другие выводы для обмена данным по SPI -те, что обычно используются библиотекой SPI в Arduino: 11,12 и 13.

В микросхемах nRF24 есть набор внутренних каналов для приема и передачи данных. С помощью одного канала можно принимать или отправлять данные, с помощью еще пяти -только слушать. У каждого канала есть адрес - нечто вроде МАС-адреса в сети. Это 64-битное число, и оно должно быть максимально случайным, чтобы избежать конфликтов. Поэтому ваша микросхема может передавать данные на один заданный адрес и принимать данные с пяти адресов. Единственное ограничение - в том, что первые четыре байта адреса для всех принимающих каналов должны быть одинаковыми. Чтобы задать 64-битное число в нашем коде, мы используем 64-битное беззнаковое целое число (добавляем LL (long long) в конец шестнадцатеричного значения числа):

const uint64_t txpipe = 0x818181818101LL;;
const uint64_t rxpipe1 = 0xFFFFFFFF01LL;;
const uint64_t rxpipe2 = 0xFFFFFFFF02LL;


He все микросхемы для создания канала удобно подключать к макету.

Метод

Преимущества

Недостатки

Реальная дальность

Двухпроводное соединение (I2C)

Просто, быстро, и к шине можно подключить несколько устройств.

С кабелем большой длины может не работать; медленнее, чем SPI в Arduino.

В пределах одного стола.

Трехпроводное соединение (SPI)

Работает на больших расстояниях по сравнению с I2C, легко написать код с помощью библиотеки SPI.

Требуется больше проводников, и для каждого подчиненного устройства нужен отдельный проводник.

В соседней комнате.

Последовательный порт (UART)

Простота, работает на довольно длинных расстояниях.

Медленный, может оказывать влияния на плату перепрограммирования; только одно соединение.

В гостиной.
RS485

Хорошие результаты и высокая скорость по длинному кабелю.

Нужны дополнительные устройства и хороший кабель; также загружает последовательную шину.

В пределах дома.
Беспроводное;
соединение

Без проводов! Не так дорого, как вы думали - классно!

Более сложная настройка; может быть менее надежным, требуется много проводов.

Внизу в саду и дальше.

В коде установке мы инициализируем объект radio:

radio. beginQ;

и настраиваем различные каналы:

radio.openWritingPipe(txpipe);
radio.openReadingPipe(1,rxpipe1);
radio.openReadingPipe(2,rxpipe2);

Функция openReadingPipe() принимает два аргумента. Первый -номер используемого канала. Канал для чтения 0 используется и для записи, поэтому если вам на самом деле не нужно шесть каналов, лучше пропустить его. Хотя мы задали адреса каналов, сам класс radio ничего не делает. Нам нужна другая команда, чтобы заставить его слушать:

radio.startlistening();

При приеме данных классом radio он заполняет буфер самой микросхемы. Чтобы получить данные в нашей программе, нужно периодически проверять, есть ли там данные, и считывать данные в собственный буфер. Очевидно, что это нужно делать в главном цикле:

unit32_t data;
if (radio.available()) {
radio.read(&data, sizeof(data));
}
radio.stopListening();

Здесь мы объявляем 32-битное целое для приема данных (данные передаются в пакетах по 32 бита). Мы проверяем, поступили ли какие-то данные, с помощью метода available(). Если да. мы их считываем! Этот метод принимает адрес, по которому нужно сохранить данные, и размер данных, которые нужно считать (в 8-битовых байтах). Можно было сразу записывать по 4 байта, чтобы сэкономить время, но вспомогательная функция sizeof() уменьшает шансы ошибиться.

Метод radio.read() также возвращает булевское значение, и если данные еще есть в буфере, то это True. С его помощью можно построить цикл для считывания всех доступных данных. Прекратив ожидание данных, мы также вызываем метод stopListening(). который по сути дела переводит радио в спящий режим. Из-за полудуплексного обмена данными нужно выключить радио, чтобы записать в него данные.

uint8_t output = 128:
bool ok = radio.write( &output, sizeof(output));
if (ok)
Serial, println("ok...");
else
Serial, println("failed.");

Метод radio.write() очень похож на метод приема данных -он принимает адрес данных и размер данных для отправки, в данном случае всего 1. Он также возвращает булевское значение, означающее, была ли принята передача. Независимо от вашего кода, в микросхемах nRF для проверки приема данных отправляются пакеты подтверждения (АСК). Существует возможность автоматической повторной передачи - в этом случае после небольшой задержки будет предпринята повторая попытка отправки каждого пакета. Ее настройки можно задать таким образом:

radio.setRetries(15,15);

Здесь первое число - задержка в блоках по 250 мкс (максимальное количество блоков -15; 250+15x250=4000 мкс), а второе - количество попыток (максимальное -15).

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

radio.setChannel(111);

Итак, переписав наш код «пинга» для RF24 (см. DVD), мы получили следующие результаты: производительность осталась хорошей, время прохождения байта составило около 25 мкс со всеми накладными расходами - это в четыре раза быстрее PC.

Как бы вы ни решили подключить друг к другу платы Arduino. помните, что сеть не идеальна. Данные иногда теряются, пакеты не доходят, а провода грызут мыши. Вам нужно все предусмотреть, чтобы ваш код не блокировался. Пишите код с умом, и помните, что неожиданности случаются всегда.


На большинстве плат nRF24xx есть встроенная антенна. Она не дает
лучшую дальность - можно также приобрести платы с разъемами для антенн, как у Wi-Fi.

Пакеты АСК

Великие умы, спроектировавшие nRF2401. предусмотрели особый режим подтверждения. Так как радиоприемник после приема должен отправить пакет назад, чтобы подтвердить получение данных (если вы не отключили эту возможность), почему бы не записать что-нибудь в этот пакет? Если определить одиночный пакет для возврата, он будет отправлен и и декодирован обычным образом.
Один из вариантов применения этой схемы - удаленный датчик. Вместо того, чтобы писать код для отправки назад значения при каждом запросе на него, можно просто сохранить текущее значение датчика в пакете АСК, и оно будет возвращено автоматически - и это освобождает вас от необходимости выключать режим прослушивания, чтобы отправить ответ. Вы просто записываете данные в пакет таким образом
radio.writeAckPayload( 1, &data, sizeof(data));
Впрочем, в определенный момент нужно очистить буфер FIFO, считав из него принятые пакеты, иначе он переполнится и перестанет выдавать подтверждения для принимаемых данных.

Скорая помощь

Вам не всегда нужно два Arduino ! Похожие возможности могут предоставить и другие (дешевые) члены семейства Atmel -их в любом случае достаточно для передачи данных датчика по последовательному соединению. Чтобы познакомиться сними поближе и запрограммировать их, вам придется окунуться в мир gcc-avr и AVRdude (см. LXF156).


Май 2012 LXF157 c.80-83