ESP32: Spuštění kódu na konkrétním jádře
Jednou z mnoha zajímavých vlastností modulu ESP32 je to, že má dvě jádra Tensilica LX6, která můžeme využít k provádění našeho kódu. Cílem tohoto příspěvku je ukázka, jak spustit kód na konkrétním jádře modulu ESP32. Ukážeme si to v prostředí Arduino IDE, přesněji řečeno si rozebereme, jak vytvořit úlohu v tzv. FreeRTOS připnutou ke konkrétnímu jádru. FreeRTOS neboli Real-Time Operating System je operační systém pracující v reálném čase na zařízení (tzv. vestavěném systému), které jej podporuje – v našem případě modul ESP32.
Na kterém jádře běžím?
Pro začátek si nejdříve ukážeme, jak zkontrolovat, na jakém jádru běží naše úloha – k tomu použijeme funkci xPortGetCoreID()
. Níže uvedený kód ukazuje použití této funkce pro zjištění čísla jádra, na kterém úloha běží. Všimněme si, že funkce xPortGetCoreID()
nepřijímá žádné argumenty, takže při získání informací o běžícím jádru je ji potřeba použít uvnitř dané úlohy.
V tomto ukázkovém kódu ve funkce setup()
vypíšeme číslo jádra, na kterém tato úloha běží. Pak se spustí nekonečná hlavní smyčka loop()
, ve které budeme opět vypisovat číslo jádra, které bude tuto úlohu zpracovávat.
/* zobrazeni jadra vykonávaného kodu */
// funkce SETUP se spusti jednou pri stisknuti tlacitka reset nebo pri zapnuti desky.
void setup() {
Serial.begin(112500);
delay(1000);
Serial.print("Setup: Je vykonavano na jadre: ");
Serial.println(xPortGetCoreID());
}
// funkce LOOP bezi stale dokola.
void loop() {
Serial.print("Smycka Loop: Je vykonavano na jadre: ");
Serial.println(xPortGetCoreID());
delay(1000);
}
Po přeložení kódu a jeho spuštění uvidíme na sériovém monitoru následující výstup (viz obr. 1):
setup()
a smyčky loop()
Na obrázku č. 1 vidíme, že procedura setup()
i procedura hlavní smyčky loop()
běží na jádře číslo 1. Upřímně řečeno, a co jsme asi čekali? Zatím přece není důvod, proč by se výkon programu měl jakýmkoliv způsobem distribuovat mezi jádra procesoru modulu ESP32.
Vzhůru na druhé jádro!
Zkusíme náš předchozí kód mírně upravit. Kromě předešlého zjišťování, kde co běží, zkusíme něco dalšího. Kromě standardních úloh daných procedurami setup() a loop() spustíme další (novou) úlohu. To uděláme pomocí funkce xTaskCreate
, a pak zkusíme analyzovat, v jakém jádře začne běžet. Vysvětlení, jak používat tuto funkci k vytvoření
úloh v rámci FreeRTOS, je v následující tabulce:
Funkce xTaskCreate
Funkci xTaskCreate
je třeba zavolat s šesticí následujících argumentů:
TaskCode | V tomto argumentu musíme předat ukazatel na funkci, která bude zadanou úlohu implementovat. V našem následujícím příkladu si vytvoříme např. funkci nazvanougenericTask . |
TaskName | Název úlohy (zadáno v podobě řetězce). My jsme si v následujícím kódu zvolili zcela neoriginální název „genericTask“ |
StackDepth | Velikost zásobníku úlohy, zadává se počet bajtů. Neexistuje jednoduchý způsob, jak určit velikost úlohy, i když lze provést určité výpočty. V našem následujícím jednoduchém příkladu předáme hodnotu, která je dostatečně velká. 😉 |
Parametr | Ukazatel na parametr, který může funkce úlohy přijímat. Musí být typu |
Priority | Priorita úkolu (o tom bude ještě „nějaké povídání“ dále) V našem příkladu jsme použili hodnotu 2 |
TaskHandle | Vrací popisovač, který lze použít pro pozdější odkaz na úlohu při volání funkcí (například pro odstranění úlohy nebo změnu její priority). Také v ukázkovém jednoduchém příkladu jej nepoužijeme, takže bude NULL. |
Funkce xTaskCreate
vrací hodnotu pdPASS při úspěchu nebo chybový kód, v případě problému. Prozatím budeme předpokládat, že úloha bude vytvořena bez problémů, takže žádnou kontrolu chyb neřešíme. Přirozeně, pro aplikaci skutečného případu bychom to museli udělat, abychom potvrdili, že úloha byla vytvořena.
/* zobrazeni jadra vykonávaného kodu + nova uloha */
// funkce SETUP se spusti jednou pri stisknuti tlacitka reset nebo pri zapnuti desky.
void setup() {
Serial.begin(112500);
delay(1000);
Serial.print("Setup: Je vykonavano na jadre: ");
Serial.println(xPortGetCoreID());
xTaskCreate(
genericTask, /* Funkce pro realizaci ulohy */
"genericTask", /* Retezec s nazvem ulohy */
10000, /* Velikost zasobniku v bajtech */
NULL, /* Vstupni parametr ulohy */
2, /* Priorita ulohy */
NULL); /* Popisovac ulohy */
delay(2000);
}
// funkce LOOP bezi stale dokola.
void loop() {
Serial.print("Smycka Loop: Je vykonavano na jadre: ");
Serial.println(xPortGetCoreID());
delay(1000);
}
// funkce nasi vytvorene ulohy.
void genericTask( void * parameter ) {
Serial.print("Vytvorena uloha: Je vykonavano na jadre: ");
Serial.println(xPortGetCoreID());
vTaskDelete(NULL);
}
Vytvoření nové úlohy (nazvané genericTask
) pomocí funkce xTaskCreate
vidíme v proceduře setup. Výkonné tělo procedury úlohy pak vidíme v proceduře genericTask
na konci výpisu programu. Tato procedura vypisuje číslo jádra, na kterém byla tato úloha puštěna.
Po vytvoření nové úlohy (v proceduře setup()
) provedeme malé zpoždění (2 s). To způsobí, že aktuální úloha bude zablokována na dobu čekání. To však ovlivní plánovač úloh, který právě začal řešit přiřazení CPU nově vzniklé úloze. Nově vytvořená úloha, která se má rozběhnout bez ohledu na to, ke kterému jádru je přiřazena, bude tedy přiřazena jinému volnému jádru. To je však to druhé – první přece blokuje čekací smyčka! Tento trik je však použitelný pouze v případě, kdy nově vytvořená úloha by měla být přiřazena ke stejnému jádru, jako se provádí funkce setup()
.
Po funkci setup()
následuje v programu hlavní smyčka loop()
, která obsahuje pouze volání funkce xPortGetCoreID
, takže můžeme vědět, v jakém jádře běží.
Konec povídání, pojďme kód otestovat! Stačí jej přeložit a nahrát pomocí prostředí Arduino IDE a otevřít sériový monitor. Měli bychom získat výstup podobný obrázku 2:
Všimněme si, že jak funkce setup()
, tak funkce hlavní smyčky loop()
se provádějí na jádře 1. To je v souladu s implementací
podpory ESP32 pro prostředí Arduino IDE. Naopak nově vytvořený úkol byl přiřazen k jádru s číslem 0. Vítejte na druhém jádře!!!
Nastavení konkrétního jádra k úloze
Předešlý příklad nám sice ukázal, že lze docílit spuštění programové úlohy na dalším jádře procesoru modulu ESP32, ale asi bychom chtěli nad tímto procesem trochu více převzít kontrolu. Minimálně bychom rádi chtěli volit připnutí výkonné úlohy ke konkrétnímu jádru modulu ESP32. Tuto možnost nám splní funkce xTask
, která jako jeden ze svých argumentů obdrží ID jádra, kde se má zadaná úloha provést. Opět si ukážeme jednoduchý příklad, jak tuto funkci použít ke spuštění jednoduchých úlohy na dvou různých jádrech procesoru modulu ESP32.
Předtím než se podíváme na následující ukázkový programový kód, řekneme si něco k samotné funkci xTask
. Tato funkce přijímá přesně stejné argumenty jako předchozí funkce xTask
, jen má navíc ještě jeden (poslední) argument, který je pro určení jádra, na kterém by se měla zadaná úloha spustit.
xTaskCreatePinnedToCore( | |
coreTask, | Funkce pro realizaci úlohy |
"coreTask", | Řetězec s názvem úlohy |
10000, | Velikost zásobníku v bajtech |
NULL, | Vstupní parametr úlohy |
0, | Priorita úlohy |
NULL, | Popisovač úlohy |
taskCore |
Jádro, kde má úloha běžet
Jádra jsou očíslována čísly 0 a 1. Při kompilaci programu v prostředí Arduino IDE je hlavní část programu (tj. procedury setup() a loop apod.) primárné přiřazena jádru č. 1.
|
); |
Pokud jsme se dostatečně „seznámili“ s funkcí xTask
můžeme se pustit do následujícího ukázkového kódu.
/* pripnuti ulohy na zadane jadro */
static int taskCore = 1;
// funkce SETUP se spusti jednou pri stisknuti tlacitka reset nebo pri zapnuti desky.
void setup() {
Serial.begin(112500);
delay(1000);
Serial.print("Setup: Je vykonavano na jadre: ");
Serial.println(xPortGetCoreID());
xTaskCreatePinnedToCore(
coreTask, /* Funkce pro realizaci ulohy */
"coreTask", /* Retezec s nazvem ulohy */
10000, /* Velikost zasobniku v bajtech */
NULL, /* Vstupni parametr ulohy */
0, /* Priorita ulohy */
NULL, /* Popisovač úlohy */
taskCore); /* Jadro, kde ma uloha bezet */
Serial.println("Uloha vytvorena...");
}
// funkce LOOP bezi stale dokola.
void loop() {
Serial.println("Hlavni smycka loop() spustena...");
while(true) {}
}
void coreTask( void * pvParameters ) {
String taskMessage = "Uloha bezi na jadre ";
taskMessage = taskMessage + xPortGetCoreID();
while(true) {
Serial.println(taskMessage);
delay(1000);
}
}
Před samotným zkompilováním, nahráním do modulu ESP32 a spuštěním se ale podíváme na tento kód trochu podrobněji!
Na začátku kódu vidíme deklarovanou globální proměnnou taskCore
, která obsahuje číslo jádra, na které bude připnutá úloha coreTask
. Nyní máme v této proměnné nastavené jádro číslo 1.
V proceduře setup()
je použita funkci xTask
, kterou jsme si popsali dříve. Všimněme si, že jsme v této funkci implementovali funkcí nazvanou coreTask a přiřadili jsme ji prioritu 0, což je nižší než u funkcí setup()
i hlavní smyčky loop()
.
V hlavní smyčce začneme vytištěním zprávy na sériový port oznamující, že spouštíme hlavní smyčku, takže nyní víme, ve kterém bodě provádění kódu se nacházíme. Poté uděláme nekonečnou smyčku while
bez jakéhokoli kódu uvnitř. Je důležité, abychom do něj nevkládali žádný typ zpožďovací funkce, takže nejlepší způsob je ponechat jej prázdný. Tímto způsobem nyní plánovač udrží provádění na CPU, která je mu přiřazena.
Funkce úlohy coreTask
je také velmi jednoduchá. Pouze tiskneme zprávu označující jádro, které je k němu přiřazeno pomocí funkce xPort
. To samozřejmě musí odpovídat jádru uvedenému v globální proměnné! Pak je vytvořena nekonečná smyčku, kde tuto zprávu tiskneme s malým zpožděním v každé
iteraci. V tomto případě není problém zadat zpoždění této úlohy, protože úloha má nižší prioritu. Takže to neovlivní naši hlavní aplikaci.
Pokud je nám celý kód jasný, můžeme jej vložit do prostředí Arduino IDE, zkompilovat a spustit. Měli bychom získat výsledek podobný tomu obrázku 3.
Jak je na obrázku č. 3 vidět, spustila se úloha setup()
, běží hlavní smyčka loop()
, ale z naší úlohy coreTask
spuštěné v rámci funkce setup()
není žádný výstup! Jak je to možné? Důvod je následující: Protože jsme ji připnuli k jádru 1 (stejně jako úlohu setup()
a loop()
) a přiřadili jsme ji prioritu 0. Hlavní smyčka bude spuštěna vždy, protože má vyšší prioritu (rovnou 1) a hlavně také běží na jádru 1.
Nyní změňme globální proměnnou taskCore
na 0 a znovu kód nahrajme do modulu ESP32. Nyní bychom měli získat jiný výsledek, jak je znázorněno na obrázku 4.
V tomto případě již získáváme výstupy z úlohy coreTask
, která běží na jádře 0. V tomto případě, přestože se hlavní smyčka loop()
vykonává na jádře 1 a zamyká jeho prostředky, protože má vyšší prioritu, může se nová úloha spustit na jádru 0, protože je volné. Nedochází tedy k zablokovaní naší úlohy coreTask
a vše funguje dobře.
Závěr
Jak jsme mohli vidět v tomto článku, provádění kódu v obou jádrech modulu ESP32 je možné a funguje. Možnost spouštění kódu v obou jádrech otevírá širokou škálu možností, např. určitou optimalizaci spouštění kódu, možnost určitého paralelního řešení některých problémů… Namátkově bychom třeba mohli zmínit generování výstupního signálu, který bude zcela nezávislý na běhu zbylého programu. Nebo kupříkladu určitou „nezávislou“ reakci na stisknuté tlačítko… no, na to by se asi lépe využilo tzv. přerušení, který modul ESP32 také disponuje. Ale o tom třeba zase jindy. 😉
Snad Vám tento článek alespoň z části nastínil další možnosti pro tvorbu aplikací využívajících plný potenciál dvoujádrového provedení modulu ESP32.