Régi adósságom az autó fedélzeti számítógépén futó szoftverről írni. Ezt most pótolom, a legelső, összedobott szoftver ismertetésével, annak apróbb hibáival, korlátaival együtt, hogy ezúttal is tudjunk miből tanulni.
A vezérlőszoftver első verzióját körülbelül 2 óra alatt írtam a Kutatók Éjszakája előtti délutánon, így igyekeztem a lehető legegyszerűbb megoldásokat használni, a legszükségesebb funkciókra koncentrálni.
A szoftver feladatai a következők:
- parancsok fogadása Bluetoothon keresztül
- perifériák (hajtás, lámpák) vezérlőjeleinek kapcsolása
- valamilyen periodikus időzítő használata az index villogtatásához
Mivel a parancsokat azonnal szeretnénk érvényre juttatni, de az index villogtatása jól látható periódusidővel történik, két lehetőségünk van:
a) Milliszekundumos periódussal pörgetünk egy ciklust, melyben elvégezzük a feladatokat, emellett folyamatosan léptetünk egy számlálót, és minden pl. 500-zal osztható értékénél invertáljuk az index vezérlőjelét.
b) Több szálon futtatjuk a programot, lesz egy gyorsan pörgő ciklusunk, mely a parancsokat dolgozza fel szinte azonnal, valamint lesz egy fél másodperces periódusidejű, mely az indexet villogtatja.
Az a) megoldás előnye, hogy egy szálon fut a szoftver, mely (legalábbis kezdetben) egyszerűbbé teszi azt.
A b) megoldás előnye, hogy több párhuzamos szál futtatásával jobban elkülöníthetők az egyes részfeladatok, kevésbé zavarják egymás végrehajtását, ráadásul a kód is hamar jóval átláthatóbbá válik, mint az a) esetben.
Mivel az mbed környezet készen ad nekünk egy beágyazott operációs rendszert, mely szolgáltatásait rendkívül egyszerűen lehet használni, nem lehet kérdés, hogy a b) pontban leírt út lesz a célravezető.
Tehát lesz
- egy szálunk (ez lesz a main thread), ami a parancsok fogadását és végrehajtását végzi
- egy szálunk, ami az index villogtatását végzi (szükség esetén)
- egy szálunk, ami egy heartbeat LED-et fog villogtatni (ez jelzi, hogy a programunk fut, elindult, nem omlott össze)
Először a main() függvényben el kell végezni a hardverelemek, illetve a belső változók inicializálását, majd el kell indítani a szálakat.
Az index és a heartbeat szálat explicit definiálni kell, majd elindítani, a fő szál viszont már adott, ez futtatja a main() függvényt, így ezzel csak annyi a dolgunk, hogy gondoskodjunk róla, hogy soha se érjen véget a futása, mert ez esetben az egész program futása véget ér, a rendszer leáll.
Ezt a célt legegyszerűbben úgy érhetjük el, ha egy végtelen ciklust teszünk a main() függvény végére, amiből sosem lépünk ki, de kifinomultabb megoldás egy szemaforra, vagy signalra váró wait() hívást használni, mert így nem foglaljuk a processzort a többi szál elől egy ciklus fölösleges pörgetésével. Hogy pontosan mikor melyik megoldást érdemes használni, az mindig az adott feladattól függ.
/**
* Heartbeat thread main loop
*/
void hb_thread_main(void const *argument) {
while (true) {
heartbeat = !heartbeat;
Thread::wait(200);
}
}
int main() {
...
printf("Starting threads\n");
hb_thread = new Thread(hb_thread_main);
...
// main control loop
while(1) {
if(BT.readable())
gotChar();
...
}
}
A fenti példán keresztül látható, hogy milyen egyszerű szálakat kezelni az mbed RTOS-ben. Csupán definiálni kell egy függvényt, melyet a szál fog végrehajtani, majd létre kell hozni egy Thread objektumot, melynek paraméterként átadjuk a függvény nevét (a háttérben igazából a címét).
A Blootooth modulhoz tartozó soros portot kezelő Serial osztály lehetővé teszi megszakításkezelő rutin csatlakoztatását, így a parancsok végrehajtását áttehetnénk egy ISR-be, azonban a soros portok megszakításkezelése az mbed RTOS használata mellett hibásan működik (nem töröl egy megszakítást jelző flag-et egy regiszterben). A flag manuális törlésére nem találtam módot (itt jön ki a hátránya az mbed-hez hasonló absztrahált környezeteknek), így a polling módszert fogom használni a main threadben.
A soros portot "pollozó" és a kapott parancsokat végrehajtó végtelen ciklus egyúttal gondoskodik róla, hogy a main() függvényből soha ne lépjünk ki, bár így pazarlóbban bánunk a CPU-val, mintha a megszakítást használnánk.
A parancsok, mint egyedi karaktereket küldjük a soros porton keresztül, tehát minden kapott karakter egy parancs. Ez egy szinten túl az átláthatóság rovására megy, de kezdetnek abszolút megfelelt:
/**
* UART Interrupt handler
*/
void gotChar() {
// TODO should clear RBR flag here
char c = BT.getc(); // read incoming character
// interpret received character as a command to switch color
switch(c) {
case 'f' : f_forward = true; f_backward = false; break; // forward
case 'b' : f_forward = false; f_backward = true; break; // backward
case 'h' : f_forward = false; f_backward = false; break; // halt
case 'l' : f_left = true; f_right = false; break; // left
case 'r' : f_left = false; f_right = true; break; // right
case 's' : f_left = false; f_right = false; break; // straight
case 'a' : f_index_left = true; f_index_right = false; break; // index left
case 'd' : f_index_left = false; f_index_right = true; break; // index right
case 'q' : f_index_left = false; f_index_right = false; break; // index off
case 'n' : f_lamp = !f_lamp; break; // lamp
case 't' : f_headlight = !f_headlight; break; // headlight
default: f_index_left = true; f_index_right = true; // ignore invalid command, put emergency indicator on
}
printf("Command: %c\n", c);
}
A gotChar() függvényt végül nem ISR-ként használtam, hanem pollozva az UART-ot a main threadből hívom.
A beérkező karakterek alapján flag-ként használt globális változókat módosítok, melyek alapján a fő programhurokban, illetve az index kezelő szál fő hurkában vezérlem a digitális kimeneteket.
**
* Index thread main loop
*/
void index_thread_main(void const *argument) {
while (true) {
if(f_index_left)
index_bal = !index_bal;
else
index_bal = 0;
if(f_index_right)
index_jobb = !index_jobb;
else
index_jobb = 0;
Thread::wait(500);
}
}
// main control loop
while(1) {
if(BT.readable())
gotChar();
if(f_forward)
hajtas_elore = 0.7f;
else
hajtas_elore = 0.0f;
if(f_backward) {
hajtas_hatra = 0.8f; tolatolampa = 1;
} else {
hajtas_hatra = 0.0f; tolatolampa = 0;
}
if(f_left) {
kormany_bal = 1; f_index_left = true; f_index_right = false;
} else {
kormany_bal = 0; f_index_left = false;
}
if(f_right) {
kormany_jobb = 1; f_index_left = false; f_index_right = true;
} else {
kormany_jobb = 0; f_index_right = false;
}
if(f_lamp)
nappali_feny = 1;
else
nappali_feny = 0;
if(f_headlight)
reflektor = 1;
else
reflektor = 0;
wait(0.02f);
}
A fenti kód bár szög egyszerű, de működik. Egyetlen apró hiba csúszott csak bele: ha befejezzük a kanyarodást, akkor az automatikus indexelést úgy kapcsoljuk ki, hogy a kézi indexelés soha nem fog működni.
Ezen az segítene, ha a gotChar() függvényben az 's', mint straight parancs fogadásakor állítanánk false-ra az index flag-eket a kormánymű vezérlőjeleivel együtt, így az "if(f_right)" és "if(f_left)" feltételek "else" ágából kivehetnénk a flagek false-ba állítását.
Ezt a hibát az első verzióban benne hagytam, hogy szemléltessem vele, hogy milyen könnyen össze tud akadni két funkció, ha nem fordítunk rájuk kellő figyelmet, valamint azt, hogy tesztelés nélkül nem lehet szoftvert fejleszteni.
Érdemes a program architektúráját úgy megválasztani, hogy átláthatóbb legyen a működés, kisebb eséllyel akadjanak össze programrészletek, így kevesebb hibát vétünk, ha pedig mégis, akkor hamarabb kiderül, hogy hol van a probléma. Ez különösen nagy projekteknél látványos költségcsökkenést eredményez, mert mint tudjuk: az idő költség. Ne pazaroljuk!
Írok majd a program második, sokkal struktúráltabb változatáról is, érdemes lesz összehasonlítani az itt ismertetettel.