Multithreading 8 bites AVR-en, avagy villog a LED, de rohadt bonyolultan ám

Vettem egy Controllino MAXI-t, amiben ATMega2560-as csip van, és lényegében Arduino kompatibilisen lehet programozni. Ki kell próbálni, hogy ha esetleg nem működne, akkor vissza tudjam küldeni! Nosza, csináljunk hozzá Hello World programot! De ne akármilyet! Többszálút!

A fűtés vezérlésemet akarom áttenni erre az új vezérlőre, mert a jelenlegi megoldásom offline működik egy hasonló, de Ethernet vezérlő nélküli PLC-vel. Egy Industrial Shields márkájú vezérlőm van jelenleg, ami teljesen jó, de ez a Controllino egy kicsit profibbnak néz ki, úgyhogy ki akartam próbálni.

Ha már vettem egy ilyen kütyüt, elindult a fejemben a hangya, hogy akkor ne csak bájtokat küldözgessünk az Ethernet portján, hanem tegyünk rá rendes web servert! És ha már web szervert teszünk rá, akkor legyen interaktív a weboldal, és tudjon kiszolgálni több klienst egyszerre, mert az marha fontos! Az Ethernet illesztője egy WizNet5100, ami elvben 4 egyidejű TCP kapcsolat kezelésére alkalmas. Ha WebSocket-et használok, akkor az azt jelenti, hogy ténylegesen 4 klienst fogok tudni kezelni, mert ahhoz elegendő egyetlen nyitott kapcsolat, hogy működjön a kétirányú kommunikáció a weboldallal.

És ezenközben a kütyü legyen képes a vezérlési feladatát is elvégezni, de úgy ám, hogy ne kelljen a vezérlés logikáját interruptokra tenni, hanem a szokásos végtelen ciklusban lekérdezem a bemeneteket, frissítem a kimeneteket módon lehessen programozni. Valójában komolyabb terveim vannak a kütyüvel, mint csak a fűtésvezérlés, ezért fontos, hogy a TCP kapcsolat kezelésétől teljesen függetlenül lehessen a logikát programozni időzítési garanciákat bizonyíthatóan betartva.

Itt van a bökkenő: két imperatívan megírt programnak kellene egyszerre futni a vezérlőn: a vezérlés ismétlései között kellene kezelni a Web kapcsolatokat, de a Web kapcsolatok kezelését a legésszerűbben blokkoló algoritmussal lehet megvalósítani. A két programrész tehát nagyon nehezen tud osztozkodni egy szálon! Pláne, ha egyszerre több Web kapcsolatot is szeretnénk nyitva tartani. Csináljunk tehát többszálú programot az ATMegán!

Elkezdtem keresgetni, hogy vannak-e ATMegához való multithread libraryk, de nem találtam semmit. Fórumbejegyzések vannak, hogy elvileg lehet persze, de nagyon bonyolult, és egyáltalán minek akarnál ilyet csinálni? Bonyolult? PIHA!

Lássuk, hogy kell megcsinálni a többszálú végrehajtást:

* Minden szálnak kell egy saját stack. A 2560-as csip bőkezű, 8K SRAM van benne, tehát a 4 Web feldolgozónak, és a vezérlésnek is simán adhatok 1-1K-t, ami bőven elegendő, és még marad 3K az adatoknak. ("Teszek bele egy TCP-IP stacket és egy bootlogót." - Hiena)
* A szál állapotát, amikor nem fut éppen, akkor a saját stackjén tároljuk. Egy ilyen mentés 36 bájtot vesz igénybe - 32 általános regiszter, 3 bájt visszatérési cím, 1 bájt SREG -, ezzel számolni kell, amikor a stackünk méretét tervezzük.
* Meg kell valósítani a context switch-et
* PROFIT$$$44$!

Hogyan működik a context switch? A legegyszerűbb úgy elképzelni, hogy a CPU teljes állapotát elmentjük a stack-re, vagy visszatöltjük a stackről. Jó, de akkor ugyanott leszünk, mint előtte, mitől lesz ez context switch? Ott van a barbatrükk, hogy az állapotmentés után és visszatöltés előtt átírjuk a stack pointert, hogy immáron a másik thread állapotáról folytassuk a végrehajtást. Kód itt

Ilyen egyszerű. Illetve mégsem, mert a szálat létre is kell hozni. Itt van a róka fogta csuka, mert a szál folytatható stackjét úgy tudjuk felépíteni, hogy lefuttatjuk az állapotmentés programrészt a szálon... De még nincs min, mert még nem hotzuk létre! A trükk az, hogy az eredeti szál stack-jének tetejét kell leklónozni olyan mélységig, ahonnan a legbelső függvényhívás történt. És ezután a call által letárolt visszatérési címet, amit majd a ret (iret) utasítás ki fog venni a stackből át kell írni az új szál belépési pontjára. (A regiszterek értékei valójában lényegtelenek, hiszen egy paraméter nélküli függvényt fogunk futtatni, tehát valójában másolni is felesleges, akár inicializálatlanul is hagyhatnánk.) Ezzel egy végrehajtható - akár igazi is lehetne - stacket kapunk, amit már bármikor lehet aktiválni a context switch művelettel. Kód itt

És mire lehet ezt használni? Csinálhatunk egy kétszálú programot, ami az egyik szálon kigyújt, a másikon pedig leolt egy LED-et. Ezért a hatalmas eredményért máris megérte a fáradozás! Kód itt

Tényleg bonyolult ez? Nézőpont kérdése. A szálkezelés kódja 100 sor alatt van. De persze elsőre nem működött, és serialon keresztül kidebuggolni nem volt egyszerű. Vannak benne rejtett aknák? Lehetséges, nincs agyontesztelve a kód. De ez a processzor olyan egyszerű, hogy nincsenek láthatatlan állapotai, úgyhogy ha egyszer működik, akkor nem sok minden tud elromlani. (Amikor ilyen kijelentéseket teszünk, akkor kell elkezdeni végrendeletet írni.)

A továbbfejlesztés iránya az lesz, hogy készülni fog hozzá egy statikus ütemező, ami előre kiosztott időszeletek szerint adja a lovat a szálak alá. Ezért lett a kontextusváltás egy időzítő interrupt-kezelőben megvalósítva, hogy időzítőre is végre lehessen hajtani ugyanezt viszonylag kis overhead mellett. Plusz a szálak esetleg nem (csak) statikusan lesznek ütemezve, hanem önkéntes alapon adják majd tovább a vezérlést, ha nincs éppen dolguk.

Lehet csinálni stack overflow ellenőrzést a context-switch-be. Sajnos a hardver nem tudja monitorozni a stacket, hogy átlépett-e egy előre beállított címet. Lehet csinálni watchdogot a prioritásos szálnak, hogy valóban lefut-e kellő periódus szerint. A többi szálat pedig ellenőrizheti a legmélyső szál, így ha bármi gikszer van, akkor automatikus reboot történik.

Az egész akkor lesz látványos, ha valóban csinálok hozzá egy WebSocket-es szervert, ami real-time küldi az adatokat egy böngészőbe, illetve onnan parancsokat vár. Addig még sok víz lefolyik a Dunán.

Hozzászólások

Már csak pár követő kell meg GNU userland és kész az Ashux :) Anno nekem is a megszakításokat ajánlották mikrovezérlőn, mikor javás tudásommal kérdeztem, hol a multithread támogatás. De ha így müxik, ezer gratula.

"Elkezdtem keresgetni, hogy vannak-e ATMegához való multithread libraryk, de nem találtam semmit. Fórumbejegyzések vannak, hogy elvileg lehet persze, de nagyon bonyolult, és egyáltalán minek akarnál ilyet csinálni? Bonyolult? PIHA!"

https://freertos.org/

Ilyet régen én is csináltam csak assemblyben, mert az egész kód abban volt, így például lehetett spórolni a lementett regiszterekkel (egy bitmapet tároltam arról, hogy mit használ az adott szál és az alapján mentettem). Mondjuk akkor még nem is volt ekkora Atmega..

Ez a statikus regiszterallokáció fincsi dolog. Valahogy az avrgcc is támogatja, de nyilván csak úgy működik, ha mindent újraforgatunk ezzel a beállítással.

Ezt lehetne odáig fokozni, hogy két külön szálat két külön statikus regiszterallokációval forgatunk :-). Brrr, belegondolni is rossz. Ennél ésszerűbb ugyanazokat a regisztereket befoglalni, de minden szálon másra használni. A baj az vele, hogy eleve interruptban szokás ilyet használni, az interrupt pedig bármelyik szálon jöhet... Szóval nem egyszerű ez.