Két Arduino összekapcsolása I2C-vel

Tartalom:

  • Két Arduino alaplap összekapcsolásának alapelve, technikai megvalósítása (vezetékezés)
  • Master és slave működés, információcsere tulajdonságai
  • Minta forráskód az adatcsere bemutatására
  • Sok master egy I2C buszon?

———————————————————————————

Előfordulhat, hogy két Arduino között kel adatokat cserélni. Az alábbi megoldást az egy interneten megjelenő cikkben találtam. Sajnos a forrást már nem tudom megjelölni. Az összeköttetés létrehozásának alapja, hogy az Arduino panelek A4 és A5 kivezetéseit kössük össze. Nem szabad elfelejteni a föld vezetéket sem, ha az Arduino panelek egymástól független táplálásúak. Ha ugyanarra a tápegységre kötjük mindkettőt, akkor a közös föld egyértelmű.

Az egyik Arduino lesz a mester, aki küldi az adatokat, míg a másik a slave, aki fogadja. Mivel az I2C buszon több slave eszköz is lehet, a slave eszközöknek önálló és megkülönböztethető címmel kell rendelkeznie. Az RTL óra panel esetében pl. ez a cím egy gyárilag beállított cím, amit „huzalozással” meg is lehet változtatni. Az Arduino esetén a címet szoftveresen kell beállítani.

A master-en mindent úgy kell csinálni, mint ahogyan megszoktuk egy akármilyen I2C buszra kötött modul adatainak lekérése, vagy számára történő adatküldés során. A setup() részben meghívjuk a wire.begin() függvényt paraméter nélkül, és ettől máris master-ként viselkedik az Arduino. Az SCK kimeneten óra jelet állít elő, amikor adatokat küld vagy fogad, az SDA kimeneten illetve bemeneten pedig közlekednek az adatok.

A slave-en csak annyi a teendőnk, hogy a wire.begin() függvényt nem paraméter nélkül, hanem a slave címének megadásával hívjuk meg a setup() ban. PL. ha azt írjuk wire.begin(8), akkor ez az Arduino egy 8-as című slave lesz. Nyilván a slave programjában le kell kezelni a master-től érkező adatok fogadását és az adatok küldését is, de az alábbi a példa programból könnyem megérthető. Az adatok fogadása és küldése megszakításokkal működik. Azaz a fő programunk fut, csinálja azt amivel foglalkozik egyébként, és ha adat érkezik a master felől, akkor keletkezik egy magszakítás. Félbemarad az éppen végrehajtott program, és meghívódik egy függvény, amit előre meg kellett írnunk. Ezt a függvényt a setup() ban a Wire.onReceive(függvénynév) függvény paraméterében kell megnevezni. Ez fogadja az adatokat, és amikor vége az adat fogadó függvényünk végrehajtásának, akkor fut tovább a fő programunk. Ha adatot kell küldenünk a master-nak, akkor ezt a master kezdeményezi, megszólítja a 8-as címen lévő eszközt, azaz a slave Arduinonkat. Keletkezik egy megszakítás és meghívódik egy függvény, amit a setup()-ban a Wire.onRequest(függvénynév) függvényben megneveztünk, ami elküldi a master-nek a szükséges adatokat, majd fut tovább a főprogramunk.

Elméletileg az I2C buszon több master is lehet. Sajnos nem találtam arra vonatkozóan semmilyen információt, hogy az I2C busz kezelésére szolgáló függvény könyvtárak tudják-e kezelni a két mastert a buszon. Valószínűnek tartom, hogy nem. Azonban nem kizárt, hogy a wire.begin() (masterterként működik) és a Wire.begin(8) (8-as című slave-ként működik) utasításokat lehet egy programon belül váltogatni, és egy Arduino szerepe egy programon belül váltogatható. Persze adminisztrálni kell, hogy milyen szerepben van a másik Arduino, nehogy ketten egyszerre legyenek masterek. Ezt nem próbáltam ki eddig.

További észrevételem, hogy a Wire.beginTransmission(8);  és a Wire.endTransmission(); programsorok között kiadható Wire.Write()-ok száma korlátozott, 32 byte vihető át egyszerre. Egy programban ennél több infót kellett átvinnem, ezért kénytelen voltam részekre bontani az átvitelt. Ekkor a Wire.beginTransmission(8) után következő buszra kiírt byte egy jelző byte volt, ami a slave-nak megmondta, hogy éppen melyik blokkot viszem át. A slave oldalon az adatok fogadására készített függvény ebből tudta, hogy az érkező adatokkal mely változókat kell tölteni!

Az alábbi példa programban a slave egy analóg bemeneten mért értéket long változóba olvas be. Direkt long, hogy látható legyen, hogyan kell byte-onként átvinni az értéket.  Amikor a master lekérdezte a slave-től az adatot, összerakja a byte-onként érkezett tartalmat egy long változóba és kiírja a soros monitorra, hogy majd lássuk mi jött. A fogadott értékeket visszaküldi a slave-nak, hogy erre is lássunk példát.

A master programja

/*
 * Ez a program demo két arduino I2C porton keresztül történő adatcseréjének bemutatására készült.
 * Ez itt a master programja. A mester másodpercenként adatot kér a slave-től, ami egy -500 és 523 közötti
 * long számot bont fel és byte-onként elküld. A mester programja összerakja a 4 byte-ot egy long
 * változóba és kiírja, valamint a 4 byte-ot azonnal vissza is küldi a slave-nak!
 */
#include<Wire.h>           
byte fogad1;  //ebbe a változóba olvassuk be a slave által küldött 1. byte-ot
byte fogad2;  //ebbe a változóba olvassuk be a slave által küldött 2. byte-ot
byte fogad3;  //ebbe a változóba olvassuk be a slave által küldött 3. byte-ot
byte fogad4;  //ebbe a változóba olvassuk be a slave által küldött 4. byte-ot
void setup()
{
  Serial.begin(9600);       // soros port inicializálása            
  Wire.begin();                 // I2C kommunikáció inicializálása, nincs cím, ezért master
}

void loop()
{
    Wire.requestFrom(8,4);      // a master kér négy bájtot a slave-től
    fogad1 = Wire.read();          //beolvassuk sorban a slave által küldött byte-okat   
    fogad2 = Wire.read();           
    fogad3 = Wire.read();           
    fogad4 = Wire.read();           
    Serial.print("Slave-tol fogadott:");    //kiírjuk a soros portra a fogadott byte-okat
    Serial.print(fogad1);Serial.print(",");Serial.print(fogad2);Serial.print(",");
    Serial.print(fogad3);Serial.print(",");Serial.print(fogad4);Serial.print("->");
    //kiírjuk a soros portra a long-nak összerakott értéket
    Serial.println(byteToLong(fogad1,fogad2,fogad3,fogad4));
    Wire.beginTransmission(8);  // elindít a master egy átvitelt a 8 című slave felé
    Wire.write(fogad1);         // küldi a a byte-okat sorban a slave felé
    Wire.write(fogad2);        
    Wire.write(fogad3);                         
    Wire.write(fogad4);                         
    Wire.endTransmission();     // vége az átvitelnek
    delay(1000);                                     
} 

long byteToLong(long inp1, long inp2, long inp3, long inp4)
//Ez a funkció 4 byte-ból csinál egy long változót és visszaadja az eredményt
{
  //4 byte long változóvá alakítása
  return ((inp1 << 0) & 0xFF) + ((inp2 << 8) & 0xFFFF) + ((inp3 << 16) & 0xFFFFFF) + ((inp4 << 24) & 0xFFFFFFFF);
}


Mint az alábbi példából látható lesz, a slave programja megszakításokkal dolgozik. Fut a loop-ban egy program, és csinálja amit kell. Ezt a programot a master részéről érkező adatkérés, illetve a mastertől küldött adatküldés megszakítja, és mindig a megfelelő függvény kerül meghívásra.


Slave programja:

/*
 * Ez a program demo két arduino I2C porton keresztül történő adatcseréjének bemutatására készült.
 * Ez itt a slave programja. A master adatkérésekor megméri az A0 portra kötött feszültséget
 * beolvassa egy long változóba, azt felbontja 4 byte-ra és byte-onként elküldi a masternek.
 * A mester programja (vissza) küld 4 byte-ot, amit a program fogadást követen long számmá alakít
 */
#include<Wire.h>
byte fogad1;
byte fogad2;
byte fogad3;
byte fogad4;

void setup()
{ 
  Serial.begin(9600);           // soros port inicializálása          
  Wire.begin(8);                // I2C kommunikáció inicializálása 8-as eszköz címmel (mivel slave, címet kell megadni)
  Wire.onReceive(slave_fogad);  //Ezt a funkciót hívja az Arduino, amikor adatot kap a mastertől
  Wire.onRequest(slave_kuld);   //Ezt a funkciót hívja meg az Arduino, amikor a master adatot kér a slave-től
  analogReference(DEFAULT);     //A tápfeszt veszi referenciának (5V)az analóg bemenetre kötött feszültség mérésekor
}

void loop()
{  
}

void slave_fogad ()     
//Ez a függvény akkor indul, amikor a slave értesítést kap, hogy a master adatot fog küldeni
{
  //4 byte-ot várunk a mastertől, és azt olvassuk be
  fogad1 = Wire.read();           
  fogad2 = Wire.read();           
  fogad3 = Wire.read();           
  fogad4 = Wire.read();
  //kiirjuk a soros portra a fogadott byte-okat          
  Serial.print("Mastertől fogadott:");    //Prints in Serial Monitor
  Serial.print(fogad1);Serial.print(",");Serial.print(fogad2);Serial.print(",");
  Serial.print(fogad3);Serial.print(",");Serial.print(fogad4);Serial.print("->");
  //visszaalakítjuk a fogadott 4 byte-ot long számmá és kiirjuk a soros portra
  Serial.println(byteToLong(fogad1,fogad2,fogad3,fogad4));
}

void slave_kuld()                            
//Ez a függvény akkor indul, amikor a master adatot kér a slave-től
{
  long kuldendo=analogRead(A0)-500; //a kipróbáláshoz az analóg bemenetet használjuk -500 és 523 között
  Serial.print("Masternek kuldve:");Serial.println(kuldendo);
  //felbontjuk 4 byte-ra a küldendő long értéket
  byte out1 = (kuldendo & 0xFF);
  byte out2 = ((kuldendo >> 8) & 0xFF);
  byte out3 = ((kuldendo >> 16) & 0xFF);
  byte out4 = ((kuldendo >> 24) & 0xFF);
  Wire.write(out1);              
  Wire.write(out2);
  Wire.write(out3);
  Wire.write(out4);
}

long byteToLong(long inp1, long inp2, long inp3, long inp4)
//Ez a funkció 4 byte-ból csinál egy long változót és visszaadja az eredményt
{
  //4 byte long változóvá alakítása
  return ((inp1 << 0) & 0xFF) + ((inp2 << 8) & 0xFFFF) + ((inp3 << 16) & 0xFFFFFF) + ((inp4 << 24) & 0xFFFFFFFF);
}

Kísérletezgetés közben jöttem rá, hogy nem mindegy hol helyezzük el a slave programjában az adatok írását ( wire.write()-okat ), mert tapasztalatom szerint kizárólag a master által kezdeményezett adatküldés kiszolgáló függvényében lehetnek. Ez a fenti példában ez a slave_kuld() függvény. Kipróbáltam olyan megoldást is, amiben a slave_kuld() függvény csak egy jelzést ad arra, hogy a master adatokat vár. Valahol a program egy másik részén akartam az adatokat elküldeni, nevezetesen a loop() ciklusban, amikor nekem megfelelő, és nem akkor amikor pont kéri a mester a slave_kuld() meghívásával. Sajnos ez így nem működött. Az adatok küldése megtörtént, de a master-hez 4db 255 tartalmú byte érkezett meg. Szóval az slave_kuld() függvénybe kell tenni a wire.write sorokat!! Elképzelhető, hogy valamit nem vettem észre, ezért még nem adom fel, de jelen tudásom szerint, csak így lehet küldeni!

A két Arduino közötti kommunikációt azért kellett megismernem, mert készítettem egy időjárás jelző állomást, aminek a vezérlő programja nem fért el egyetlen Arduino nano alaplapban. Így szükség volt kettőre, melyeknek kommunikálniuk kellett. Ha érdekel, akkor itt találod meg a leírásokat és mindkét alaplap programját.

————————————————————————-

Ugyanezt az időjárás állomást épp tovább fejlesztem. Immár 3 Arduino is működik benne, mert kiegészült egy szélsebesség és csapadékmérővel. Felmerült az igény, hogy a három eszköz közösen használjon egy I2C buszon működő FRAM-ot. Mégpedig úgy, hogy mindenki írhatja és olvashatja is ezt a ram-ot. Ehhez készítettem egy több master-es megoldást. Az Arduino-k sorban adogatják egymásnak a mester funkciót, és nem kell egyetlen mastert kijelölni, ami mindent adminisztrál, és adogatja az adatokat az egyes eszközök között. Itt találsz részletes leírást és forráskódokat!

————————————————————————-

Eltelt pár hónap. Lassan elkészül a beltéri egységgel kiegészített időjárás állomásom. Azonban ennek megvalósításában mégsem a multimaster megoldás nyert. Ennek oka, hogy egy-két naponta folyton lefagyott a rendszer. Nagyon fontos tapasztalatokat szereztem, mire megoldottam a lefagyási problémát. Sajnos azt nem tudom, hogy mi a lefagyás oka, de megfejtettem nagy nehézségek és egy laptop helyszíni kitelepítésével és a soros port monitorozásával, hogy mi történik. A problémát egy régi neoncsöves világítótest bekapcsolása okozza. Amit persze nosztalgiából nem fogok lecserélni az ügy miatt. Valamint ez bármilyen más indukciós gép is lehetne, ami jelentős zavarjeleket termel. Kb. minden 10.-ik felkapcsoláskor a három Arduino egyike a neoncső gyújtótrafójának impulzusaitól részben működésképtelenné vált. Feltételezem, hogy az I2C busz órajel kimenete fagy bele egy lehúzott 0 állapotba. Miközben a chip egyéb részei működtek tovább, pl. villogott a led, ami a szélkerék forgását jelezte. Az eredmény az lett, hogy teljesen megszűnt az I2C kommunikáció. Látszólag minden lefagyott, de valójában nem. Csak éppen az időjárás állomásom egyik tagja elakadt az aktuális I2C műveletnél. A többi chip vígan tette tovább a dolgát, erről mit sem tudva. Nagyon okosan építettem watchdog programrészt a programba, ami szépen újra is indította a chip-et egy belső reset-el 8 másodpercenként. Azonban ekkor is csak az első I2C műveletig jutott, hiszen valaki „fogja” az I2C buszt. Egyetlen megoldást találtam, reset jelet kell adni a többi Arduino nano-nak is. Így most van egy master az áramkörben, ami watchdog-al újra indítja magát, ha lefagy. Az egyik lábat el kellett használnom reset jelnek. Induláskor a setup első sora ennek a lábnak a kimenetre programozása, és egy LOW impulzus. Ennek hatására a többi nano is kap egy reset-et, újraindul és zajlik tovább az élet!

Nem várt hatása lett azonban ennek a megoldásnak. Mivel ugyanarról a tápról megy az egész, a programfeltöltés nehézkessé vált. Ugyanis programfeltöltéskor a chip kap egy reset-et. Most viszont a programozó (vagy az USB soros illesztő) hiába adna LOW jelet a reset bemenetre, azt éppen felhúzza masszívan egy másik Arduino kimenete, ami a programozón keresztül szintén kap tápot, tehát működik. A megoldás az lett, hogy be kellett tenni egy diódát a master kimenete és a slave reset bemenete közé. Persze a reset lábon van egy 10K-s felhúzó ellenállás is! Így nem kell az áramkörből kivenni az Arduino nano-t ahhoz, hogy a többi chip-be programot töltsek. Álom luxus kivitelben!

Mennyire volt hasznos amit olvastál? Értékelés után szövegesen is leírhatod megjegyzéseidet és véleményedet!

Kattints egy csillagra az értékeléshez!

Szövegesen is leírhatod véleményedet! Ha kérdésed van, ne felejtsd el megadni az email címedet!