Interrupts

interrupts, polling, ISR, low power, sleep mode

Introduction: Indiana Jones and the Golden Idol

  • (slide) Verím, že Indiana Jonesa predstavovať netreba. Vo svojom prvom filmovom príbehu sa na začiatku filmu nachádza časť, v ktorej sa snaží získať artefakt s názvom Golden Idol. Ten sa nachádza na bezpečnostnom mechanizme, ktorý sa snaží Indy “hacknúť.”

    Indiana Jones: The Golden Idol Scene (zdroj)
  • (slide) Pôvodná elektrická schéma tohto pekelného zabezpečovacieho systému sa síce nedochovala, ale schéma jej repliky realizovaná s doskou Arduino UNO vyzerá nasledovne:

    Indiana Jones: Schematic of Golden Idol’s Security System
  • A kód pre mikrokontrolér vyzerá zasa takto:

    void lockdown(){
      Serial.println("Running all the booby-traps");
      Serial.println("Close the cave entrance");
      exit(0); 
    }
    
    int main(){
      init();
    
      // setup
      Serial.begin(9600);
      pinMode(2, INPUT);
    
      // loop
      for(;;){
        if(digitalRead(2) == LOW){
          Serial.println("Silence...");
          delay(1000);
        }else{
          Serial.println("Intruder detected");
          Serial.println("Lockdown initiated.");
          lockdown();
        }
      }
    }
  • Ak sa na ten kód pozrieme pozorne, tak nám musí byť jasné, prečo Indiana Jones tak váhal a dal si záležať na rýchlosti výmeny artefaktu Golden Idol: čas, ktorý mal na výmenu, trval len 1s!

  • Čo to znamená, že mal čas len 1s? Prečo nie viac a prečo nie menej? Ako pomôcť Hovitským programátorom lepšie zabezpečiť svoj bezpečnostný mechanizmus postavený na doske (nie len) Arduino UNO?

Blocking Function Call

  • (slide) Funkciu delay() radíme medzi tzv. blokujúce funkcie. Obecne môžeme povedať, že blokujúce volanie je taký typ volania, ktoré pozastaví vykonávanie ďalšieho kódu programu, pokiaľ sa toto volanie neukončí. Napr. môže ísť o čakanie ne odpoveď zo vzdialeného servera alebo o výsledok dlhotrvajúceho výpočtu.

  • To isté platí aj pre funkciu delay(). Táto funkcia pozastaví vykonávanie programu na uvedený čas v milisekundách. Podľa dokumentácie však má niekoľko výnimiek: zaznamenanie prichádzajúcej sériovej komunikácie, zachovanie úrovní pinov (ako digitálnych, tak aj PWM) a fungujú prerušenia.

  • Takže ak sa pozrieme do skeču, je nám jasné, že Indiana Jones má na výmenu práve 1s, na ktorú sa vykonávanie celého programu pozastaví.

  • Takže - koľko času stačí? Bude systém bezpečnejší, keď čas bude kratší? Ušetrím viac energie, keď bude čas dlhší? Koľko je teda dosť? A neviem zareagovať okamžite po stlačení?

The Polling

  • Ešte predtým, ako sa pohneme ďalej, pomenujeme jeden prístup, ktorý bol v uvedenom fragmente kódu objavený. Okrem toho sa s ním môžete bežne stretnúť v mnohých ďalších programoch. Priblížme si ho drobnou ilustráciou.

  • (slide) Pamätáte sa na druhú časť animáku Shrek? Shrek šiel spolu s Fionou a oslíkom na návštevu k Fioniným rodičom do kráľovstva za siedmymi horami a siedmymi dolinami. A pamätáte sa na to, čo robil celý čas oslík? Celý čas sa pýtal rovnakú otázku: “Kedy tam už budeme?”

    Are we there yet? (zdroj)
  • A presne rovnaký prístup zvolili aj autori predmetného zabezpečovacieho systému. A rovnaký spôsob sme zvolili aj v prípade čítania hodnoty z LDR a PIR senzorov v prípade nášho nočného svetla. A to v pravidelných 1s intervaloch.

  • (slide) Pri získavaní hodnôt z týchto senzorov sa správame presne tak, ako oslík v Shrekovi - neustále sa pýtame na tú istú vec. Znova a znova. Vo svete informatiky sa tento prístup nazýva slovom polling, pretože sa vo veľkom pýtame (z angl. poll). Jeho princíp spočíva v neustálom dopytovaní sa externého zariadenia, či na ňom nedošlo k požadovanej udalosti, resp. k zmene stavu. Polling predstavuje synchrónnu operáciu.

  • Polling je veľmi jednoduchý a častokrát postačuje pre riešenie väčšiny problémov. Má však aj niektoré nevýhody. Jednou z nich je vysoká pravdepodobnosť straty údajov. K tomu môže dôjsť napr. vtedy, ak externé zariadenie pošle údaje mikrokontroléru tesne potom, ako sa ich ten pokúsil prečítať. Alebo aj vtedy, ak dôjde k pohybu uprostred 500 ms prestávky a počas tých istých 500 ms sa pohyb aj skončí.

  • Samozrejme existujú požiadavky, pri ktorých je takéto správanie postačujúce. Napr. merať vonkajšiu teplotu stačí v niekoľkominútových pravidelných intervaloch a prípadný výkyv teplôt medzi dvoma meraniami je možné považovať za zanedbateľný.

  • Vďaka neustálemu dopytovaniu sa si polling vyžaduje vyhradenie väčšieho množstva zdrojov a hlavne času mikrokontroléra. Ten teda miesto vykonávania potrebných operácií plytvá svojimi zdrojmi na neustále kontrolovanie stavu externého zariadenia. To sa priamoúmerne dokáže prejaviť na spotrebe energie, pretože čím častejšie kontrolujeme stav externého zariadenia, tým viac energie spotrebujeme.

The Interrupts

  • Ako teda zabezpečiť, aby mikrokontrolér ošetril vzniknutú udalosť v momente, keď k nej dôjde bez akéhokoľvek spozdenia? A je to vôbec možné?

  • (slide) Zareagovať okamžite v prípade zmeny úrovne signálu na pine mikrokontroléra, môžeme pomocou prerušenia (z angl. interrupt). Koncept prerušení je vo svete mikrokontrolérov veľmi dôležitý.

  • Prerušenie je mechanizmus, ktorým dá prostredie mikrokontroléru vedieť, že sa práve stalo niečo dôležité. Vo všeobecnosti je možné povedať, že prerušenie je špeciálny signál pre mikrokontrolér, ktorý poslal softvér alebo hardvér a vyžaduje si okamžitú pozornosť.

  • Výhodou takéhoto prístupu je vo všeobecnosti zníženie zaťaženia mikrokontroléra, ktorý nemusí stále dookola testovať, či k udalosti došlo alebo ešte nie (viď polling). V porovnaní s polling-om má prerušenie lepší reakčný čas, pretože reaguje na udalosť okamžite.

  • Princíp prerušenia sa využíva aj v architektúre riadenej udalosťami (a zngl. event driven architecture) alebo v programovaní riadenom udalosťami (z angl. event driven programming). V objektovom programovaní je zasa tento prístup opísaný pomocou návrhového vzoru pozorovateľ (z angl. observer).

  • Podobne, ako keď donášková služba volá priamo zákazníkovi, aby ho informovala o tom, že je donáška pripravená, aj požiadavku o prerušenie dostane od zariadenia priamo mikrokontrolér Tým pádom môže prísť požiadavka o prerušenie kedykoľvek a prerušenie predstavuje asynchrónny spôsob komunikácie.

  • Hlavný rozdiel medzi polling-om a prerušením je v tom, či sa softvér sám opýta alebo hardvér sám oznámi, či došlo k predmetnej udalosti.

Interrupt Service Routine

  • (slide) Inštrukcie programu, ktorý beží v mikrokontroléri, sa (obyčajne) vykonávajú sekvenčne. To znamená, že po vykonaní jednej inštrukcie sa vykoná nasledujúca inštrukcia v poradí. Akonáhle však mikrokontrolér dostane požiadavku na prerušenie, pozastaví sa vykonávanie inštrukcií programu mikrokontroléra a ten spustí špeciálnu funkciu na spracovanie prerušenia, ktorá sa označuje ISR (z angli. Interrupt Service Routine). Po jej skončení sa obnoví vykonávanie prerušeného programu vykonaním ďalšej inštrukcie v poradí.

    Calling of ISR (zdroj)
  • ISR funkcie nevracajú žiadnu hodnotu a nemajú žiadny parameter. Ak je teda potrebné vo vnútri ISR funkcie zmeniť stav správania aplikácie, je na to možné použiť globálne premenné. Je to vlastne jediný spôsob, pomocou ktorého je možné prenášať údaje medzi ISR a hlavným programom.

  • (slide) Pri tvorbe ISR je dobré dodržiavať niekoľko odporúčaní:

    • ISR majú byť čo najkratšie a čo najrýchlejšie, aby zbytočne nebrzdili hlavný program alebo prípadne ďalšie prerušenia, ktoré môžu nastať.
    • Je potrebné sa vo vnútri ISR vyhnúť použitiu funkcie delay()!
    • Nepoužívať sériovú komunikáciu!
    • Ak programujete v jazyku C, tak globálne premenné, ktoré používate na zdieľanie údajov medzi ISR a programom, označte pomocou kvalifikátora volatile! Tým poviete prekladaču, že táto premenná môže byť použiteľná kdekoľvek v kóde a prekladač jej obsah vždy pri použití znovu načíta a nebude sa spoliehať na jej kópiu v registri. Zabránite tak aj prípadným optimalizáciám prekladača, vďaka ktorým by ju mohol napr. vyhodiť, pretože sa nepoužíva (v hlavnom programe).
    • Nevypínať ani nezapínať podporu prerušení vo vnútri ISR. V tomto prípade však existujú výnimky, ktoré budú opísané neskôr v kapitole.

Reasons to use Interrupts

  • (slide) Existuje veľa dôvodov, prečo prerušenia používať . Niektoré z nich sú tieto:

    • To detect pin changes (eg. rotary encoders, button presses)
    • Watchdog timer (eg. if nothing happens after 8 seconds, interrupt me)
    • Timer interrupts - used for comparing/overflowing timers
    • SPI data transfers
    • I2C data transfers
    • USART data transfers
    • ADC conversions (analog to digital)
    • EEPROM ready for use
    • Flash memory ready

Types of Interrupts

  • (slide) Pri práci s mikrokontrolérmi je možné prerušenia rozdeliť do dvoch skupín:

    1. hardvérové prerušenia, známe tiež ako externé prerušenia, alebo tiež pin-change prerušenia, a
    2. softvérové prerušenia, ktoré sú známe ako interné prerušenia alebo tiež časovače.
  • Ako už názov napovedá, signál prerušenia prichádza v prípade hardvérových alebo externých prerušení z externého zariadenia. Toto zariadenie je s mikrokontrolérom priamo prepojené. Keďže sú prerušenia asynchrónne, k prerušeniu môže dôjsť kedykoľvek.

  • Interné prerušenia zasa referujú na čokoľvek vo vnútri mikrokontroléra, čo dokáže vyvolať prerušenie. Príkladom môžu byť napríklad časovače, pomocou ktorých je možné zabezpečiť, aby k vyvolaniu prerušenia dochádzalo pravidelne napr. každú 1 sekundu.

Interrupt Vectors in ATmega328P

  • V jednej chvíli môžu byť naraz vyvolané viaceré žiadosti o prerušenie a mikrokontrolér sa musí rozhodnúť, ktorá z nich bude ošetrená ako prvá. Je teda potrebné, aby mikrokontrolér vedel povedať, ktoré prerušenia majú prednosť pred inými.

  • Mikrokontrolér obsahuje tzv. tabuľku vektorov prerušení (viď. tabuľka XXX). Táto tabuľka sa nachádza na začiatku programovej flash pamäti a obsahuje adresy ISR funkcií jednotlivých prerušení. V ich poradí je však aj priorita - čím má prerušenie nižšiu adresu, resp. vektor prerušenia má nižšie číslo, tým má vyššiu prioritu. Z tabuľky je teda možné vidieť, že najvyššiu prioritu má prerušenie od zdroja RESET a najnižšiu prioritu od zdroja SPM READY.

Vector No. Program Address Source Interrupt Definition
1 0x0000 RESET External Pin, Power-on Reset, Brown-out Reset and Watchdog System Reset
2 0x0002 INT0 External Interrupt Request 0
3 0x0004 INT1 External Interrupt Request 1
4 0x0006 PCINT0 Pin Change Interrupt Request 0
5 0x0008 PCINT1 Pin Change Interrupt Request 1
6 0x000A PCINT2 Pin Change Interrupt Request 2
7 0x000C WDT Watchdog Time-out Interrupt
8 0x000E TIMER2 COMPA Timer/Counter2 Compare Match A
9 0x0010 TIMER2 COMPB Timer/Counter2 Compare Match B
10 0x0012 TIMER2 OVF Timer/Counter2 Overflow
11 0x0014 TIMER1 CAPT Timer/Counter1 Capture Event
12 0x0016 TIMER1 COMPA Timer/Counter1 Compare Match A
13 0x0018 TIMER1 COMPB Timer/Coutner1 Compare Match B
14 0x001A TIMER1 OVF Timer/Counter1 Overflow
15 0x001C TIMER0 COMPA Timer/Counter0 Compare Match A
16 0x001E TIMER0 COMPB Timer/Counter0 Compare Match B
17 0x0020 TIMER0 OVF Timer/Counter0 Overflow
18 0x0022 SPI, STC SPI Serial Transfer Complete
19 0x0024 USART, RX USART Rx Complete
20 0x0026 USART, UDRE USART, Data Register Empty
21 0x0028 USART, TX USART, Tx Complete
22 0x002A ADC ADC Conversion Complete
23 0x002C EE READY EEPROM Ready
24 0x002E ANALOG COMP Analog Comparator
25 0x0030 TWI 2-wire Serial Interface
26 0x0032 SPM READY Store Program Memory Ready
: Reset and Interrupt Vectors in ATmega328 and ATmega328P

Handling the External Interrupts with Arduino

  • Vrátime sa teda k predchádzajúcemu zdrojovému kódu, kedy sme na detekciu pohybu využili metódu polling-u. Tentokrát sa problém pokúsime vyriešiť pomocou prerušenia. Konkrétne sa bude jednať o externé prerušenie, keďže prerušenie vyvolá PIR senzor tým, že pošle signál do mikrokontroléra vtedy, keď bude detekovať pohyb.

  • (slide) Ošetrenie prerušenia sa inicializuje pomocou funkcie attachInterrupt(). Inicializácia môže prebehnúť kdekoľvek v programe, ale pokiaľ sa ošetrenie prerušenia v programe nemení, je ideálne ho inicializovať vo funkcii setup(). Táto funkcia má tri parametre, ktoré hovoria o tom, čo všetko je potrebné vedieť pri ošetrovaní prerušenia:

    1. číslo prerušenia, ktoré je potrebné ošetriť,
    2. názov (resp. adresu) ISR funkcie, ktorá sa zavolá na jeho ošetrenie, a
    3. režim prerušenia, ktorý definuje, aké správanie na pin-e vyvolá prerušenie.
  • (slide) Pozrime sa najprv na to, aké správanie na pin-e vyvolá prerušenie. Prototypovacia doska Arduino UNO pozná tieto štyri režimy prerušenia:

    1. LOW - Prerušenie je vyvolané vtedy, keď sa na pine nachádza úroveň LOW. Tento proces sa deje napríklad vtedy, ak je tlačidlo pripojené k digitálnemu pinu v režime INPUT_PULLUP a po jeho stlačení sa na pin privedie úroveň LOW.

    2. CHANGE - Prerušenie je vyvolané vtedy, keď dôjde k zmene úrovne pinu buď z HIGH na LOW alebo z LOW na HIGH. Tento proces sa deje napríklad pri stláčaní prepínača.

    3. RISING - Prerušenie je vyvolané vtedy, keď dôjde k zmene úrovne z LOW na HIGH. Tento proces sa deje napríklad pri stlačení tlačidla.

                   HIGH
                +----------
                |
                |
                ^
          LOW   |
       ------->-+
    4. FALLING - Prerušenie je vyvolané vtedy, keď dôjde k zmene úrovne z HIGH na LOW. Tento proces sa deje napríklad pri uvoľnení stlačeného tlačidla.

          HIGH
       -------->-+
                 |
                 V
                 |
                 |   LOW
                 +---------
  • (slide) Na základe uvedených režimov prerušenia mikrokontroléra sa je možné stretnúť ešte s nasledujúcim rozdelením externých prerušení:

    • Level Interrupts - Prerušenie je vyvolané zakaždým, keď sa na vstupe objaví signál konkrétnej úrovne (HIGH alebo LOW). Pri tomto type prerušenia je dobré dať pozor na to, že pri nezmenenom signále môže k prerušeniu dochádzať opakovane aj počas ošetrovania predchádzajúceho prerušenia.

    • Edge Interrupts - Prerušenie je vyvolané vtedy, keď dôjde k zmene jednej úrovne signálu na druhú (napr. ak dôjde k zmene úrovne z HIGH na LOW alebo z LOW na HIGH).

  • (slide) Každý mikrokontrolér je špecifický tým, že na zachytenie externého prerušenia z pripojeného zariadenia nie je možné použiť každý digitálny pin. Vždy je preto potrebné overiť si možnosti mikrokontroléra v jeho dokumentácii. V prípade mikrokontrolérov Arduino sa je možné orientovať pomocou tabuľky XXX.

Board Digital Pins Usable For Interrupts
Uno, Nano, Mini, other 328-based 2, 3
Uno WiFi Rev.2 all digital pins
Mega, Mega2560, MegaADK 2, 3, 18, 19, 20, 21
Micro, Leonardo, other 32u4-based 0, 1, 2, 3, 7
Zero all digital pins, except 4
MKR Family boards 0, 1, 4, 5, 6, 7, 8, 9, A1, A2
Due all digital pins
101 all digital pins (Only pins 2, 5, 7, 8, 10, 11, 12, 13 work with CHANGE)
  • V porovnaní s mikrokontrolérom ESP8266 je na tom ATmega 328P horšie, pretože je možné použiť piny GPIO0GPIO15. To umožňuje naraz sledovať až 16 rozličných prerušení.

  • PIR senzor je aktuálne v IoT zariadení na detekciu pohybu pripojený k pin-u č. 2. Ten podľa uvedenej tabuľky je možné použiť na zachytávanie externých prerušení.

  • Následne je potrebné už len poznať názvy ISR funkcií. V predchádzajúcom prípade sme používali funkcie dve a to alarm() a idle(). Funkcia alarm() bola zavolaná vtedy, keď došlo k zisteniu pohybu (na výstupnom pine PIR senzoru došlo k prechodu z úrovne LOW do úrovne HIGH). Funkcia idle() bola zasa vyvolaná vtedy, keď tento pohyb pominul (na výstupnom pine PIR senzoru došlo k prechodu z úrovne HIGH na úroveň LOW).

  • Vzhľadom na uvedené správanie vieme presne určiť, v akom režime prerušenia má byť zavolaná funkcia alarm() a v akom funkcia idle():

    • alarm() sa bude volať v režime RISING
    • idle() sa bude volať v režime FALLING
  • V kóde funkcie setup() to teda bude vyzerať nasledovne:

    attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                    alarm, RISING);
    attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                    idle, FALLING);
  • Tu však pozor! Každý mikrokontrolér obsahuje tabuľku vektorov prerušení (viď tabuľka XXX). Každý riadok v tejto tabuľke obsahuje informáciu o tom, ktorá ISR funkcia sa použije na ošetrenie ktorého zdroja prerušenia. To znamená, že na ošetrenie prerušenia na jednom vstupe sa použije len jedna ISR funkcia. V zázname sa nenachádza informácia o tom, v akom režime môže k prerušeniu dôjsť. Ak sme teda v našom prípade na jeden pin namapovali pomocou funkcie attachInterrupt() dve ISR funkcie, tak sme vlastne prepísali ošetrenie prerušenia prvou funkciou pomocou druhej funkcie. Kód sa úspešne preloží, aj sa spustí, ale k ošetreniu prerušenia dôjde len pomocou ISR funkcie idle() v režime FALLING.

  • Aj napriek tomu je však možné prekonfigurovať ošetrenie prerušenia neskôr v programe. Akonáhle teda k prerušeniu dôjde a vyvolá sa ISR funkcia, jej súčasťou bude volanie funkcie attachInterrupt() na nastavenie nového ošetrenia prerušenia za iných podmienok. Týmto spôsobom si budú jednotlivé ISR funkcie prehadzovať ošetrenie prerušenia navzájom.

  • Aby všetko pracovalo správne, je potrebné aplikovať tieto informácie a refaktorovať funkciu setup() z predchádzajúceho riešenia. Globálnu premennú isMovement je potrebné označiť kľúčovým slovom volatile a zadefinovať ošetrenie prerušenia pomocou ISR funkcie alarm() v režime RISING (zistenie pohybu). Aktualizovaný kód funkcie setup() sa nachádza vo výpise XXX.

    #include <Arduino.h>
    
    #define PIN_LED 8
    #define PIN_PIR 2
    
    volatile bool isMovement;
    
    void idle();
    void alarm();
    
    void setup(){
        // setup serial
        Serial.begin(9600);
        while(!Serial);
    
        // set pin modes
        pinMode(PIN_LED, OUTPUT);
        pinMode(PIN_PIR, INPUT);
    
        // initial state
        isMovement = false;
    
        // enter idle state
        idle();
    }
  • Následne je potrebné upraviť ISR funkcie alarm() a idle(). Okrem pôvodného správania v každej z nich predefinujeme ošetrenie prerušenia:

    • v prípade ISR funkcie alarm() predefinujeme ošetrenie prerušenia pomocou funkcie idle() v režime FALLING (zo stavu ALARM zariadenie prejde do stavu IDLE, ak na pine dôjde k prechodu z úrovne HIGH na úroveň LOW)

    • v prípade ISR funkcie idle() predefinujeme ošetrenie prerušenia pomocou funkcie alarm() v režime RISIGN (zo stavu IDLE zariadenie prejde do stavu ALARM, ak na pine dôjde k prechodu z úrovne LOW na úroveň HIGH)

  • Po úprave budú tieto funkcie vyzerať takto:

    void idle(){
        Serial.println("> Idle State");
    
        // update state
        isMovement = false;
        digitalWrite(PIN_LED, LOW);
    
        // (re)attach interrupt
        attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                        alarm, RISING);
    }
    
    void alarm(){
        Serial.println("> Alarm State");
    
        // update state
        isMovement = true;
        digitalWrite(PIN_LED, HIGH);
    
        // (re)attach interrupt
        attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                        idle, FALLING);
    }
  • Nakoniec už zostáva len funkcia loop(), ktorá bude v tomto prípade prázdna. Celý kód, ktorý bol použitý v prípade riešenia pomocou metódy pooling bol totiž presunutý do ISR funkcií. Týmto sme využili všetky výhody, ktoré ponúka mechanizmus prerušení a odstránili sme všetky nedostatky, ktoré ponúka polling. Vo funkcii loop() teda zostáva priestor na vlastný ḱód, ktorý môže na základe globálnej premennej isMovement vykonávať ďalšie operácie.

    void loop(){
        // nothing to do :-/
    }
  • Kompletný výpis kódu sa nachádza vo výpise XXX.

    #include <Arduino.h>
    
    #define PIN_LED 8
    #define PIN_PIR 2
    
    bool isMovement;
    
    void idle();
    void alarm();
    
    void idle(){
        Serial.println("> Idle State");
    
        // update state
        isMovement = false;
        digitalWrite(PIN_LED, LOW);
    
        // (re)attach interrupt
        attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                        alarm, RISING);
    }
    
    void alarm(){
        Serial.println("> Alarm State");
    
        // update state
        isMovement = true;
        digitalWrite(PIN_LED, HIGH);
    
        // (re)attach interrupt
        attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                        idle, FALLING);
    }
    
    void setup(){
        // setup serial
        Serial.begin(9600);
        while(!Serial);
    
        // set pin modes
        pinMode(PIN_LED, OUTPUT);
        pinMode(PIN_PIR, INPUT);
    
        // initial state
        isMovement = false;
    
        // enter idle state
        idle();
    }
    
    void loop(){
        // nothing to do :-(
    }
  • Toto riešenie je možné ešte zjednodušiť a vrátiť sa tak k pôvodnému konceptu, kedy boli prerušenia RISING a FALLING zadefinované naraz. Upravíme zapojenie tak, že výstup z PIR senzoru pripojíme naraz ku pinu 2 aj 3. na pine 2 bude mikrokontrolér sledovať prerušenie typu RISING a na pine 3 prerušenie typu FALLING.

  • Upravená schéma zapojenia sa nachádza na obrázku XXX a aktualizovaná podoba kódu riešenia sa nachádza vo výpise XXX.

    #include <Arduino.h>
    
    #define PIN_LED 8
    #define PIN_IDLE 2
    #define PIN_ALARM 3
    
    bool isMovement;
    
    void alarm();
    
    void idle(){
        Serial.println("> Idle State");
    
        // update state
        isMovement = false;
        digitalWrite(PIN_LED, LOW);
    }
    
    void alarm(){
        Serial.println("> Alarm State");
    
        // update state
        isMovement = true;
        digitalWrite(PIN_LED, HIGH);
    }
    
    void setup(){
        // setup serial
        Serial.begin(9600);
        while(!Serial);
    
        // set pin modes
        pinMode(PIN_LED, OUTPUT);
        pinMode(PIN_ALARM, INPUT);
        pinMode(PIN_IDLE, INPUT);
    
        // initial state
        isMovement = false;
    
        // attach interrupt
        attachInterrupt(digitalPinToInterrupt(PIN_ALARM), 
                        alarm, RISING);
        attachInterrupt(digitalPinToInterrupt(PIN_IDLE), 
                        idle, FALLING);
    }
    
    void loop(){
        // nothing to do :-(
    }
  • Týmto prístupom sa síce sprehľadnil a zjednodušil kód, ale stratila sa možnosť využiť druhý pin pre možnosť sledovania prerušení z ďalšieho zariadenia. Preto je vždy dobré takýto prístup zvážiť.

Turning Interrupts On and Off

  • Ošetrovanie prerušení je po zapnutí mikrokontrolérov ATmega 328P zapnuté. Automaticky sa však vypnú v okamihu, keď dôjde k ošetreniu prerušenia a pomocou ISR funkcie a opät sa automaticky zapnú, keď sa ISR funkcia ukončí.

  • Existujú však dve makrá, pomocou ktorých je možné ošetrovanie prerušení globálne vypínať a zapínať aj počas vykonávania ISR funkcie. Tieto makrá sú:

    • interrupts() - globálne povolí ošetrovanie prerušení

    • noInterrupts() - globálne zakáže ošetrovanie prerušení

  • Nasledujúci fragment kódu ilustruje situáciu, ak by sme chceli opätovne zapnúť prerušenia v ISR funkcii alarm():

    void alarm(){
        interrupts();
        isMovement = true;
        digitalWrite(PIN_LED, HIGH);
        attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                        idle, FALLING);
    }

Handling the Internal Interrupts with Arduino

  • Do kategórie interných prerušení patria časovače. Pomocou nich je možné napríklad presne načasovať spúšťanie časti programu, čo sa častokrát používa aj na vytvorenie dojmu “paralelizácie” vykonávaných úloh.

  • Mikrokontrolér ATmega328P obsahuje 3 časovače označené ako TIMER0, TIMER1 a TIMER2. TIMER0 a TIMER28 bitové, zatiaľ čo TIMER2 je 16 bitový časovač. Ich funkcionalita však závisií od frekvencie mikrokontroléra. V prípade Arduina je použitý externý oscilátor s frekvenciou 16 MHz.

  • Kažý časovač môže generovať jeden alebo viac prerušení. Tieto sú:

    • Compare Match Interrupt - Tento typ prerušenia vznikne v situácii, keď je potrebné, aby sa časovač zastavil pri dosiahnutí konkrétnej hodnoty. Jeho aktuálna hodnota sa porovnáva s požadovanou a keď dôjde k zhode, dôjde k vyvolaniu tohto prerušenia.

    • Overflow Interrupt - Každý časovač používa vnútorné počítadlo, ktoré má svoj rozsah v závislosti od veľkosti použitého registra. Hodnota počítadla sa postupne zvyšuje, a keď je dosiahnutá maximálna hodnota a tá sa znova zvýši o jednotku, dôjde k pretečeniu a vyvolaniu tohto typu prerušenia.

    • Input Capture Interrupt - Tento typ prerušenia sa používa na zachytenie vzniknutej udalosti na pine. K prerušeniu dôjde vtedy, keď je na pine zaznamenaná konkrétna hrana signálu. Tá môže byť vzostupná, zostupná alebo akákoľvek. Časovač dokáže zaznamenať čas vzniku tejto udalosti.

  • S použitím týchto časovačov sa je možné stretnúť pri programovaní Arduina bežne. Časovač TIMER0 sa používa pre funkcie pracujúce s časom, ako sú delay(), millis() a micros(). TIMER1 je zasa možné nájsť v knižnici Servo.h a TIMER2 sa používa pri generovaní zvuku pomocou funkcie tone(). Funkcia analogWrite() využíva každý časovač, ale každý pre rozličné piny.

  • Prehľad vlastností dostupných časovačov na mikrokontroléri ATmega328P sa nachádza v tabuľke XXX.

Timer Size Range Possible Interrupts
TIMER0 8 bits 0-255 Compare Match, Overflow
TIMER1 16 bits 0-65535 Compare Match, Overflow, Input Capture
TIMER2 8 bits 0-255 Compare Match, Overflow
  • Osobitným časovačom je Watchdog. Ak sa zariadenie dostane do chybového stavu, je jeho úlohou po uplynutí časového intervalu mikrokontrolér reštartovať. Pre prácu s Watchdog časovačom je k dispozícii knižnica avr/wdt.h.

TimerOne Library

  • S časovačmi je možné pracovať dvoma spôsobmi: pomocou štandardných AVR knižníc alebo pomocou knižníc tretích strán. A práve knižnica TimerOne ponúka veľmi jednoduché API, ktoré zakryje nízkoúrovňový prístup ponúkaný knižnicami AVR.

  • Táto knižnica poskytuje kolekciu funkcií na nastavenie 16 bitového časovača označovaného ako TIMER1 (odtiaľ názov knižnice). Pôvodným zámerom bolo vytvoriť jednoduchý a rýchly spôsob na nastavenie periódy PWM. Nakoniec však bol do knižnice zahrnutý aj prerušenie timer overflow a iné vlastnosti.

  • Na prácu s knižnicou stačí poznať len tieto funkcie:

    • Timer1.initialize(microseconds) - Táto funkcia inicializuje prácu s časovačom a musí byť zavolaná ako prvá. Parameter microseconds definuje periódu časovača.
    • Timer1.attachInterrupt(function) - Vždy po uplynutí periódy sa spustí ISR funkcia, ktorej názvoj je uvedený v parametri.
    • Timer1.detachInterrupt() - Zakáže prerušenie, takže ISR funkcia sa prestane spúšťať.
  • Pomocou knižnice je však možné ovládať aj už spustený časovač pomocou týchto funkcií:

    • Timer1.start() - Spustí časovač a začne novú periódu.
    • Timer1.stop() - Zastaví časovač.
    • Timer1.restart() - Reštartuje časovač od začiatku novej periódy.

Timers in Motion Detection Scenario

  • Pre ilustráciu použitia časovačov upravíme scenár vytváraného zariadenia na detekciu pohybu. K existujúcim stavom bude pridaný nový stav s názvom WATCHING. Do neho systém prejde zo stavu IDLE po zistení pohybu. V stave WATCHING sa zasvieti LED dióda a spustí sa časovač. Pokiaľ sa v priebehu najbližších 10 sekúnd pohyb neukončí, systém prejde do stavu ALARM a LED dióda sa rozbliká. Ak sa ale v priebehu týchto 10 sekúnd pohyb stratí, dióda zhasne a systém prejde späť do stavu IDLE. Zo stavu ALARM je možné prejsť do stavu IDLE stlačením tlačidla. Aktualizovaný stavový diagram sa nachádza na obrázku XXX.

  • Keďže do systému pribudlo tlačidlo, dôjde aj k atualizácii schémy zapojenia. Tá sa nachádza na obrázku XXX.

  • Zmien sa dočkal aj samotný kód. Vo funkci setup() prebehne štandardná inicializácia, ale rovnako sa v nej inicializuje časovač a nastaví sa jeho perióda na 1 sekundu. Z funkcie sa systém dostane rovno do stavu IDLE.

  • Novou je funkcia watch(). Tá v premennej countdown nastaví 10 sekundový odpočet a nastaví ISR funkciu tick() pre ošetrenie prerušenia časovača. Tá sa bude volať každú sekundu a zakaždým z premennej countdown odpočíta 1. Pri dosiahnutí hodnoty 0 systém prejde do stavu ALARM.

  • Kompletný výpis kódu sa nachádza vo výpise XXX.

    #include <Arduino.h>
    #include <TimerOne.h>
    
    #define PIN_LED 8
    #define PIN_PIR 2
    #define PIN_BTN 3
    
    volatile bool isMovement;
    volatile byte countdown;
    
    void idle();
    void watch();
    void alarm();
    
    void idle(){
        Serial.println("> Idle State");
    
        isMovement = false;
        digitalWrite(PIN_LED, LOW);
    
        // reattach interrupts
        detachInterrupt(digitalPinToInterrupt(PIN_BTN));
        attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                        watch, RISING);
        Timer1.detachInterrupt();
    }
    
    void tick(){
        countdown--;
        if(countdown == 0){
            alarm();
        }
    }
    
    void watch(){
        Serial.println("> Watch State");
    
        // update state
        digitalWrite(PIN_LED, HIGH);
    
        // reattach interrupts
        attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                        idle, FALLING);
        countdown = 10;
        Timer1.attachInterrupt(tick);
        Timer1.restart();
    }
    
    void alarm(){
        Serial.println("> Alarm State");
    
        // update state
        digitalWrite(PIN_LED, HIGH);
        isMovement = true;
    
        // reattach interrupts
        Timer1.detachInterrupt();
        detachInterrupt(digitalPinToInterrupt(PIN_PIR));
        attachInterrupt(digitalPinToInterrupt(PIN_BTN), 
                        idle, LOW);
    }
    
    void setup(){
        // set pin modes
        pinMode(PIN_LED, OUTPUT);
        pinMode(LED_BUILTIN, OUTPUT);
        pinMode(PIN_PIR, INPUT);
        pinMode(PIN_BTN, INPUT_PULLUP);
    
        Serial.begin(9600);
        while(!Serial);
    
        // setup timer
        Timer1.initialize(1 * 1000000);
    
        // enter idle state
        idle();
    }
    
    void loop(){
        if (isMovement == true){
            digitalWrite(PIN_LED, HIGH);
            delay(1000);
            digitalWrite(PIN_LED, LOW);
            delay(1000);
        }
    }

Pros and Cons of Interrupts

  • Výhody:

    • lepší reakčný čas v porovnaní s polling-om
    • šetrenie zdrojov
  • Nevýhody:

    • Prerušenia sa výrazne horšie ladia, pretože k prerušeniu môže dôjsť aj počas ladenia programu. A vy zrazu neviete, koľko prerušení bolo vykonaných medzi krokovaním aktuálnej časti programu.

Measuring Power when Using Interrupts

  • žiadna zmena

Putting Your Device to Sleep

  • To, čo sa aktuálne podarilo použitím prerušení, je odstrániť kód z hlavnej slučky programu. Mohlo by sa zdať, že tým, že je táto slučka prázdna, je možné ušetriť energiu. Toto zdanie je však mylné. Mikrokontrolér je totiž stále aktívny a stále v ňom dochádza k volaniu funkcie loop(), aj keď je jej telo prázdne. Môžete si vyskúšať, čo dosiahnete na svojom počítači, ak v interpretéri príkazového riadku napíšete takýto skript:

    while true; do
        # nothing to do in here
        :
    done
  • Ak sa následne pozriete na vyťaženie procesora uvidíte, že je (v prípade viacproserového systému je jedno jadro) vyťažené na 100%. A to aj napriek tomu, že uvedený fragment kódu nič nerobí.

  • Ako teda usporiť energiu v prípade, ak je obsah funkcie loop() prázdny a mikrokontrolér “nič” nevykonáva? Odpoveďou je uspanie mikrokontroléra.

What is Sleep Mode?

  • Režim spánku je špeciálny režim mikrokontroléra, do ktorého je možné mikrokontrolér prepnúť v čase jeho neaktivity. V tomto režime sa mikrokontrolér prepne do režimu nízkej spotreby, čím je možné elektrickú energiu ušetriť. Jej ušetrené množstvo závisí od toho, aké všetky komponenty zostanú napájané aj po prechode do režimu spánku. Stav mikrokontroléra sa v režime spánku nestratí, pretože zostáva v jeho pamäti.

  • Princíp fungovania je podobný, ako keď váš domáci spotrebič, ako je napr. televízor, DVD prehrávač alebo set-top box, vypnete pomocou diaľkového ovládača. Zariadenie sa nevypne úplne, pretože čaká na signál z diaľkového ovladača, ktorý ho opätovne zobudí (zapne) a uvedie do plnej prevádzky. Počas režimu spánku je však jeho odber elektrickej energie minimálny v porovnaní s množstvom elektrickej energie, ktorú odoberá pri plnej prevádzke. Mnohé zariadenia tento režim reprezentujú pomocou červenej LED diódy.

  • Režim spánku nefunguje bez použitia prerušení. Ak sa raz mikrokontrolér uspí, okrem prerušenia ho vie zobudiť len reset (ktorý je vlastne tiež prerušením). Prerušenie v tomto prípade funguje ako budík - zobudí mikrokontrolér, ktorý prerušenie automaticky obslúži pomocou príslušnej IRS funkcie. Následne môže mikrokontrolér opäť uspať alebo sa začne vykonávať hlavný program.

Putting Arduino to Sleep

  • (slide) Množstvo ušetrenej energie bude závisiť od toho, akú prototypovaciu dosku Arduino použijete. Rozličné verzie Arduín obsahujú rozličné súčasti, ktoré tiež spotrebujú nejakú energiu. Ak napríklad použijete Arduino Uno, v režime spánku bude mať spotrebu 19 mA, zatiaľ čo v bežnej prevádzke bude mať spotrebu v rozmedzí 3040 mA. Ak ale použijete Arduino Pro Mini, jeho spotreba počas spánku bude len 0.57 mA a v bežnej prevádzke 25 mA. Rozdiel je teda značný.

    • TODO: blokova schema arduina so sucastami, ktore je mozne vypnut
  • To, aké režimy spánku podporuje konkrétny mikrokontrolér, je potrebné vždy overiť v jeho dokumentácii. V prípade mikrokontroléra ATmega328P, ktorý je srdcom Arduino Uno sa jedná o 6 režimov, z ktorých je len 5 dostupných v hlavičkovom súbore avr/sleep.h:

    • Idle (SLEEP_MODE_IDLE)
    • ADC Noise Reduction (SLEEP_MODE_ADC)
    • Power-save (SLEEP_MODE_PWR_SAVE)
    • Standby (SLEEP_MODE_STANDBY)
    • Power-down (SLEEP_MODE_PWR_DOWN)
  • Najmenej úsporným režimom je režim Idle. Je to taktiež predvolený režim, takže ak počas behu programu nebude explicitne zvolený iný režim, pri uvedení mikrokontroléra do režimu spánku sa použije režim Idle. Z tohto režimu je možné mikrokontrolér zobudiť takmer ľubovoľným spôsobom.

  • Najviac úsporným režimom je režim Power-down. V tomto režime je zakázaných najviac súčastí mikrokontroléra a zo spánku ho je možné prebudiť len pomocou externých prerušení.

  • Jednotlivé režimy sa od seba navzájom líšia tým, aké všetky súčasti mikrokontroléra budú vypnuté, ako aj tým, akým spôsobom bude zasa mikrokontrolér prebudený. V tabuľke XXX sa nachádza prehľad možností, ktorými je možné mikrokontrolér ATmega328P zobudiť. Pre konkrétny mikrokontrolér je vždy dobré overiť jeho možnosti v dokumentácii.

Wake-up Sources Idle ADC Noise Reduction Power-save Standby Power-down
INT1, INT0 and Pin Change X X X X X
TWI Address Match X X X X X
Timer2 X X X
SPM/EEPROM Ready X X
A/D Converter X X
Watchdog Timer X X X X X
Other I/O X
  • Netreba zabudnúť na to, že mikrokontrolér sa dá z každej úrovne spánku vždy úspešne prebudiť pomocou RESET-u.

  • Pre prácu s režimom spánku je potrebné do programu načítať hlavičkový súbor avr/sleep.h. Ten obsahuje všetky potrebné makrá a funkcie na prácu s režimom spánku. Vo všeobecnosti bude stačiť použiť tieto z nich:

    • set_sleep_mode() - funkcia na nastavenie režimu spánku, pričom parametrom môže byť len SLEEP_MODE_IDLE, SLEEP_MODE_ADC, SLEEP_MODE_PWR_SAVE, SLEEP_MODE_STANDBY alebo SLEEP_MODE_PWR_DOWN

    • sleep_mode() - makro na prechod do režimu spánku spolu s nastavením bitu SE (Sleep Enable) pred prechodom do spnánku a aj jeho vyčistením po zobudení

  • Jednoduchá ilustrácia uvedenia mikrokontroléra do režimu spánku sa nachádza v nasledujúcom fragmente kódu, ktorý predstavuje modifikáciu štandardného príkladu Blink. Na 500 ms sa rozsvieti vstavaná LED dióda, na čo sa mikrokontrolér uvedie do najtvrdšieho spánku (Power-down). Nakoľko však pred spánkom nebol zadefinovaný žiadny spôsob opätovného prebudenia, mikrokontrolér sa už nezobudí a LED dióda už nezhasne. To znamená, že úroveň HIGH zostane na pin-e nezmenená aj po uspatí. Jediný spôsob, ako ho opätovne prebudiť, je stlačiť tlačidlo RESET.

    #include <Arduino.h>
    #include <avr/sleep.h>
    
    void setup(){
        pinMode(LED_BUILTIN, OUTPUT);
        set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    }
    
    void loop(){
        // blink
        digitalWrite(LED_BUILTIN, HIGH);
        delay(500);
    
        // sleep
        sleep_mode();
    
        // unreachable code
        digitalWrite(LED_BUILTIN, LOW);
        delay(500);
    }

Waking the Microcontroller with External Interrupts

  • (slide) Upravíme prípad použitia zariadenia na rozpoznávanie pohybu. Tentokrát sa zariadenie po spustení uspí. Akonáhle PIR senzor detekuje pohyb, zariadenie sa zobudí a začne blikať externou diódou. Blikať bude dovtedy, kým sa nestlačí tlačidlo, ktoré zariadenie zresetuje, vypne LED diódu a zariadenie uspí.

  • Stavový diagram zariadenia sa veľmi nebude líšiť od predchádzajúceho. Zariadenie sa bude môcť opät nachádzať v jednom z dvoch stavov. Zmenia sa však prechody medzi stavmi.

  • Aktualizovaná schéma zapojenia sa nachádza na obrázku XXX.

  • Zdrojový kód riešenia sa nachádza vo výpise XXX. Za najväčšiu odlišnosť oproti predchádzajúcemu kódu je možné považovať prechod do režimu spánku.

    #include <Arduino.h>
    #include <TimerOne.h>
    #include <avr/sleep.h>
    
    #define PIN_LED 8
    #define PIN_PIR 2
    #define PIN_BTN 3
    
    volatile bool isMovement;
    volatile byte countdown;
    
    void idle();
    void watch();
    void alarm();
    
    void idle(){
        isMovement = false;
        digitalWrite(PIN_LED, LOW);
        digitalWrite(LED_BUILTIN, LOW);
    
        // reattach interrupts
        detachInterrupt(digitalPinToInterrupt(PIN_BTN));
        attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                        watch, RISING);
        Timer1.detachInterrupt();
    
        // go to sleep
        set_sleep_mode(SLEEP_MODE_PWR_DOWN);
        interrupts();
        sleep_mode();
    }
    
    void tick(){
        countdown--;
    
        if(countdown == 0){
            alarm();
        }
    
        // go to sleep
        interrupts();
        sleep_mode();
    }
    
    void watch(){
        // update state
        digitalWrite(PIN_LED, HIGH);
    
        // reattach interrupts
        set_sleep_mode(SLEEP_MODE_ADC);
        attachInterrupt(digitalPinToInterrupt(PIN_PIR), 
                        idle, FALLING);
        countdown = 10;
        Timer1.attachInterrupt(tick);
        Timer1.restart();
    
        // go to sleep
        interrupts();
        sleep_mode();
    }
    
    void alarm(){
        // update state
        digitalWrite(PIN_LED, HIGH);
        isMovement = true;
    
        // reattach interrupts
        Timer1.detachInterrupt();
        detachInterrupt(digitalPinToInterrupt(PIN_PIR));
        attachInterrupt(digitalPinToInterrupt(PIN_BTN), 
                        idle, LOW);
    }
    
    void setup(){
        // set pin modes
        pinMode(PIN_LED, OUTPUT);
        pinMode(PIN_PIR, INPUT);
        pinMode(PIN_BTN, INPUT_PULLUP);
    
        // setup timer and sleep mode
        Timer1.initialize(1 * 1000000);
    
        // enter idle state
        idle();
    }
    
    void loop(){
        if (isMovement == true){
            digitalWrite(PIN_LED, HIGH);
            delay(1000);
            digitalWrite(PIN_LED, LOW);
            delay(1000);
        }
    }
  • V tomto prevedení je možné vidieť ošetrenie externých prerušení. A to konkrétne level interrupt, ktorý reprezentuje tlačidlo a pin change interrupt, ktorý reprezentuje PIR senzor.

Low-Power Library

  • Mikrokontrolér často obsahuje aj súčasti, ktoré potrebujú osobitné nastavenia, ak chceme znížiť ich spotrebu energie. Jedná sa napríklad o:

    • A/D prevodník
    • analógový komparátor
    • Brown-out detect
    • 3 časovače
    • Watch-dog timer
  • Tieto súčasti mikrokontroléra je možné vypínať a zapínať selektívne. Je to možné dosiahnuť pomocou registrov mikrokontroléra alebo pomocou volaní makier nachádzajúcich sa v knižnici avr/power.h. Napríklad pre vypnutie A/D prevodníka je možné zavolať makro power_adc_disable() a pre jeho opätovné zapnutie zasa makro power_adc_enable().

  • Existuje však knižnica s názvom Low-Power, ktorá zjednodušuje prácu s režimom spánku a umožňuje pred prechodom do spánku vypínať požadované komponenty s cieľom dosiahnutia vyššej úspory energie. Knižnica má tieto vlastnosti:

    • podporuje všetky režimy spánku mikrokontroléra ATmega328P,
    • umožňuje mikrokontrolér uspať na 15 ms, 30 ms, 60 ms, 120 ms, 250 ms, 500 ms, 1 s, 2 s, 4 s, 8 s, a na stálo,
    • umožňuje vypnúť A/D prevodník,
    • umožňuje vypnúť modul Brownout Detector
    • vo vybraných režimoch spánku umožňuje vypnúť všetky časovače, USART0, TWI, a modul SPI
  • Knižnica sa používa prostredníctvom triedy LowPower, ktorá má niekoľko metód: idle(), adcNoiseReduction(), powerDown(), powerSave(), powerStandby() a powerExtStandby(). Každá z nich reprezentuje niektorý z režimov spánku mikrokontroléra. Pomocou parametrov týchto metód je možné nastaviť dĺžku strvania spánku ako aj zoznam súčastí, ktoré majú byť zapnuté alebo vypnuté.

  • Príklad použitia ilustruje nasledujúci riadok kódu. Pomocou neho sa mikrokontrolér uspí na 8 sekúnd v režime Power Down, pričom moduly ADC a BOD budú vypnuté.

    LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);