ESP32 - simulátor pre Linux
Pri vývoji komplexnejších aplikácií pre ESP32 môže nahrávanie kódu do zariadenia zabrať pokojne aj niekoľko minúť. Aby som sa vyhol čakaniu napísal som malý, jednoduchý a dosť nepresný simulátor.
Prečo nie emulátor?
Emulácia hardvéru je pekne komplexná záležitosť. Emulátor jadra ESP32 je pomerne mladý dá sa povedať, že ešte nie je celkom dokončený. Jadro je však z celého emulátora tá najjednoduchšia časť.
Omnoho väčším problémom je emulácia zvyšku hardvéru, ktorý obsahuje mikrokontrolér. Patrí sem napr. DMA hardvér, ktorý môže pristupovať k pamäti paralelne voči jadru, zvukové vstupy / výsupy, PWM kanály atď. Takmer každý hardvér môže zároveň ešte interagovať s DMA. Vstupy a výstupy sa dajú rôzne konfigurovať a rôzne pripájať k externým zariadeniam. Najhoršie na tom celom je, že referenčná dokumentácia nie je úplná, takže sa nedá presne určiť, čo by zariadenie malo vlastne robiť.
Presná emulácia by bola extrémne zložitá a benefity, ktoré by som tým získal nemajú pre mňa ktovie aký význam. Omnoho jednoduchšie je reimplementovať niektoré funkcie z ESP-IDF a abstrahovať od práce s hardvérom.
Simulátor
Vývojové prostredie ESP-IDF obsahuje kompilátor (gcc), pomerne malé knižnice pre prístup k hardvéru, rôzne utility (napr. logovanie) a zopár externých viac-či menej multiplatformových knižníc.
Kompilátor
Simulátor pracuje tak, že skompiluje normálnu spustiteľnú binárku pre Linux. Prvý komponent, ktorý k tomu potrebujeme - kompilátor gcc stačí len nainštalovať.
Knižnice pre prístup k hardvéru
Implementovať knižnice pre prácu s hardvérom by bolo veľmi náročné a ako som spomínal, dokumentácia nie je kompletná, takže ani nie je jednoduché zistiť, čo by mal hardvér robiť v rôznych okrajových prípadoch. Takže na podporu hardvéru sa môžme rovno vykašlať ;)
Omnoho jednoduchšie je izolovať prácu s hardvérom do pár funkcií, ktoré sa použijú namiesto priameho prístupu k hardvéru. Ako príklad použijem audio výstup v mojom internetovom rádiu.
Na reálnom hardvéri zabezpečuje audio výstup I2S rozhranie, čo je extrémne extrémne komplexný kus hardvéru. Tá vec je šialená. V dokumentácii tvrdia, že je to digitálne sériové audio rozhranie ... ktoré ale dokáže fungovať aj ako vstup. Dokonca dokáže fungovať aj ako analógový audio vstup / výstup. Dokonca má LCD režim (tj. digitálny paralelný max. 24-bitový výstup). Dokonca má aj vstupný LCD režim teda vlastne režim kamery s paralelným vstupom + 2 ďalšími vstupmi pre hsync / vsync. Okrem toho má niekoľko deličov frekvencií, vlastný oscilátor, viacero hodinových vstupov / výstupov ...
Jednoducho I2S je strašne komplexný hardvér. Jeho emulácia by bola veľmi náročná. Pritom na audio výstup by stačilo pár jednoduchých C-čkovych funkcíí.
void audio_output_init(void); void audio_output_set_sample_rate(int rate); void audio_output_write(audio_sample_t *buf, size_t samples_count);
Na použitie audio výstupu nám stačí rozhranie, ktoré obsahuje inicializáciu audio výstupu, nastavenie počtu samplov za sekundu a zápis audio dát.
Tieto 3 funkcie stačí napísať v 2 variatoch. Raz pre reálny hardvér s I2S funkciami a druhý krát pre simulátor napr. pomocou alsa API. Obe implementácie sa budú rovnako volať a navonok budú pracovať rovnako. Jeden však pobeží na reálnom hardvéri, druhý na počítači s Linuxom.
Kompletný kód audio výstupu mám zverejnený na githube.
Utility
Z utilít som implementoval zatiaľ 2 celky. Prvým je logovanie, kde som len nahradil funkciu pre zápis do UART-u obyčajným printf.
Druhým je systém eventov, ktorý je veľmi užitočný pri komunikácii medzi taskmi.
Tieto časti sa mi zdajú najužitočnejšie pre bežné použitie. Zatiaľ som nemal potrebu implementovať niečo ďalšie z utilitiek.
FreeRTOS
Celé ESP-IDF je postavené na „operačnom systéme“ FreeRTOS. Používam úvodzovky, pretože FreeRTOS nezapadá do žiadnej definície operačného systému, akú poznám.
Na rozdiel od iných operačných systémov FreeRTOS nerozdeľuje kód na user space a kernel space. Jednoducho aplikácia má prístup k všetkému. FreeRTOS nerobí prostredníka medzi hardvérom a aplikáciou, neimplementuje súborový systém atď. Dokonca ani neoddeľuje aplikáciu a operačný systém, pretože celý FreeRTOS funguje len ako knižnica, ktorá sa linkuje spolu s aplikáciou.
Už sme si povedali, čo FreeRTOS, ale čo vlastne robí? FreeRTOS dokáže spúšťať úlohy (task = C funkcia s vlastným stackom), prepínať medzi úlohami s prioritou, poskytuje synchronizačné mechanizmy (mutexy, semafory), fronty, správy. Vďaka FreeRTOS môže aplikácia navonok vyzerať, ako keby používala vlákna, aj keď reálne beží v režime kooperatívneho multitaskingu.
Väčšina FreeRTOS je platformovo nezávislá, pričom pre portovanie na inú platformu stačí implementovať pár funkcií. Konkrétne port pre Linux má asi 700 riadkov kódu. Na oficiálnej stránke projektu je odkaz na POSIX port, ktorý je pekných pár rokov mŕtvy. Nedávno som zobral túto starú vykopávku a aktualizoval som ho na novú verziu FreeRTOS. Kód som ako obyčajne zverejnil na githube.
Sieť
Najjednoduchšie zo všetkého vyzerala práca so sieťou. ESP-IDF totiž používa socket api, takže kód sieťovej vrstvy je identický, ako v Linuxe. Tak som si teda veselo includol socket.h a ono to zo začiatku vyzeralo, že to funguje správne. Naplnil sa mi buffer a moje rádio začalo hrať. Keď som pridal HTTP server pre ovládanie celá aplikácia prestala bežať.
Zabudol som na jeden podstatný detail. Vo FreeRTOS mám kooperatívny multitasking. Úloha sa teda prepne iba v prípade, že:
- bolo vyvolané hardvérové prerušenie (v simulátore nenastane)
- úloha bola explicitne zastavená (vTaskSuspend), alebo zrušená (vTaskDelete)
- úloha bola explicitne prepnutá (taskYIELD)
- úloha bola zablokovaná (čakaním na mutex, semafor, zápisom do plnej fronty, čítaním z prázdnej fronty, čakaním vTaskDelay)
Ak zavolám funkciu socketu recv
, tá bude blokovať úlohu kým nedostanem dáta. Volanie recv
zabráni vykonávaniu akejkoľvek ďalšej úlohy kým nedostane dáta. Unixové API je navrhnuté synchrónne s predpokladom, že na danej platforme sa dajú použiť vlákna a že majú minimálnu réžiu. Inými slovami unixové API je naprd. Je naprd na embedded a je naprd aj na normálnom desktope, pretože réžia vlákien je významná.
Ideálne API by pracovalo s callbackmi. Jednoducho urobím požiadavku, zablokujem task semaforom a v callbacku odblokujem semafor.
Na úrovni hardvéru sa prerušenia najviac podobajú callbackom. Na úrovni kernelu sa so sieťovými zariadeniami pracuje pomocou callbackov. Verejné API je však už len synchrónne. Rôzne webové servery to obchádzajú tak, že všetky sieťové spojenia sa obsluhujú v jednom na to vyhradenom vlákne cez volanie select
a následne sa volajú callbacky. Stále však platí, že na konverziu synchrónneho API na callback API je nutné obetovať minimálne jedno vlákno. Naopak konverzia callback API na synchrónne API je triviálna a stačí na ňu mutex.
Nech už boli dôvody pre takýto návrh unixového API akékoľvek výsledok je ten, že sockety s vo FreeRTOS nedajú použiť bez zablokovania celej aplikácie.
Na reálnom ESP32 však recv neblokuje aplikáciu. Pozrel som sa teda na zúbok ich sieťovej vrstve.
Sieťovú vrstvu na ESP32 zabezpečuje knižnica LWIP. Je to knižnica navrhnutá podobne ako FreeRTOS, takže máme tu veľkú platformovo nezávislú časť a dve menšie platformovo závislé časti. Ide konkrétne o časť pre integráciu so systémom (tj. mutexy, semafory, časovače) a integráciu s hardvérom.
Vďaka integrácii so systémom volanie recv
použije mutex z FreeRTOS. V tom momente sa úloha prepne do blokovaného stavu, čím umožní vykonávanie ďalších úloh. Až budú k dispozícii nové dáta úloha sa odblokuje a môže spracovať dáta.
Integrácia s hardvérom je pomerne jednoduchá. Tvorí ju funkcia, ktorá prijme surový packet a pošle ho na spracovanie do nižších vrstiev LWIP a funkcia, ktorá odošle dáta na hardvér.
Aby LWIP fungoval správne v simulátore stačí použiť systémovú integráciu pre FreeRTOS (je priamo v repozitári LWIP) a hardvérovú integráciu s Linuxom (tiež je už v repozitári LWIP). Teda teoreticky ...
Linux neumožní priamo pristupovať k hardvéru. Dokonca by to bol dosť zlý nápad o niečo také sa pokúsiť na desktope.[1] Môžme však v systéme vytvoriť virtuálne sieťové rozhranie, ktorému normálne priradíme IP adresu, nastavíme routing, maškarádu a povolíme prístup zo simulátora. Vytvorenie virtuálneho rozhrania a pridelenie IP adresy je triviálne.
sudo ip tuntap add dev tap0 mode tap user `whoami` sudo ip link set tap0 up sudo ip addr add 10.0.0.1/24 dev tap0 export PRECONFIGURED_TAPIF=tap0
Posledný export je nutný, aby sa hardvérová integračná vrstva nepokúšala nastaviť sieťové rozhranie (predpokladám, že nikto nechce spúšťať simulátor pod rootom).
Aby bolo možné dostať sa z virtuálnej sieťovky na internet je nutné povoliť forwardovanie packetov.
sudo echo 1 > /proc/sys/net/ipv4/ip_forward
Zostáva ešte nastavenie maškarády. Pre ukážku som zvolil nftables, ale to isté je možné dosiahnuť aj pomocou iptables.
#!/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority 0; policy drop; ct state invalid counter drop ct state {established, related} counter accept iif lo accept iif != lo ip daddr 127.0.0.1/8 counter drop iif != lo ip6 daddr ::1/128 counter drop ip protocol icmp counter accept ip6 nexthdr icmpv6 counter accept iifname tap0 accept } chain forward { type filter hook forward priority 0; policy accept; } chain output { type filter hook output priority 0; policy accept; } } table ip nat { chain input { type nat hook input priority 0; policy accept; ip protocol icmp accept } chain prerouting { type nat hook prerouting priority 0; policy accept; } chain postrouting { type nat hook postrouting priority 100; policy accept; ip daddr != 10.0.0.0/24 ip saddr 10.0.0.0/24 masquerade; } chain output { type nat hook output priority 0; policy accept; } }
Nastavenie firewallu sa načíta cez nft -f nftables.rules
. Pozor, pravidlá firewallu je potrebné si upraviť podľa vlastných požiadaviek.
Pár slov k integrácii LWIP s Linuxom
Integrácia s Linuxom rozhodne nebola bezproblémová. Prvý malý problém bol v tom, že FreeRTOS POSIX simulator používa SIGALRM na implementáciu tick timera. To spôsobí okamžité odblokovanie všetkých read/write/recv/send operácií s chybou EINTR. V podstate stačilo odchytiť EINTR a zavolať jednu z tých funkcií znovu.
Všetko vyzeralo byť funkčné, kým som sa nedostal k ovládaniu cez web sockety. Vtedy začala aplikácia padať v sieťovej vrstve. Asi dva týždne som sa s tým trápil, kým som zistil, že v integrácii s Linuxom je nutné pridať uzamknutie TCPIP core pred odoslaním každého packetu do nižších vrstiev. Bez toho tam dochádzalo k race conditionu ak bolo čítanie prerušené zápisom, alebo opačne.
Výsledok
Po pár týždňoch trápenia sa som teda mal ako-tak funkčný simulátor, ktorý umožňuje:
- používať logovacie funkcie
- používať udalosti pomocou ESP-IDF API
- používať FreeRTOS
- používať sieť
Na druhej strane nedokáže:
- emulovať hardvér
Simulácia rozhodne nie je úplne presná. Je však dostatočná na to, aby som mohol programovať svoje internetové rádio bez toho, aby som musel testovať na reálnom hardvéri. Rekompilácia a spustenie zvyčajne trvá menej net 0,5s, čo je výrazne lepšie, než čakať na upload kódu do zariadenia.
Zdrojáky simulátora a krátky popis rozbehania mám zverejnené na stránke projektu.
- Tu som sa splietol, článok, ktorý som čítal tvrdil, že RAW sockety pracujú na sieťovej vrstve, nie linkovej
Pre pridávanie komentárov sa musíte prihlásiť.
POSIX nie je naprd, len ho nespravne nepouzivas. Synchronne je dobre ako je. Ak chces lahko rychlo, pouzijes fork() resp. vlanko a rychlo nakodis apku. Vlastnosti socketu vies oplyvnit cez ioctl. Ak chces v jednom vlakne viac aplikacii, tak pouzijes poll()/epoll().
I v linuxe sa da posielat RAW packety. Vid.: https://gist.github.com/austinmarton/1922600.
https://github.com/mireq/Posix_GCC_Simulator/ Aka je licencia? Aka je licencia povodne prebrateho kodu?
Lenže:
Licencia - kernel MIT, zvyšok autor nešpecifikoval. Je to na oficiálnej stránke FreeRTOS, takže predpokladám, že tiež MIT.
Toto je embedded OS. LOW level. NIekde to staci. Trebars na jednu automaticku vec - napr. otvaranie dveri. Comfortnejsi LOW level - C/ASM nechranenny rezim. Ak Ti to vadi, preco nepouzujes nieco ako Arduino s Linuxom?
Tak si kod so suladom s licencou.
Hm, nejaky simulator (roluj skoro uplne dole): https://github.com/aws/amazon-freertos
Niekde tvrdím, že mi to vadí? Ja som sa nikde nevyjadroval, že by mi kooperatívny multitasking vadil, ale keď už sa mám vyjadriť tak mi vyhovuje viacej než preemptinvy, pretože nie je tak citlivý na synchronizáciu.
V čom konkrétne porušujem licenciu (MIT + GPL)?
Prehrabal som sa v dokumentácii k RAW socketom a áno zmýlil som sa. Pozeral som len na jeden zdroj, ktorý uvádzal chybnú informáciu.
pekne, vdaka
Spravicka tematicky pre Teba: http://www.abclinuxu.cz/zpravicky/mongoose-os-2.17.0-pro-esp32http://www.abclinuxu.cz/zpravicky/mongoose-os-2.17.0-pro-esp32
Mám taký pocit, že som ten projekt zaznamenal už dávnejšie a hovoril som si, že je to fajn pre niekoho, kto vie pracovať len s javascriptom a zubami-nechtami sa bráni tomu, aby sa naučil niečo nové ;)
Samozrejme beriem, že je tam aj C API, ale väčšinu poskytovaných funkcií má už v sebe priamo ESP-IDF.