egy mobil robot építésének története

Robotkocsi

Robotkocsi

Command & Conquer

2018. március 03. - dralisz82

Áttérve az mbed OS 5-ös verziójára azonnal sínre került a soros portos kommunikációért és a parancsok feldolgozásáért felelős programmodul fejlesztése. Ebben a posztban bemutatom, hogy hol volt az áttörés kulcsa, milyen módon és miért úgy valósítottam meg ezt a részt.

Az eredmény egy olyan parancsértelmező, mely a parancsszó után tetszőleges számú argumentumot is tud kezelni, ráadásul buffereli a beérkezett parancsokat, tehát az aktuális parancs végrehajtása nem akadályozza a következő parancs fogadását.

Az mbed OS 5-ben végre nem akad össze az UART megszakításkezelője az RTOS szálkezelésével, így egyszerűbbé vált a program, melyet immáron debugger használata nélkül is jól meg tudtam írni.

Miután a parancssor beolvasása már jól működött, elkezdtem a kapott string feldolgozását elsősorban az strtok() függvényre támaszkodva. Amikor ezzel is készen voltam, átalakítottam a programot úgy, hogy a megszakításkezelőben csak a parancssor beolvasása történjen, de a feldolgozás már egy parancsértelmező szálban fusson. Az ISR és a szál között pedig az operációs rendszer által nyújtott Mailbox funkció segítségével valósítottam meg a kommunikációt. A végleges kódot ismertetem a továbbiakban.

A megszakításkezelő a dobozban látható formára egyszerűsödött:

char command[20];
int commandPos;

typedef char str20[20];
Mail<str20, 16> cmdMailBox;

 

void gotChar() {
   str20 *mail;

   char c = BT.getc(); // read incoming character

  if(c != '\n')
     command[commandPos++] = c; // receive characters till EOL
   else
     command[commandPos++] = '\0'; // replace EOL with null terminator

  if(c == '\n' || commandPos == 20) { // if EOL or buffer is full...
    if(commandPos == 20)
      command[19] = '\0'; // Better would be some error handling here
    mail = cmdMailBox.alloc(2); // 2 ms timeout
    if(mail != NULL) {
      strcpy(*mail, command);
      cmdMailBox.put(mail);
    }
    commandPos = 0;
  }
}

Tehát szépen töltögetjük a command névre hallgató buffert, mely aktuálisan 20 karaktert tud tárolni. Ha sorvége karaktert kapunk, akkor string lezáró null értéket írunk a buffer következő elemébe. Ha megtelik a buffer, akkor az utolsó elemet szintén 0-ra állítjuk, és átadjuk az értelmezőnek, hátha... Itt mondjuk elkéne némi hibakezelés, de ilyen apróságokkal most nem foglalkozok. Egyelőre elég beszúrni ide egy kommentet, ami erre a lehetőségre figyelmeztet.

Ha lezártuk a stringet a lezáró nullával, akkor átadom a Mailboxnak, majd nullára állítom a commandPos nevű számlálómat, ami a beérkezett karakterek számát mutatja, illetve a command buffert indexeli, és várom a következő parancs beérkezését.

A blokk elején látható, hogy definiáltam egy str20 típust, mely a command buffer char[20] típusával megegyezik. A későbbiekben kiderül, hogy miért jó ez (érthetőbben fog kinézni a kód egy helyen). A Mailbox ilyen típusú elemeket továbbít, továbbá 16 parancsot tároló queue van benne, ahogy azt a konstruktorban megadtam.

A Mailbox .alloc() függvényével tudunk lefoglalni egy újabb elemet. Ez a függvény siker esetén egy pointerrel, sikertelenség esetén 0-val tér vissza. Akkor lesz sikertelen a hívás, ha a 16 elemű buffer tele van.

Ha nem adnánk meg egy 0-nál nagyobb timeout értéket paraméterként, akkor a végtelenségig blokkolódna, várva, hogy egy hely felszabadul a bufferben. Mivel viszont megszakításkezelő rutinban vagyunk, aminek a prioritása magasabb, mint az elemeket fogyasztó parancsértelmező szálnak, ez esetben nem tudna ürülni a buffer, így programunk beragad ebbe az állapotba, ezt hívják deadlock-nak.

A 2 ms-os timeout elégséges ahhoz, hogy lefoglaljunk egy helyet a bufferben, ha van szabad. Ha tele a buffer, akkor 0-val térünk vissza, és eldobjuk az adott parancsot. Ha van hely, akkor az strcpy() függvénnyel átmásoljuk ide a command string tartalmát, hiszen az a megszakításkezelő következő hívásakor már felül fog íródni, tehát nem elég pusztán egy rá mutató pointert átadnunk.

 A parancsértelmező szál nagyon egyszerűen néz ki:

Thread cmdHandlerThread;

void cmdHandlerMain() {
   while(1) {
     handleCommand();
     Thread::wait(2000); // For testing purposes only
   }
}

 

int main() {

  cmdHandlerThread.start(cmdHandlerMain);

...

Egy végtelen ciklusban hívjuk a handleCommand() függvényt, mely blokkolódva vár arra, hogy új elem érkezzen a Mailboxon keresztül, majd feldolgozza azt. A tesztelés kedvéért beiktattam egy 2 másodperces várakozást is a ciklusba, ezzel imitálva egy komplex parancs feldolgozásának extrém hosszú időigényét. Így lehetséges akár manuálisan is annyi paranccsal elárasztani a rendszert, hogy beteljen a 16 elemű buffer. Az ilyen tesztelés azért fontos, mert eleinte nem volt 2 ms-os timeoutom az alloc() függvényben, melynek hatását fentebb ecseteltem.

A handleCommand() függvény végzi a beérkezett string megtisztítását és feldarabolását (tokenizálását):

void handleCommand() {
   char *trimmedCommand;
   const char separator[2] = " ";
   char *token;

   char cmd[20];
   char args[4][20] = {0};
   int argIdx = 0;

   osEvent evt = cmdMailBox.get();
   if (evt.status != osEventMail)
      return;

   str20 *mail = (str20*)evt.value.p;
   trimmedCommand = trimwhitespace(*mail);


   /* get the first token */
   token = strtok(trimmedCommand, separator); // TODO: replace with strtok_r()

   if(token == NULL)
      return;

   strcpy(cmd, token);

   /* walk through arguments */
   while( (token = strtok(NULL, separator)) != NULL ) {
      strcpy(args[argIdx++], token);
   }

   cmdMailBox.free(mail);
   execCommand(cmd, argIdx, args);
}

Először is megpróbálunk egy parancssort kivenni a Mailboxból. Ha ez sikertelen, visszatérünk, siker esetén folytatódik a végrehajtás. Az osEvent típusú struktúra tartalmazza a pointert a megszakításkezelőben átadott stringre.

A kapott string elejét és végét a trimwhitespace() függvény megtisztítja a szóköz, tabulátor és kocsivissza karakterektől (újsor karakter itt már nem lehet a stringben, mert az ISR azok mentén határolja el a beérkezett parancssorokat).

Az strtok() függvénnyel a separator karakterek mentén részekre bontjuk a beérkezett parancsot. Érdemes megfigyelni, hogy a separator karaktertömb két elemű, pedig csak a szóközt tartalmazza. Ez azért van, mert ilyen esetekben mindig helyet kell biztosítani a stringlezáró nullának is, melyet a fenti esetben automatikusan kitölt a fordító.

Az strtok() függvény tárol egy belső állapotot, így többször egymás után meghívva szép sorban visszaadja a stringünk szeparátorok által elválasztott részeit. Emiatt a belső állapot miatt ez a függvény nem thread safe, tehát, ha több szálból is használni kívánjuk, érdemes az strtok_r() függvényt használni, mely egy általunk megadott helyen tárolja a belső állapotát. Egyelőre ezen az egy helyen használom a példakódban, de én is át fogom írni, nehogy a későbbi fejlesztés során misztikus hibákat kelljen debugolnom. Érdemes az ilyesmit kommentben felírni a kódba is.

A függvény első hívása során átadjuk neki paraméterként a kiinduló stringet, ekkor kimenetként megkapjuk a parancsszót.
A további hívások esetén, ha NULL-t adunk át első paraméterként, akkor a legutóbb kapott string feldolgozását fogja folytatni a függvény. Ilyen módon kinyerjük, és egy tömbbe írjuk az argumentumokat is.

Végül az execCommand() függvény végzi magát a parancsvégrehajtást:

void execCommand(char *cmd, int argc, str20 *args) {

  if(!strcmp(cmd, "red"))
    color = 'r';

  if(!strcmp(cmd, "green"))
    color = 'g';

  if(!strcmp(cmd, "blue"))
    color = 'b';

  if((!strcmp(cmd, "rgb")) && argc == 3) {
    color = 'x';
    red = atoi(args[0]);
    green = atoi(args[1]);
    blue = atoi(args[2]);
   }
  printf("%s %d; %s, %s, %s, %s, %d#%d#%d\n", cmd, argc, args[0], args[1], args[2], args[3], atoi(args[0]), atoi(args[1]), atoi(args[2]));
}

Itt nyer értelmet a typedef char str20[20]; azaz az str20 típus definiálása: Az args változó így nem egyszerűen char** típusú lesz (ami ráadásul hibás elgondolás lenne, mert az csupán egy mutatóra mutató mutató, nem tartalmaz méretinformációkat), hanem a fordító tudni fogja, hogy 20 elemű tömbök tömbjéről van szó, így kényelmesen hivatkozhatunk az argumentumokra args[index] formában. Deklarálhatnánk ezt a változót úgy is, hogy char (*args)[20], de ennél sokkal egyértelműbb a str20 *args forma.

Ebben a példakódban a kártyára épített RGB LED-et vezérlem az argumentum nélküli red, green, blue és a 3 argumentumot váró rgb parancssal.

Nem utolsósorban ebben a függvényben használhatjuk a printf()-et is debugolási célokra, hiszen a cmdHandlerThread szálban vagyunk. Ha a megszakításkezelő rutinból (ISR-ből) próbálnánk ezt, akkor nem működne.

Ezekből a részekből épül fel a parancsértelmező, amit az autó szoftverébe fogok beépíteni, és ami a 4 példaparancs helyett jóval több parancsot fog értelmezni. A Mailboxos megoldásnak nagy előnye, hogy abba több forrásból is dobhatunk be végrehajtandó utasításokat. Ez azért hasznos, mert a jövőben a jelenleg használt Bluetoothos interfészen kívül egy másik interfészt is tervezek kialakítani, amin keresztül a Freedomboard egy fedélzeti Raspberry Pi-vel tud kommunikálni. A cél, hogy a Raspberry Pi legyen a fő vezérlőegység, futtassa a számításigényes algoritmusokat, kezeljen komplexebb perifériákat (pl. kamera, LIDAR), a Freedomboard feladata pedig az egyszerűbb perifériák alacsonyszintű kezelése lesz. Erről az architektúráról egy következő cikkben fogok bővebben írni, a soron következő poszt viszont a változatosság kedvéért ismét hardveres témájú, forrasztógyanta szagú lesz.

A bejegyzés trackback címe:

https://robotkocsi.blog.hu/api/trackback/id/tr6313677848

Kommentek:

A hozzászólások a vonatkozó jogszabályok  értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai  üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a  Felhasználási feltételekben és az adatvédelmi tájékoztatóban.

Nincsenek hozzászólások.
süti beállítások módosítása