Fyzikální kabinet FyzKAB
Články Moduly ESP32 a ESP32-CAM ESP32 – vlastnosti a metody ESP32: Přerušení – konfigurace a použití

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

UPOZORNĚNÍ
Následující článek byl již upraven pro jádro Arduino ESP32 verze 3.x.
ESP-IDF

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 mohou generovat „alarmy“, když jejich hodnoty dosáhnou zadané hodnoty. To lze využít pro spouštění různých událostí. 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 (od Arduino ESP32 verze 3.x) je zadána frekvence „tikání“ časovače (zadává se v Hz). Číslo použitého časovače a zadané frekvenci odpovídající hodnota nastavení děličky frekvence je nyní nastavována automaticky. V našem příkladu použijeme frekvenci časovače 1 000 000 Hz.

timer = timerBegin(1000000);

Takto definovaný čítač bude inkrementovat 1 000 000krát za sekundu, což odpovídá zvýšení hodnoty čítače každou mikrosekundu.

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.

timerAttachInterrupt(timer, &onTimer);

Dalším krokem je použití funkce timerAlarm, 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 false/true indikující, zda se má po nastalém přerušení časovač opět (automaticky) znovu aktivovat. Čtvrtým parametrem funkce je celkový počet těchto automatických spuštění – hodnota 0 odpovídá nekonečnému počtu (čtvrtý parametr se neprojeví, pokud je restartování časovače zakázáno třetím parametrem).

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 frekvenci tak, aby hodnota čítače odpovídala počtu 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 zde testujeme dosažení hodnoty 1 000 000 mikrosekund. Čtvrtý parametr nastavíme na hodnotu 0, což odpovídá nekonečnému opakování.

timerAlarm(timer, 1000000, true, 0);

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

V počáteční proceduře setup() bude celé výše popsané nastavení vypadat jako v následující ukázce kódu:

void setup() {
  timer = timerBegin(1000000);   // nastaveni casovace (frekvence v Hz)
  timerAttachInterrupt(timer, &onTimer);   // nastaveni preruseni (casovac, ISR)
  timerAlarm(timer, 1000000, true, 0);   // kdy spustit preruseni (casovac, 1s, opakovat ANO, nekonecnekrat)
}

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 timerStop, 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:

timerStop(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(1000000);   // nastaveni casovace (frekvence v Hz)
  timerAttachInterrupt(timer, &onTimer);   // nastaveni preruseni (casovac, ISR)
  timerAlarm(timer, 1000000, true, 0);   // kdy spustit preruseni (casovac, 1s, opakovat ANO, nekonecnekrat)
}

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

    if (totalInterruptCounter > 99) {
      timerStop(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 č. 4.

vystup serial port - softwarove preruseni
Obrázek č. 4 – Zobrazení počtu přerušení a jeho odpojení

Nyní si dáme takový malý test pro zvídavé a hlavně pozorné čtenáře! Možná Vás napadly následující otázky:

  1. Proč testujeme počet průběhů rutiny přerušení, když nám funkce timerAlarm umožňuje nastavit počet spuštění časovače?
  2. Proč jsme nenastavili frekvenci 1 Hz a netestujeme ve funkci timerAlarm hodnotu časovače 1?

Na obě otázky si zkuste nejdříve sami odpovědět, pak se podívejte dále…

Odpovědi:

  1. V případě počtu volání přerušení lze skutečně nastavit hodnotu zavolání ve funkci timerAlarm. Celá podmínka testující proměnnou totalInterruptCounter by v kódu byla jen kvůli finálnímu vypsání koncové hlášky, popřípadě pro odpojení rutiny přerušení. Kdo tedy tipoval, že výše uvedený kód by bylo možné přepsat pomocí 4. parametru funkce timerAlarm, měl pravdu!
  2. V případě změny pracovní frekvence časovače na 1 Hz však již nepochodíme. Prozorný čtenář si jistě v úvodu všiml zmínky, že časovače jsou osazeny 16bitovými děličkami, tedy že maximální hodnota dělení základní frekvence (80 MHz) je 216 (tedy 65536). Minimální dosažitelná frekvence je 1221 Hz. Dosadíme-li do funkce timerBegin hodnotu menší frekvence, bude docházet k chybě a modul ESP32 se bude jen cyklicky restartovat.

Následující kód je úpravou kódu předešlého, kde je využito nastavení opakování alarmu na hodnotu 100 a na nejnižší frekvenci 1221 Hz. (Je otázkou, zda je frekvence 1221 Hz při časování 1 s výhodná?)

/* Preruseni SW-casovac - ukazka 2 */

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(1221);   // nastaveni casovace (frekvence v Hz)
  timerAttachInterrupt(timer, &onTimer);   // nastaveni preruseni (casovac, ISR)
  timerAlarm(timer, 1221, true, 100);   // kdy spustit preruseni (casovac, 1s, opakovat ANO, 100krat)
}

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

    if (totalInterruptCounter > 99) {   // NYNI jen kvuli odpojeni IRQ a vypisu hlasky KONCIME
      timerDetachInterrupt(timer);   // odpojeni preruseni
      Serial.println("A uz toho bylo dost. KONCIME!");
    }

  }

}

A skončíme tedy ještě jednou odpovědí na otázkou – Proč je v předešlých kódech lepší pracovat na frekvenci 1 MHz a testovat hodnotu časovače 1 000 000, než pracovat na frekvenci 1221 Hz a testovat hodnoltu časovače 1221?

Skoro by se chtělo říci: „Ďábel se neskrývá v detailu, ale v děličce!“ 😈

Použijeme-li základní frekvenci časovače 1 MHz, je třeba pracovní frekvenci 80 MHz modulu ESP32 vydělit číslem 80 (což je celé číslo). V případě pracovní frekvence 1 MHz je doba změnu hodnoty časovače přesně 1 μs, tedy při testování hodnoty 1 000 000 nedochází k žádné chybě časování. Zatímco při použití frekvence 1221 Hz je odpovidající dělicí poměr 65520,06552…. Konfigurace děličky však umožňuje zadat pouze celočíselný dělicí poměr, hodnota je tedy zaokrouhlena na 65520. Tato hodnota ale pak odpovídá skutečné frekvenci časovače 1221,001221… Hz. V případě testování hodnoty 1221 je takto vygenerovaná vteřina cca o 1 μs kratší, to je možná zanedbatelná (ale také zbytečná!) chyba.

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ýš. 😎

Autor článku: Miroslav Panoš
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!