Журнал LinuxFormat - перейти на главную

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

Материал из Linuxformat
Перейти к: навигация, поиск


Элек­тро­ни­ка Нау­чим ма­кет­ные пла­ты раз­го­ва­ри­вать друг с дру­гом

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

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

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

Ин­тер­фейс SPI быстр и хо­ро­шо под­дер­жи­ва­ет­ся в Arduino, но тре­бу­ет жут­ко­го ко­ли­че­­ст­ва про­во­дов – на него тра­тит­ся по мень­шей ме­ре че­ты­ре вы­во­да (или боль­ше, для ад­ре­са­ции). Тут еще есть смысл при бес­про­вод­ном со­единении, но ес­ли пла­ты связаны локаль­но, бо­лее чем доста­точ­но двух­про­вод­ного ин­тер­фейса I2C.

Биб­лио­те­ка Wire (см. на­ши бо­лее ранние экс­пе­ри­мен­ты с EEPROM в LXF152/153) по­зво­ля­ет про­сто свя­зать два Arduino, сэко­но­мив мно­же­ст­во до­полнитель­ных вы­во­дов. В ней надо за­дей­ст­во­вать ана­ло­го­вые вы­во­ды 4 и 5, но это то­же про­сто.

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

Wire.begin();

Wire.beginTransmission(Slave_address);

Wire.send(0x01);

Wire.endTransmission();

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

char buffer[8];

Wire.requestFrom(slave_address, 8);

int count=0;

while(Wire.available())

{

buffer[count] = Wire.receive();

count++;

}

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

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

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

  1. 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(“Initialised 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);

Wire.endTransmission();

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

Serial.println(“Waiting for response”);

delay(20);

Wire.requestFrom(Slave,1);

delay(20);

Serial.println(“Data requested”);

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 мкс без за­дер­жек. Не так уж пло­хо.

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

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

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

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

  1. include <SPI.h>
  1. include “nRF24L01.h”
  1. include “RF24.h”

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

RF24 radio(8,9);

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

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

const uint64_t txpipe = 0x818181818101LL;

const uint64_t rxpipe1 = 0xFFFFFFFF01LL;

const uint64_t rxpipe2 = 0xFFFFFFFF02LL;

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

radio.begin();

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

radio.openWritingPipe(txpipe);

radio.openReadingPipe(1,rxpipe1);

radio.openReadingPipe(2,rxpipe2);

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

radio.startListening();

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

uint32_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 для про­вер­ки прие­ма дан­ных от­прав­ля­ют­ся па­ке­ты под­твер­ждения (ACK). Су­ще­ст­ву­ет воз­мож­ность ав­то­ма­ти­че­­ской по­втор­ной пе­ре­да­чи – в этом слу­чае по­сле неболь­шой за­держ­ки бу­дет пред­при­ня­та по­вто­рая по­пыт­ка от­прав­ки ка­ж­до­го па­ке­та. Ее на­строй­ки мож­но за­дать та­ким об­ра­зом:

radio.setRetries(15,15);

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

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

radio.setChannel(111);

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

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

LXF00.mugs.nick_col.psd Наш эксперт

Ко­гда LXF толь­ко поя­вил­ся, его дер­жа­ли на пла­ву исключительно скрип­ты Bash от Ни­ка Вей­ча. По­том их за­ме­ни­ли «лю­ди», и это, по мне­нию Ни­ка, ста­ло ша­гом на­зад...

Персональные инструменты
купить
подписаться
Яндекс.Метрика