Arduino a přerušení

V tomto článku se podíváme na to, jak Arduino pracuje s hardwarovým přerušením, jaké jsou jeho omezení a jak je obejít. Také si vysvětlíme co to přerušení vlastně je a na co je dobré ho použít.

V závěru článku odkazuji na anglicky psané zdroje, ve kterých je popsáno jak pracovat s přerušením v ATMega328 a obejít tak funkce Arduino IDE.

Hardwarové přerušení

Na začátek ještě uvedu rychlý úvod do tématu. Pokud víte, co to přerušení je a na co je dobré, můžete přeskočit na další sekci.

Každý procesor pracuje tak, že načítá z paměti jednotlivé instrukce a ty následně zpracovává. Toto načítání probíhá synchronně, to znamená že instrukce se zpracovávají v pořadí v jakém jsou uloženy v paměti. Představme si jednoduchý program, tedy posloupnost instrukcí, který v nekonečném cyklu inkrementuje proměnnou v pracovním registru. (Cože to vlastně dělá? Dokola přičítá jedničku k proměnné — i = i + 1;.) Tím jsme získali hlavní program, který nikdy neskončí. Pokud bychom tento program chtěli zrealizovat pro Arduino, bude vypadat nějak takto:

  int i;

  setup() { i = 0; }

  loop() { i = i + 1; }

Co když ale chceme, aby program přestal sčítat v okamžiku, kdy uživatel zmáčkne tlačítko? Tato událost se projeví změnou hodnoty na některém vstupním pinu. V případě Arduina to může vypadat následovně (za proměnnou buttonPin si dosaďte číslo pinu, na kterém je připojeno tlačítko):

  int i;
  boolean run;

  setup() {
    i = 0;
    run = true;
    pinMode(buttonPin, INPUT);
  }

  loop() {
    if(run)
      i = i + 1;

    if(digitalRead(buttonPin) == HIGH)
      run = false;
  }

Pokud bychom tento příklad přeložili a spustili, nejspíš bude fungovat tak jak má. Co když ale bude program v hlavní smyčce časově náročnější? Například:

  loop() {
    if(run)
      workHard();

    if(digitalRead(buttonPin) == HIGH)
      run = false;
  }

Předpokládejme, že funkce workHard() dělá něco smysluplného a její provedení trvá 3 vteřiny. To ale znamená, že program bude kontrolovat stav tlačítka pouze každou třetí (a nějaké drobné) vteřinu. Předpokládám, že většina uživatelů nebude trpělivě držet zmáčknuté tlačítko a čekat až náš hardware zareaguje. Spíše to vidím tak, že uživatel rychle zmáčkne tlačítko a když se nic nestane bude mačkat dál a dál, až tlačítko umačká k smrti :)

Jak tento problém vyřešit? Pomocí hardwarových přerušení.

Přerušení je způsob jak asynchronně obsloužit důležité události. To znamená, že pokud se vyskytne událost, na kterou musíme okamžitě reagovat, procesor přeruší načítání instrukcí hlavního programu a začne načítat program určený pro zpracování přerušení. Říkáme, že vykoná obsluhu přerušení. Jakmile obsluha přerušení skončí, procesor se opět vrátí k původní činnosti. K tomu, aby se tato vlastnost dala použít musí mít procesor přerušení hardwarově implementované.

Existuje několik různých druhů přerušení, na které je možné reagovat. V příkladu výše by se hodilo použít hardwarové přerušení. Ale občas můžeme chtít reagovat i na jiné události, například chceme nějaký výpočet opakovaně spouštět po určité době. K tomu využijeme přerušení z časovače. Nebo v případě chyby programu chceme, aby procesor vygeneroval přerušení a my na tuto chybu smysluplně reagovali. Případně chceme pomocí přerušení reagovat v okamžiku, kdy bude procesor dělit nulou.

Aby procesor reagoval na přerušení, musí to mít povoleno. Občas se totiž stane, že provádíme nějakou netriviální operaci a nechceme, aby byla přerušena. V takovém případě se přerušení může vypnout a procesor bude všechny vnější události ignorovat. Pokud je ale přerušení povoleno, pak procesor v okamžiku, kdy nastane důležitá událost zastaví načítání instrukcí hlavního programu, uloží hodnoty pracovních registrů do zásobníku a skočí na adresu na které se nachází náš podprogram pro přerušení. Této adrese se říká vektor přerušení.

Teď už víme vše potřebné, pojďme se podívat jak toto využít v Arduinu.

Přerušení v Arduinu

Arduino poskytuje celkem dvě externí přerušení. Jsou očíslovány 0 a 1 a jsou na digitálních pinech 2 a 3 (v případě Arduino Mega je techto pinů 6). Na těchto pinech můžeme sledovat celkem 4 různé druhy událostí:

  • LOW — přerušení nastane vždy, když je pin v logické nule.
  • CHANGE — přerušení nastane při změně logické hodnoty na daném pinu.
  • RISING — přerušení s příchodem vzestupné hrany.
  • FALLING — přerušení s příchodem sestupné hrany.
Sestupná a vzestupná hrana signálu.
Sestupná a vzestupná hrana signálu.

Pokud chceme například reagovat na zmáčknutí tlačítka, budeme sledovat vzestupnou nebo sestupnou hranu signálu.

V dokumentaci Arduina můžete nalézt čtyři funkce, které slouží pro práci s hardwarovým (tedy externím) přerušením. Tyto funkce jsou:

  attachInterrupt()
  detachInterrupt()
  interrupts()
  noInterrupts()

Poslední dvě funkce, tedy interrupts() a noInterrupts() zapínají a vypínají přerušení. Pokud nezavoláme ani jednu z těchto funkcí, je výchozí chování Arduina nereagovat na externí přerušení. To znamená, že pokud tuto funkci chceme použít, musíme ji explicitně povolit zavoláním funkce interrupts().

Funkce attachInterrupt() slouží k registraci námi definované funkce pro obsluhu přerušení. Ukažme si to na příkladu. Předpokládejme že máme funkci button() a chceme aby ji mikrokontrolér zavolal pokaždé, když uživatel zmáčkne tlačítko na digitálním pinu číslo 2. Budeme tedy reagovat na přerušení číslo 0 a na vzestupnou hranu:

void setup() {
  attachInterrupt(0, button, RISING);
}

void loop() {
  // Tady bude hlavní program vykonávat
  // nejakou smysluplnou činnost.
}

void button() {
  // Reakce na stisknutí tlačítka.
}

Při příchodu vzestupné hrany na pinu číslo 2 se okamžitě přeruší vykonávání programu ve funkci loop(), dočasně se zakáže přerušení, provede se funkce button(), opět se přerušení povolí a začne se vykonávat funkce loop() od místa, kde došlo k přerušení.

Vzhledem k tomu, že při obsluze přerušení (tedy v našem příkladě ve funkci button()) jsou všechna přerušení zakázané, je vhodné aby funkce provedla svou činnost co nejrychleji. Během vykonávání obsluhy přerušení totiž mikrokontrolér nebude reagovat na žádnou jinou externí událost.

No a poslední z naší čtveřice funkcí je detachInterrupt(), která, jak už název napovídá, odpojí funkci od daného přerušení. Např:

  detachInterrupt(0);

Přerušení v ATMega328

Pokud jste studovali datasheet mikrokontroléru ATMega328 (což je srdce Arduina Una), pak jste si jistě všimli, že tento čip dokáže reagovat na přerušení na libovolném vstupně-výstupním pinu. Vývojáři Arduina tuto funkčnost sice omezili, ale to se dá obejít.

Pokud potřebujeme více než dvě (resp. 6 v případě Arduino Mega) přerušení, můžeme použít postup, který je popsán například v článku How to Enable Interrupts on ANY pin. Některé zajímavé informace se dají vyčíst i z této diskuse na oficiálních stránkách Arduina: Is better to use ISR(PCINT2_vect) or attachInterrupt on pins 2, 3?. Musím ale upozornit, že informace uvedené v této diskusi berte s rezervou a vždy si je ověřte. Některé příspěvky jsou dost zavádějící.

Články v sérii<< Arduino a sériová komunikaceKde sehnat Arduino? >>