ESP32 - simulátor pre Linux

01.02 | 14:31 | Mirecove dristy | Miroslav Bendík

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:

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:

Na druhej strane nedokáže:

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.

STM32 mandelbrot

  1. Tu som sa splietol, článok, ktorý som čítal tvrdil, že RAW sockety pracujú na sieťovej vrstve, nie linkovej

Prílohy

    • RE: ESP32 - simulátor pre Linux 01.02 | 17:21
      Avatar debian+   Návštevník

      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?

      • RE: ESP32 - simulátor pre Linux 01.02 | 18:22
        Avatar Miroslav Bendík Gentoo  Administrátor

        Lenže:

        • nemám hardvér podporujúci vlákna, nemám fork
        • synchrónne nie je dobré na embedded

        Licencia - kernel MIT, zvyšok autor nešpecifikoval. Je to na oficiálnej stránke FreeRTOS, takže predpokladám, že tiež MIT.

        • RE: ESP32 - simulátor pre Linux 04.02 | 21:39
          Avatar debian+   Návštevník

          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

          • RE: ESP32 - simulátor pre Linux 05.02 | 07:40
            Avatar Miroslav Bendík Gentoo  Administrátor

            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)?

      • RE: ESP32 - simulátor pre Linux 05.02 | 19:15
        Avatar Miroslav Bendík Gentoo  Administrátor

        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.

    • RE: ESP32 - simulátor pre Linux 03.02 | 11:32
      Avatar redhawk75 mx19, galliumos, antiX, Devuan,  Používateľ

      pekne, vdaka

    • RE: ESP32 - simulátor pre Linux 09.02 | 01:21
      Avatar debian+   Návštevník

      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

      • RE: ESP32 - simulátor pre Linux 09.02 | 17:45
        Avatar Miroslav Bendík Gentoo  Administrátor

        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.