Fyzikální kabinet FyzKAB

Články Moduly ESP32 a ESP32-CAM ESP32: Přerušení - konfigurace a použití

ESP32: Přerušení - konfigurace a použití

Při řešení určitého projektu můžeme občas potřebovat, aby modul ESP32 normálně prováděl svůj běžný kód a zároveň nepřetržitě sledoval nějakou událost – například stisknutí nějakého důležitého tlačítka. Řešením této situace je použití tzv. přerušení. Pojďme se podívat, co to je a jak jej můžeme při programování modulu ESP32 v prostředí Arduino IDE nastavit a využít.

Přerušení modulu ESP32

Modul ESP32, podobně jako i jiné mikrokontroléry, nabízí možnost využívání přerušení. Přerušení (anglicky interrupt) je metoda asynchronní obsluhu událostí, kdy procesor přeruší vykonávání sledu instrukcí, vykoná obsluhu přerušení, a pak pokračuje v předchozí činnosti (viz obrázek č. 1).

princip preruseni

Obrázek č. 1 – Princip přerušení

Výhodou modulu ESP32 je poměrně velké množství možných přerušení, neboť má až 32 přerušovacích slotů (pro každé jádro).

Každé přerušení má určitou úroveň priority a obecně je lze rozdělit do dvou základních typů:

  1. Hardwarová přerušení – k nim dochází v reakci na zadanou vnější událost. Například přerušení od GPIO, když je stisknuto tlačítko připojené na vstupním pinu apod.
  2. Softwarová přerušení – k nim dochází v reakci na zadanou softwarovou instrukci. Například přerušení odvislé od časovače – když vyprší zadaný čas.


Hardwarová přerušení  – GPIO Přerušení

V modulu ESP32 můžeme definovat funkci rutiny přerušení, která bude volána, když se na zadaném GPIO pinu změní hodnota signálu. U vývojových desek s modulem ESP32 lze všechny piny GPIO nakonfigurovat tak, aby fungovaly jako vstupy požadavků na přerušení – i když ne všechny piny jsou na to vhodné.
Zájemcům o problematiku využití GPIO pinů modulu ESP32 doporučujeme přečíst článek: ESP32 – Jak používat GPIO piny?, kde se mohou dozvědět jistá úskalí využití některých GPIO pinů.

Připojení přerušení k GPIO pinu

V prostředí Arduino IDE používáme pro nastavení přerušení funkci attachInterrupt(). Doporučená syntaxe vypadá následujícím způsobem:

attachInterrupt(GPIOPin, ISR, Mode);

Tři parametry této funkce mají následující význam:

GPIOPin
Nastavení zadaného GPIO pinu jako pinu přerušení (říká modulu ESP32, který pin má monitorovat)
ISR
Název funkce, která bude volána při každém spuštění přerušení
Mode
Definice stavu pinu, při kterém se má přerušení spustit. Jako platné hodnoty je předdefinováno pět následujících konstant:

LOW – Spustí se přerušení, kdykoli je na pinu nízká úroveň LOW

HIGH – Spustí se přerušení, kdykoli je na pinu vysoká úroveň HIGH

CHANGE – Spustí se přerušení, kdykoli pin změní hodnotu (z HIGH na LOW nebo LOW na HIGH)

FALLING – Spustí se přerušení, když pin přejde z hodnoty HIGH do LOW

RISING – Spustí se přerušení, když pin přejde z LOW do HIGH


Odpojení přerušení od GPIO pinu

Pochopitelně můžete také přerušení zrušit a odpojit jej od daného pinu. To uděláme pomocí funkce detachInterrupt(). Doporučená syntaxe vypadá následovně:

detachInterrupt(GPIOPin);

kde parametr GPIOPin určuje pin od kterého se má přerušení odebrat (který se má přestat monitorovat).


Servisní rutina přerušení

Jak již bylo naznačeno dříve, při přerušení se spustí speciální část kódu, které říkáme servisní rutina služby přerušení (ISR – Interrupt Service Routine). Syntaxe takové rutiny vypadá následovně:

void IRAM_ATTR ISR_Rutina() {
   // tady bude kod rutiny preruseni
      :
      :
}

Procedury ISR jsou v ESP32 speciální druhy funkcí, které mají některá speciální pravidla, která většina ostatních funkcí nemá, kupříkladu:

  • Obslužná rutina přerušení musí mít dobu provádění co nejkratší, protože blokuje normální provádění programu. Neměly by provádět dlouhé operace, jako je zápis na sériový port apod. (nedej bože čekání!)
  • Rutiny služby přerušení by měly mít atribut IRAM_ATTR


   Co je IRAM_ATTR?
Označením části kódu s atributem IRAM_ATTR deklarujeme, že zkompilovaný kód bude umístěn do vnitřní paměti RAM (IRAM) modulu ESP32. Standardně je normální kód umístěn do paměti Flash. Paměť flash modulu ESP32 je mnohem pomalejší než interní RAM.

Pokud je kód, který chceme spustit, rutina služby přerušení (ISR), obecně ji chceme provést co nejrychleji. Kdybychom museli stále „čekat“, než se ISR rutina načte z flash, nebylo by to zrovna nejšťastnější řešení.
 

Dobrým způsobem implementace kódu pro obsluhu přerušení je tedy to, že ISR pouze signalizuje výskyt přerušení a odkládá skutečné zpracování (které může obsahovat operace, které chvíli trvají) do hlavní smyčky.

Hardwarové připojení

Začíná to být až příliš „akademické“? Tak tedy dost teorie! Podíváme se na praktickou ukázku. 😉

Připojíme k vývojové desce s ESP32 tlačítko – zapojíme jej mezi piny GND a pin GPIO 18. Schéma tohoto zapojení vidíme na obrázku č. 2.

schema pripojeni tlacitka k ESP32 pro test preruseni

Obrázek č. 2 – Připojení tlačítka k ESP32 pro přerušení GPIO


   POZNÁMKA:
Pokud se Vám nechce nic „drátovat“ vzpomeneme si, že většina vývojových desek s modulem ESP32 je osazena dvěma tlačítky – jedno je RESET (někdy též označené EN), tak s tím moc legrace neuděláme, ale druhé je BOOT, které je připojeno k GPIO pinu 0. Takže pokud v následujícím kódu použijeme místo zvoleného pinu 18 pin 0, budeme moci mačkat na vestavěné tlačítko BOOT a dosáhneme stejného efektu (i bez externího tlačítka)
 

Příklad: Jednoduché HW přerušení

Jako ukázkový příklad využití hardwarového přerušení si vytvoříme program, který bude nejen reagovat voláním přerušovací procedury na stisknutí tlačítka, ale po uplynutí jedné minuty se přerušení od GPIO pinu odpojí, takže na další stisknutí tlačítka již reagovat nebude.

Následující program ukazuje výše popsané použití přerušení a zápis obslužné rutiny přerušení. Kód je vysvětlen přímo v komentářích kódu.

/* Preruseni HW - ukazka */

// vytvoreni struktury pro tlacitko
// slo by resit i trojici globalnich promennych
struct Button {
  const uint8_t PIN;   // cislo GPIO pinu tlacitka
  unsigned int numberKeyPresses;   // pocet stisnuti
  bool pressed;   // byla jiz reakce na stitsnute tlacitko?
};

unsigned long lastMillis;   // promenna pro urceni casu behu programu
bool IRQ_assigned;   // promenna pro urceni, zda je preruseni pripojeno na pin?

Button button1 = {18, 0, false};   // nastaveni struktuty tlacitka (pin GPIO 18, pocet 0, nestisknuto)

// --- Obsluzna rutina IRQ --
void IRAM_ATTR ISR_Rutina() {
  button1.numberKeyPresses += 1;   // zvyseni poctu zmacknuti
  button1.pressed = true;   // uz je tlacitko vyrizeno, tak cekame na dalsi stisk
}

// funkce SETUP se spusti jednou pri stisknuti tlacitka reset nebo pri zapnuti desky.
void setup() {
  Serial.begin(115200);   // prenosova rychlost serioveho vystupu
  pinMode(button1.PIN, INPUT_PULLUP);   // rezim GPIO pinu: vstup s PULLUP rezistorem
  attachInterrupt(button1.PIN, ISR_Rutina, FALLING);   //pripojeni ISR rutiny k sestupne hrane na GPIO pinu

  IRQ_assigned = true;   // je preruseni pripojeno na pin?
  lastMillis = millis();   // zacatek behu hlavni smycky

}

// funkce LOOP bezi stale dokola.
void loop() {
  if (button1.pressed) {   // pokud bylo tlacitko stisknute vypis hlasku
    Serial.print("Tlacitko bylo jiz stisknuto ");
    Serial.print(button1.numberKeyPresses);
    Serial.println(" krat.");
    button1.pressed = false;
  }

  // po uplynuti 1 minuty se reakce ma tlacitko zrusi (odpojeni preruseni)
  if ((millis() - lastMillis > 60000)&&(IRQ_assigned)) {   // pokud program bezi dele nez 60k ms a IRQ je jeste pripojeno
    detachInterrupt(button1.PIN);   // odpoj preruseni od GPIO pinu
    Serial.println("Preruseni bylo po 1 minute odpojeno!");
    IRQ_assigned = false;
  }

}


Jakmile nahrajeme program do modulu ESP32, stiskněme tlačítko RESET (resp. EN) a otevřeme sériový monitor s přenosovou rychlostí 115200, měli bychom získat podobný výstup, jako je znázorněn na obrázku č. 3.

serial monitor - hw preruseni

Obrázek č. 3 – Sériový monitor: zobrazení počtu stisknutí, po minutě odpojení přerušení


Softwarové přerušení

Zkusíme si ukázat použití softwarového přerušení vyvolaného časovačem. Dále uvedený program bude pomocí časovače každou sekundu spouštět softwarové přerušení.

Časovač

ESP32 má dvě skupiny časovačů, z nichž každá má dva univerzální hardwarové časovače. Všechny časovače jsou založeny na 64bitových čítačích a 16bitových děličkách. Dělička slouží k dělení frekvence základního signálu (obvykle 80 MHz), který se pak používá k zvyšování/snižování hodnoty čítače časovače. Vzhledem k tomu, že dělička má 16 bitů, může dělit frekvenci hodinového signálu faktorem od 2 do 65536, což dává docela velkou volnost při konfiguraci.

Čítače časovače lze nakonfigurovat tak, aby odpočítávaly vzestupně i sestupně a podporují své opětovné automatické spuštění. Mohou také generovat alarmy, když jejich hodnoty dosáhnou zadané hodnoty. Hodnotu čítače lze též programově načítat (timerRead(timer)), nebo přepisovat (timerWrite(timer, hodnota)).

Připojení přerušení k časovači

Nejdříve si musíme nastavit časovač, pak mu přiřadit přerušení s jeho servisní rutinou. Časovač inicializujeme voláním funkce timerBegin, která vrací ukazatel na strukturu typu hw_timer_t, kterou si musíme na začátku programu definovat jako globální proměnnou (viz celý výpis programu dále). Jako vstup této funkce je zadáno číslo časovače, který chceme použít (od 0 do 3, protože máme 4 hardwarové časovače), hodnota děličky frekvence a příznak indikující, zda má čítač počítat nahoru (true) nebo dolů (false). V našem příkladu použijeme první časovač, faktor děličky 80 a poslednímu parametru předáme true, takže čítač počítá nahoru.

timer = timerBegin(0, 80, true);

Ohledně děličky jsme si dříve řekli, že typická frekvence základního signálu používaného čítači modulu ESP32 je 80 MHz. Tato hodnota znamená, že ke zvýšení čítače časovače dojde 80 000 000krát za sekundu. Tato frekvence může být někdy až příliš vysoká. Využijeme tedy zabudovanou děličku frekvence. Pokud například původní frekvenci vydělíme číslem 80 (použijeme hodnotu 80 jako hodnotu děličky), dostaneme pro čítač signál s frekvencí 1 MHz. Ten bude čítač časovače inkrementovat 1 000 000krát za sekundu, což odpovídá zvýšení hodnoty čítače každou mikrosekundu. Takže pomocí snížení frekvence děličkou s hodnotou 80 budeme později moci specifikovat stanovený čas v mikrosekundách.

Než však povolíme časovač, musíme jej ještě svázat se servisní rutinou přerušení, která se provede, když se vygeneruje přerušení. To se provádí voláním funkce timerAttachInterrupt. Tato funkce přijímá jako první parametr ukazatel na použitý časovač (my jen máme vložený do globální proměnné timer). Jako druhý parametr funkce přijímá ukazatel na ISR funkci, která bude přerušení zpracovávat. Třetí parametr je příznak udávající, zda se jedná o přerušení, které má být vyvoláno náběžnou hranou (true) nebo jakoukoliv změnou úrovně signálu hodin (false).

timerAttachInterrupt(timer, &onTimer, true);

Dalším krokem je použití funkce timerAlarmWrite, pomocí které zadáme hodnotu čítače, při které bude generováno přerušení časovače. Tato funkce jako první vstup opět přijímá ukazatel na časovač, jako druhý parametr hodnotu čítače, při kterém by mělo být generováno přerušení, a třetí parametr je příznak indikující, zda se má po nastalém přerušení časovač opět (automaticky) znovu aktivovat.

Pokud chceme volat ISR rutinu každou vteřinu předáme jako první argument znovu naši globální proměnnou časovače a jako třetí argument předáme true, aby se čítač znovu aktivoval a tak se periodicky generovalo přerušení. Ohledně druhého argumentu, nezapomeňme, že jsme nastavili děličku tak, aby to znamenalo počet mikrosekund, po kterých by mělo dojít k přerušení. V našem případě tedy předpokládáme, že chceme generovat přerušení každou sekundu, což znamená, že překročíme hodnotu 1 000 000 mikrosekund.

timerAlarmWrite(timer, 1000000, true);



   Důležité:
Mějme na paměti, že hodnota druhého parametru je uvedena v mikrosekundách pouze tehdy, pokud zadáme faktor děličky čítače rovný hodnotě 80! Můžeme použít různé hodnoty děličky, ale v tom případě musíme spočítat správnou hodnotu alarmu, kterou počítadlo dosáhne v požadovaném okamžiku.
 

Na závěr nastavení softwarového přerušení pomocí časovače dokončíme povolením časovače zavoláním funkce timerAlarmEnable s odkazem na zadaný časovač.

timerAlarmEnable(timer);

V počáteční proceduře setup() bude celé výše popsané nastavení vypadat jako v následující ukázce kódu:
(schématicky je to znázorněno na obrázku č 4)

void setup() {
  timer = timerBegin(0, 80, true);   // nastaveni casovace (cislo, delicka, vzestupne)
  timerAttachInterrupt(timer, &onTimer, true);   // nastaveni preruseni (casovac, ISR, hrana)
  timerAlarmWrite(timer, 1000000, true);   // kdy spustit preruseni (casovac, 1s, opakovat)
  timerAlarmEnable(timer);   // vse nastaveno, tak jedem!
}


nastavení softwaroveho preruseni pro casovac

Obrázek č. 4 – Funkční schéma přerušení časovače a jeho nastavení v Arduino IDE

 

Podobně jako u hardwarového přerušení si ještě ukážeme, jak to celé zastavit. V našem ukázkovém programu necháme proběhnout rutinu přerušení 100krát a pak volání přerušení zastavíme.

Nejdříve zastavíme alarm pomocí příkazu timerAlarmDisable, pak odpojíme přerušení od časovače příkazem timerDetachInterrupt a nakonec zastavíme samotný časovač pomocí timerEnd. Celé to ukazují následující řádky:

timerAlarmDisable(timer);   // stop alarm
timerDetachInterrupt(timer);   // odpojeni preruseni
timerEnd(timer);   // konec casovace timer


Příklad: Ukázka SW přerušení generovaného časovačem

Hlavní kód programu ukazující využití softwarového přerušení vyvolávaného časovačem, je uveden níže. Význam a popis nejdůležitějších částí ohledně nastavení časovače a přerušení jsme popsali dříve, zbylá část kódu je popsána v komentářích kódu:

/* Preruseni SW-casovac - ukazka */

hw_timer_t * timer = NULL;   // objekt casovace

unsigned int totalInterruptCounter;     // globalni promenna poctu nastalych preruseni
bool NastalCas = false;     // indikator, zda doslo k zavolani preruseni

// --- Obsluzna rutina IRQ --
void IRAM_ATTR onTimer() {
  totalInterruptCounter++;   // zvyseni poctu posti preruseni
  NastalCas = true;   // zprava pro hlavni smycky, ze ma vypsat pocet
}

// funkce SETUP se spusti jednou pri stisknuti tlacitka reset nebo pri zapnuti desky.
void setup() {
  Serial.begin(115200);   // prenosova rychlost serioveho vystupu

  timer = timerBegin(0, 80, true);   // nastaveni casovace (cislo, delicka, vzestupne)
  timerAttachInterrupt(timer, &onTimer, true);   // nastaveni preruseni (casovac, ISR, hrana)
  timerAlarmWrite(timer, 1000000, true);   // kdy spustit preruseni (casovac, 1s, opakovat)
  timerAlarmEnable(timer);   // vse nastaveno, tak jedem!
}

// funkce LOOP bezi stale dokola.
void loop() {
  if (NastalCas) {
    Serial.print("Nastalo preruseni! Celkovy pocet: ");
    Serial.println(totalInterruptCounter);
    NastalCas = false;

    if (totalInterruptCounter > 99) {
      timerAlarmDisable(timer);   // stop alarm
      timerDetachInterrupt(timer);   // odpojeni preruseni
      timerEnd(timer);   // konec casovace timer
      Serial.println("A uz toho bylo dost. KONCIME!");
    }

  }

}


Po kompilaci a odeslání program do modulu ESP32 otevřeme sériový monitor s přenosovou rychlostí 115200. Po restartu modulu ESP32 bychom měli získat výstup, jako je znázorněn na obrázku č. 5.

vystup serial port - softwarove preruseni

Obrázek č. 5 – Zobrazení počtu přerušení a jeho odpojení

Závěr

V dnešním článku jsme si ukázali, co to je přerušení a jak jej můžeme využít pro dvě základní možnosti volání – tedy pro volání pomocí vnějšího (hardwarového) signálu nebo prostřednictvím softwarové události (v našem případě události časovače). I když volání přerušení a tvorba jejich obslužných rutin již asi nepatří mezi začátečnické programátorské dovednosti, je otázkou, zda se potom, co již o modulu ESP32 víme, se ještě za začátečníky máme považovat 😏. Navíc, jak jsme viděli výše v tomto článku, práce s přerušením v prostředí Arduino IDE není na modulu ESP32 bůhvíjak složitá. Jistě by byla škoda této možnosti ve svých projektech nevyužít! Na druhou stranu je třeba si opravdu přiznat, že při práci s přerušením je třeba být při návrhu programu obezřetný, neboť se pohybujeme na tenkém ledě paralelního zpracování kódu a na „strojově nižší“ úrovni než u klasického lineárního programu. A to již trochu zkušenosti opravdu vyžaduje. ALE, jak tyto zkušenosti nasbírat? Pouhou četbou tutoriálů? To asi ne!

Takže neváhejte a pusťte se do dobrodružství jménem PŘERUŠENÍ. A zkoušejte… zkoušejte… Co se může stát? V nejhorším případě program prostě nebude fungovat! Ale to přece nebude poprvé a ani naposled! A až se to konečně vše rozběhne tak, jak jste si přáli, zjistíte, že jste na schodišti „bastlířského“ umění opět postoupili o další stupínek výš. 😎

UPOZORNĚNÍ:
Nesouhlasíme s vyřazením Newtonových zákonů, Ohmova zákona a zákona zachování energie z učiva fyziky základních škol v České republice!