Á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.