Hibatűrő adatcsere két Arduino között

Elkészült az időjárás állomásom, ami a csapadékmennyiséget is méri. Tőle kb. 5 méterre található a locsolórendszerem vezérlője, amihez el kell juttatnom a csapadékmennyiséget, hogy tudjon róla, ha elegendő eső esett és nem kell elindítani a locsolást. Még csak most csinálom az említett adatcserét, de lassan elkészülök ezzel is. Mivel az időjárás állomásomban I2C busz eléggé leterhelt. Sok eszköz van rajta, és kb. 4-5 méteres kábellel vezettem be a ház oldalából a BME280 szenzort, így nem akartam még több vezetéket hozzákötni. Az I2C busz már így is csak 30khz frekvenciával működik üzembiztosan. Marad a soros port! A program feltöltéssel természetesen baj van! A program feltöltés idejére ki kell húzni a két berendezést összekötő soros kábelt, különben hibára fut a programozás. Sajnos így le kell mondani a debug funkcióról is, hiszen nem fog tudni a egyik eszköz sem adatokat küldeni a PC-re, amikor egymással beszélgetnek. Ezzel azonban nincs mit tenni, majd az LCD kijelzőket használom debug-ra, mert az szerencsére mindkét készüléken van. Másik megoldás, hogy mindkét berendezés programjában lehet időlegesen „szimulálni” a soros porton érkező adatokat, így belőhető a program működése, és csak amikor minden készen van, akkor „élesítem” a működést a valós programrészekkel. Érezhetően macerássá válik a hibakeresés.

Itt jön a következő probléma! Mivel a soros vonalon nem csak jelzéseket akarok küldeni, hanem összefüggő adatokat, nem mindegy, hogy mikor dugom vissza a soros kábelt. Esetleg lemaradhatok adatokról! Ha egyszer valahol elveszítek egy-két byte-ot egy kontakt hiba miatt, már nehéz újra megtalálni a szinkront! Ehhez a vevő oldalon úgy kell megírni a programot, hogy észre vegye, ha valami adat kiesés volt, és megtalálja az adatcsomagok elejét. Ezt csináltam meg általános módon!

Megoldásomban 5 értékes byte-ot küldök át egyik eszközből a másikba! Miért pont ötöt?! Megvalósítottam az időjárás állomásom és a beltéri egység között egy RF433 rádiós adó-vevő párral egy rádiós kapcsolatot. Itt 5 byte-os adatcsomagokat küldözgetek, aminek első byte-ja meghatározza, hogy milyen jellegű adat érkezik. Így több RF433 adót és vevőt is tudok használni a lakásban párhuzamosan, mindegyik a saját adatait adja, és csak azokat veszi figyelembe vételnél, ami neki szól. A maradék 4 byte-on át tudok vinni pontos időt, dátumot, byte, int és long típusú adatokat is. Ezért maradtam a soros kapcsolatnál is ebben az adatcsomag a szerkezetben. Tehát a feladat az, hogy 5 értékes byte-ot vigyek át úgy, hogy bármikor ki lehessen húzni az összekötő kábelt, és ne zavarja meg az egyik eszköz működését sem. Az se zavarja meg az átvitelt, ha pont adatcsomag átvitel közben szakad meg a kapcsolat A kábelt pl. a programfeltöltés idejére is ki kell húzni, tehát ez üzemszerű és valós helyzet, valamint a berendezéseimet is kikapcsolom időnként egymástól függetlenül!

Még nem is említettem, hogy mind az adó, mind pedig a vevő oldalon esetenként időigényes feladatok zajlanak. Pl. adatok átvitele egy lassúra állított I2C buszon, hosszú, egy-két tizedmásodperces rádiós átvitel stb. Tehát lesz olyan eset, amikor lemaradok a soros porton érkezett adatokról. Ez ugyebár nem gond, hiszen a hardveres sorosport az adatokat egy 64 byte-os pufferbe helyezi, amiket sorban ki tudok olvasni. Ez tehát pipa! Hacsak nem ömlik az adat, elég, ha néha-néha ránézek, hogy jött-e valami a soros vonalon!

Tovább haladás előtt esetleg érdemes megtekinteni a két Arduino soros porton történő adatcseréjével kapcsolatos tapasztalataimat. Leírtam a lehetséges problémákat, és néhány egyszerű hardverrel kapcsolatos információ is kiderült számomra, amikor ezzel foglalkoztam. Az összefoglaló itt található!

Nem találtam fel a spanyolviaszt! Elég öreg vagyok már, halottam valahol ilyen módszerekről. Az értékes infót keretezni kell ismert adatokkal, és azt kell figyelni, hogy a bevezető és a záró adatok hibátlanul megérkeztek-e?! Ha igen, akkor köztük van az értékes adat. Elővettem ennek elméletét, de a hibajavításokkal kapcsolatos dolgok is előkerültek, annak elmélete annyira bonyolult matematikailag, hogy nem volt kedvem elolvasni. Végül is nem az Apolló űrhajót akarom irányítani, megengedek magamnak egyelőre egy kis elméleti hibát is. Arra gyorsan rájöttem, hogy a keret adatok ne legyenek szimmetrikusak, ne legyen az átvinni kívánt adatcsomag (keret adatokkal együtt) páros. Arra is számítanom kell, hogy a pufferben már több adatcsomag várakozik, tehát fel kell ismerni a keretjelek mintázatát, kiemelni az értékes infót és így folytatni tovább a feldolgozást. Ha az adatok feldolgozásával elkészültek a további programrészek, majd jelezniük kell, hogy lehet kiolvasni a további adatcsomagokat, persze csak ha vannak. Így már nem annyira egyszerű a probléma, de még mindig nem bonyolult.

Az alapötlet az volt, hogy csinálok egy kilenc elemű tömböt, abba fogom sorban beolvasni az érkezett adatokat. Mindig amikor jön a soros porton egy byte, akkor azt beolvasom a tömb 1. elemébe. Persze előtte, minden elemét eggyel tovább léptetek, tehát a kilencedik elem elveszik, helyére kerül a nyolcadik, a nyolcadik helyére a hetedik és így tovább. Tehát a tömbben az adatfolyam utolsó kilenc byte-ját látom. Minden adatbeolvasás után megnézem a mintázatot, azaz ellenőrzöm az önkényesen választott keret adatokat. Ha egyeznek, akkor „heuréka”, megjött egy adatcsomag.

A programban deklaráltam egy struktúrát „soros_adat” névvel. A soros port adó és vevő oldali programja is ezzel a struktúrával létrehozott globális változót használja, ebből lépteti ki az adatokat a portra, illetve ebbe a struktúrába tölti be az érkezett adatokat. A profi programozók most biztosan sírnak a megoldástól, de nekem így volt kényelmes. Tehát a függvényeim csak akkor használhatók fel változtatás nélkül, ha adó oldalon a programban definiálásra kerül a soros_adat nevű struktúra és egy s_adat_tx globális változó, valamint vevő oldalon egy ugyanilyen nevű struktúra és egy s_adat_rx nevű globális változó. A változó nevek azért különböznek, mert várhatóan egy programon belül adni és venni is szeretnék. A vevő oldaldalon még egy soros_adat_tomb[] nevű tömböt is létre kell hozni globálisan!

A példa programot igyekeztem szokásomhoz híven jó alaposan kommentezni. Már holnap nem fogok emlékezni mit miért csináltam, ezért ennek jómagam is felhasználója vagyok.

Küldő oldal programja:

/***************************************************************************
 * Soros vonali teszt adó, ami 5 értékes byte-ot ad le a soros vonalon     *
 * de ezeket 4 byte keretjellel látja el. Fél másodpercenként küld adatot, *
 * az egyik byte sorszám jellegű.                                          *
 ***************************************************************************/
long ido_tmp=millis();
byte szam=0;  //ezt növelem ciklikusan, hogy lássam a vevő oldalon, hol járok az adatokkal
              //Így elvileg felismerhetem, ha elveszett adatcsomag.

struct soros_adat {     //A soros átvitelhez készített struktúra. Ezeket az adatokat fogja adni a soros vonalon keret-adatokkal ellátva
  byte tipus;           //az adatcsomag típusa
  byte byte0;           //az első byte
  byte byte1;           //második byte
  byte byte2;           //harmadik byte
  byte byte3;           //negedik byte
  bool ervenyes;        //érvényesség=1, ha ezt a bitet 1-re állítja a program, akkor történik meg a 
                        //továbbítás a doros vonalon, továbbítás megtörténte után visszáll 0-ra
};

soros_adat s_adat_tx;      //létrehozzuk soros_adat_tx  típusú, s_adat_tx nevű változónkat


void setup(void) {
  Serial.begin(2400);
  s_adat_tx.tipus=1;     //a továbbítandó adatok (ez csak demó, véletlenszerű értékeket adtam)
  s_adat_tx.byte0=133;
  s_adat_tx.byte1=200;
  s_adat_tx.byte2=21;
  s_adat_tx.byte3=44;
  s_adat_tx.ervenyes=0;   //még nem kell továbbítani
}

void loop() {
  soros_tovabbitas();
  if (ido_tmp+500<millis()) {     //félmásodpercenként teljesül a feltétel
    s_adat_tx.ervenyes=1;         //kérjük a függvénytől a továbbítást. Ha legközelebb
                                  //meghívásra kerül a függvény, akkor el fogja küldeni az adatokat
                                  //és az s_adat_tx.ervenyes változót 0-ra állítja.
    ido_tmp=millis();             //innen indul a következő adás időzítése
    szam++;                      
    s_adat_tx.byte0=szam;         //így legalább az egyik küldött byte változik (számol felfelé)
  }
}


void soros_tovabbitas() {
  if (s_adat_tx.ervenyes==1) {
    s_adat_tx.ervenyes=0;   //ne legyen még egyszer küldés, csak ha a programban valahol ezt 1-re állítom
    Serial.write(0);              //bevezető keret 1. byte
    Serial.write(255);            //bevezető keret 2. byte
    //a továbbítandó 5 byte adat kiírása a soros portra egymás után
    Serial.write(s_adat_tx.tipus);
    Serial.write(s_adat_tx.byte0);
    Serial.write(s_adat_tx.byte1);
    Serial.write(s_adat_tx.byte2);
    Serial.write(s_adat_tx.byte3);
    Serial.write(64);             //záró keret 1. byte
    Serial.write(128);            //záró keret 2. byte
  }
}

Fogadó oldal programja:

/****************************************************************************
 * Soros vonali vevő program, ami átvisz 5 értékes byte-ot további 4 byte   *
 * keretjel felhasználásával. Ennek a megoldásnak az előnye, hogy a soros   *
 * kapcsolat vezetékeit szabadon lehet megszakítani, nem sérül az adat.     *
 * A program időigényes dolgokat is csinálhat, lemaradhat adatokról,        *
 * de mindig csak a következő értékes adatcsomagot fogja visszaadni!        *
 ****************************************************************************/
int soros_adat_tomb[]={0,0,0,0,0,0,0,0,0};   //ebbe a tömbbe olvasom a soros porton érkezett adatokat
long ido_tmp=millis();

void setup() {
  Serial.begin(2400);   //soros port kezelés bekapcsolva 2400 boud-al
  Serial.println("Indul...");  // a soros port TX kivezetése még alkalmas arra, hogy PC-nek adatokat küldjön
                              //csak az RX-et használom, azon jönnek az adatok  
}

struct soros_adat {     //A soros átvitelhez készített struktúra. Ezeket az adatokat fogja adni a soros vonalon keret-adatokkal ellátva
  byte tipus;           //az adatcsomag típusa
  byte byte0;           //az első byte
  byte byte1;           //második byte
  byte byte2;           //harmadik byte
  byte byte3;           //negedik byte
  bool ervenyes;        //érvényesség=1, ha ezt a bitet 1-re állítja a program, akkor történik meg a továbbítás a soros vonalon, továbbítás megtörténte után visszaáll 0-ra
};

soros_adat s_adat_rx;      //létrehozzuk soros_adat típusú s_adat nevű változónkat


void loop() {
  soros_vetel();
  if (s_adat_rx.ervenyes==1) {
    Serial.print("kész:");                   //keret stimmel, tehát megjött az adat
    Serial.print(s_adat_rx.tipus);Serial.print(",");
    Serial.print(s_adat_rx.byte0);Serial.print(",");
    Serial.print(s_adat_rx.byte1);Serial.print(",");
    Serial.print(s_adat_rx.byte2);Serial.print(",");
    Serial.println(s_adat_rx.byte3);
    s_adat_rx.ervenyes=0; //így csak akkor fogjuk kiírni az érkezett adatokat, ha új érkezik
  }
  //annak bemutatására, hogy mi történik, ha a program más feladatokkal el van 
  //foglalva hosszú ideig, és közben jön egy csomó adatcsomag. Alábbi rész 5 
  //másodpercenként beiktat 2 másodpercet, amig nem olvassuk ki az adatpuffertű
  //tehát feltorlódnak az adatok a pufferben. D nem veszik el semmi, kiolvassa
  // gyorsan egymás után.
  if (ido_tmp+5000<millis()) {
    ido_tmp=millis();
    delay(2000); 
  }
}


void soros_vetel() {
  while (Serial.available()>0 and s_adat_rx.ervenyes==0) {   //ha van valami a soros pufferben, és az előző adatokat is feldolgoztuk már 
                                                             //akkor folytathatjuk a soros puffer ürítését                                   
    for (byte i=8;i>0;i--) {                                 //végig megyünk a tömbösszes elemén, de hátulról kezdve
      soros_adat_tomb[i]=soros_adat_tomb[i-1];               //minden tömbelemet a következő tömbelembe másolok 6. a 7.-be, 6. a 6.-ba stb.
                                                             //a 0. tömbelem üres szabad marad, abba olvasok egy elemet
    }
    soros_adat_tomb[0]=Serial.read();                            //kiolvasok egy byte-ot a soros pufferből és beteszem a 0. tömbelembe
    if (soros_adat_tomb[0]==128 and soros_adat_tomb[1]==64       //keret viszgálata
        and soros_adat_tomb[7]==255 and soros_adat_tomb[8]==0) { 
      //keret stimmel, ezért kiolvassuk az adatokat s_adat_rx struktúrába
      //az adatok sorrendja pont forított (tipus jött elősször, tehát az van a tömbben
      //a nagyobb index értéken)   
      s_adat_rx.tipus=soros_adat_tomb[6];
      s_adat_rx.byte0=soros_adat_tomb[5];
      s_adat_rx.byte1=soros_adat_tomb[4];
      s_adat_rx.byte2=soros_adat_tomb[3];
      s_adat_rx.byte3=soros_adat_tomb[2];
      s_adat_rx.ervenyes=1;                //ez jelzi a program más részeinek, hogy lehet az adatot  feldolgozni
                                           //Célszerű ezt 0-ra állítani feldolgozás után, de nem kötelező
      break;                               //itt most felfüggesztjük az adaok további olvasását. Ha a program 
                                           //további részei feldolgozták ami jött, akkor majd tovább olvasunk, addig nem
                                           //ha közben megtelik a soros adatpuffer 64 byte-ja, akkor így jártun.

    }
  }        
}

Mennyire volt hasznos amit olvastál?

Kattints egy csillagra az értékeléshez!

Szövegesen is leírhatod véleményedet!