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

Robotkocsi

Robotkocsi

Ész az agyban

avagy gondolatok a vezérlőszoftverről

2017. október 13. - dralisz82

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.

 

A bejegyzés trackback címe:

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

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