State Machines I.

statové stroje, stavové diagramy, sériová komunikácia, digitálny výstup, analógový vstup

Záznam z prednášky

Use-Case: Night Security Light

  • (slide) Predtým, ako sa začneme venovať dnešnej téme, si predstavíme prípadovú štúdiu, ktorá nás bude sprevádzať - vytvoríme jednoduché nočné svetlo. Zariadenie tohto typu môžete bežne kúpiť aj v bežných potravinácha ko Kaufland alebo Tesco.

    Night Light Illustration (zdroj)
  • Obecne sa je možné stretnúť s dvoma variantami:

    1. bez použitia pohybového senzora - vtedy dochádza k zapnutiu svetla pri poklese intenzity vonkajšieho svetla pod konkrétnu úroveň.

    2. s použitím pohybového senzora - o niečo drahší model, ale o to užitočnejší, kedy k zapnutiu svetla dôjde pri splnení dvoch podmienok: intenzita vonkajšieho svetla poklesla pod konkrétnu úroveň a bol detekovaný pohyb.

  • My sa budeme zároveň snažiť vytvoriť práve druhý variant obsahujúci pohybový senzor.

Ingredients

  • Ako som už naznačil, budeme potrebovať tieto komponenty:

    • LED diódu - bude reprezentovať samotné nočné svetlo; tí náročnejší z vás môžu použiť spínacie relé a zapojiť normálnu lampu na 220V, ale v našich podmienkach si vystačíme s LED-kou

    • fotorezistor (LDR) - rezistor, ktorého hodnota sa mení vplyvom slnečného žiarenia, resp. svetla

    • pohybový senzor (PIR) - senzor pohybu, ktorý pošle signál HIGH, keď bude detekovať pohyb, ináč bude posielať signál LOW

    • odpory

    • (neskôr aj tlačidlo)

Schematic

  • (slide) Ak si budete chcieť takéto svetlo postaviť aj doma alebo len experimentovať so mnou, riaďte sa podľa nesledujúceho zapojenia:

    Night Light: Schematic

State Machines

Introduction

  • (slide) Ak vývoj aplikácií pre dosky Arduino poznáte, tak viete, že celý “magic” sa deje vo vnútri funkcie loop(). Čo je horšie, častokrát sa nedeje nikde inde. Vo výsledku je to potom jedna pomerne robustná funkcia s množstvom if-else-ov, ktorá v prípade rozsiahlejších projektov môže mať niekoľko stoviek riadkov.

  • My sme sa však v rámci predmetu venovali aj tomu, čo je to modulárne programovanie a aké sú jeho výhody. A taktiež sme si ukazovali, že keď rozdelíme projekt na menšie časti (moduly), zvýši sa jeho prehľadnosť. Týmto spôsobom máte navrhnuté aj všetky zadania, na ktorých pracujete v rámci predmetu.

  • Keď teda “všetci” svoje programy robia takýmto spôsobom - existuje aj iná možnosť?

What is State Machine?

  • (slide) Mnoho projektov a možno skôr zariadení, v ktorých sa programujú mikrokontroléry, majú povahu správať sa ako tzv. stavové stroje. Čo to však je?

  • (slide) Stavový stroj reprezentuje model správania sa. Pozostáva z konečného počtu stavov, v ktorých sa zariadenie môže nachádzať, a prechodov medzi nimi. Stavový stroj sa môže vždy nachádzať práve v jednom stave. K prechodu medzi stavmi môže dôjsť na základe rôznych vstupov (napr. externých, ako je prijatie konkrétneho signálu/správy alebo interných, ako napr. vypršanie časovača).

State Diagrams

  • (slide) Pre grafickú reprezentáciu správania stavových strojov používame stavové diagramy. Stavový diagram sa skladá z dvoch základných grafických komponentov:

    • z konečného počtu stavov (elipsa alebo obdĺžnik so zaoblenými hranami), a
    • prechodov medzi stavmi (orientovaná hrana začínajúca v zdrojovom stave a končiaca v cieľovom stave) .
  • (slide) Príklad stavového diagramu, ktorý reprezentuje turniket, môžete vidieť na nasledujúcom obrázku:

    State Diagram: Turnstile (zdroj)
  • O tomto stavovom diagrame a teda o stavovom stroji, ktorý je ním modelovaný, vieme povedať nasledovné:

    • pozostáva z dvoch stavov: locked a unlocked
    • jeho počiatočným stavom (tzv. pseudostavom), do ktorého sa stroj dostane pri zapnutí, je stav locked
    • prejsť zo stavu locked do stavu unlocked vieme len vhodením mince
    • do stavu unlocked zo stavu locked neprejdeme tým, že budeme na turniket len tlačiť (použitím sily ;)
    • zo stavu unlocked do stavu locked sa budeme vedieť spätne dostať zatlačením na turniket (nepotrebujeme do neho hodiť žiadnu ďalšiu mincu)
  • Okrem samotných stavov a prechodov medzi stavmi môžeme do diagramu stavov vložiť aj ďalšie informácie, ako napr.:

    • podmienky prechodu
    • čo sa stane v stave pri prechode doň (on entry) alebo odchode z neho (on leave)

Programming Representation of State Machines

  • (slide) Ak by sme chceli stavový stroj naprogramovať, tak v prípade objektového programovania máme k dispozícii priamo návrhový vzor s menom state, pomocou ktorého to vieme jednoducho zvládnuť. O tom sa ale budete rozprávať v predmete Objektovo orientované programovanie.

  • V prípade procedurálneho programovania je situácia iná. Tu si totiž treba uvedomiť, že sa nejedná o algoritmický problém, ale skôr o organizáciu kódu. Jednotlivé stavy tu vieme reprezentovať samostatnými funkciami. A prechody medzi nimi zasa pomocou príkazu switch a riadiacej premennej, ktorá reprezentuje aktuálny stav.

Security Night Light as State Machine

  • Vráťme sa ale teraz naspäť k našej prípadovej štúdii nočného svetla, ktoré sa pokúsime reprezentovať v podobe stavového stroja.

State 1: The Day

  • (slide) Prvým ako aj počiatočným stavom bude stav Day. Do neho sa teda stroj dostane priamo po zapnutí. To, že sa stroj bude v tomto stave nachádzať, bude znamenať, že svetlo nebude zasvietené, pretože svietiť cez deň je zbytočné.

    Night Security Light: State Day
  • (slide) My tento stav budeme reprezentovať funkciou s názvom state_day(), ktorej štruktúra bude vyzerať nasledovne:

    void state_day(){
        // on enter
    
        // main loop
    
        // on leave
    }
  • Samozrejme okrem funkcie reprezentujúcej stav potrebujeme vytvoriť aj ostatnú kostru programu, ktorá bude zatiaľ vyzerať takto:

    #define LED 6
    
    void state_day(){
      // on enter
      digitalWrite(LED, LOW);
    
      // main loop
      for(;;){
      }
    
      // on leave
    }
    
    int main(){
      // taken from original main.cpp
      init();
    
      // setup
      pinMode(LED, OUTPUT);
    
      // run initial state
      state_day();
    }

Serial Communication

  • (slide) Máme však menší problém. Ak teraz skeč necháme preložiť a nahrať do mikrokontroléra, nemáme veľa možností, ako overiť, či skeč funguje ako má. Naozaj sa dostal do správneho stavu? To je veľmi dôležité pre testovanie. V prípade programovania v jazyku C by sme si pomohli pomocnými výpismi alebo spustením debugger-a. Čo však môžeme využiť tu?

  • (slide) Arduino má k dispozícii sériový port, pomocou ktorého je možné komunikovať buď s ďalšími zariadeniami alebo priamo s počítačom. To je veľmi výhodné pre ladenie programov.

  • Sériová komunikácia

    • je jednoduchá a lacná

    • (slide) na jej realizáciu stačia len dva káble (dokopy však treba štyri - komunikácia je obojsmerná (full duplex))

      Serial Communication: Wiring (zdroj)
    • obyčajne sa prenáša 8b, ku ktorým sa na začiatku pripojí START bit a na konci STOP bit s paritným bitom (môže a nemusí byť)

      Serial Communication

Serial Communication Setup

  • Pred použitím sériovej linky je potrebné ju inicializovať (v časti kódu označenej ako // setup alebo po starom vo funkcii setup()). To zabezpečíme volaním metódy Serial.begin(), ktorej jediným parametrom je rýchlosť komunikácie (tiež známy ako baudrate).

  • (slide) Príklad nastavenia rýchlosti prenosu po sériovej linke na hodnotu 9600 b/s bude vyzerať takto:

    Serial.begin(9600);

Importance of Same Baudrate

  • Nakoľko je komunikácia po sériovej linke asynchrónna (neprenáša sa tu žiadny synchronizačný signál - žiadne hodiny), komunikujúce zariadenia sa musia dohodnúť na prenosovej rýchlosti komunikácie. V opačnom prípade dôjde k príjmu nesprávnych údajov.

  • Ukážme si, ako to bude vyzerať v prípade, že sa obe strany na rýchlosti prenosu nezhodnú. Arduino IDE obsahuje jednoduchý monitor sériového portu, ktorý môžeme použiť ako na čítanie tak aj na zápis hodnôt do sériového portu. Na túto ukážku použijeme jemne modifikovanú predchádzajúcu verziu kódu:

    void setup() {
        Serial.begin(9600);
    }
    
    void loop() {
        Serial.println("Hello world");
    }
  • So sériovým portom je samozrejme možné komunikovať aj mimo Arduino IDE napr. priamo z OS Linux. K dispozícii existuje niekoľko nástrojov a jedným z nich je aj nástroj screen, ktorý je možné použiť nasledovne:

    $ screen /dev/ttyACM1 9600

Serial Communication API

  • Na prenos po sériovej linke je možné použiť dva základné typy funkcií:

    • funkcie na zapisovanie - napr. Serial.print(), Serial.println()
    • funkcie na čítanie - napr. Serial.read(), Serial.readString()

Code Refactoring

  • Rozšírime teda našu implementáciu o komunikáciu prostredníctvom sériovej linky, aby sme ju mohli používať hlavne na overenie správania zariadenia:

    void state_day(){
      // on enter
      Serial.println(">> State Day Entered.");
      digitalWrite(LED, LOW);
    
      // main loop
      for(;;){
      }
    
      // on leave
    }
    
    int main(){
      // taken from original main.cpp
      init();
    
      // setup
      Serial.begin(9600);
      pinMode(LED, OUTPUT);
    
      // run initial state
      state_day();
    }

State 2: The Night

  • (slide) Druhým stavom bude stav Night, do ktorého sa stavový stroj dostane vtedy, ak intenzita svetla poklesne pod konkrétnu úroveň. Ak sa stavový stroj bude nachádzať v tomto stave, bude svetlo svietiť. Naspäť do stavu Day sa stavový stroj dostane vtedy, ak intenzita svetla stúpne nad konkrétnu úroveň.

    Night Security Light: State Night
  • Ak sa pozrieme na tento stavový diagram, tak podľa neho vytvorené zariadenie už sme schopní predávať, pretože sa jedná o nočné svetlo bez pohybového senzora. Pre efekt môžeme vytvoriť práve takúto verziu zariadenia, kedy pri prechode do stavu Night sa svetlo zasvieti.

LDR Sensor

  • (slide) Pre získanie aktuálnej hodnoty intenzity svetla budeme potrebovať LDR (z angl. Light Dependent Resistor) senzor. Jedná sa o súčiastku, ktorá je citlivá na svetlo a túto zmenu reprezentuje zmenou veľkosti odporu.

    LDR: Sensor (zdroj)
  • (slide) Ak sa pozrieme na charakteristiku tohto senzora, tak vidíme, že čím viac svetla na senzor dopadá, tým je výsledný odpor menší. A naopak - v čím väčšej tme sa senzor nachádza, tým je odpor väčší.

    LDR: Characteristics (zdroj)
  • (slide) LDR senzor zapájame ako odporový delič, resp delič napätia. Na základe zapojenia LDR senzora si však treba uvedomiť, že dokážeme obrátiť spôsob jeho práce - a teda so zvyšujúcou sa intenzitou svetla sa bude výsledný odpor zvyšovať.

    LDR: Function (zdroj)

Analog Signal

  • Ako je zrejmé, zmenou intenzity svetla sa mení odpor a so zmenou odporu sa bude meniť aj úbytok napätia. To sa nemení skokovo, ako v prípade digitálneho vstupu (napr. stlačenie tlačidla, detekovanie pohybu, …), ale zmena prichádza spojito, postupne.

  • (slide) Takýto signál, ktorý sa v čase mení spojito (v každom čase má jednoznačnú hodnotu napätia), nazývame analógový signál.

    Analog Signal (zdroj)
  • (slide) Pre porovnanie - digitálny alebo diskrétny signál, ktorý nie je spojitý (nemá v každom čase jednoznačnú hodnotu napätia), vyzerá takto:

    Digital Signal (zdroj)

Analog to Digital Converter

  • (slide) Na prevod analógového signálu na digitálny potrebujeme mať k dispozícii analógovo-digitálny prevodník (označovaný aj ako A/D prevodník). Jeho úlohou je odmerať veľkosť vstupného napätia (amplitúdu) (alebo prúdu) a previesť ho na číslo zodpovedajúce jeho veľkosti.

  • A/D prevodníky sa veľmi často používajú v zariadeniach, ktoré merajú napätie alebo prúd. Rovnako sa používa v zariadeniach, ktoré merajú inú fyzikálnu veličinu a tú prevádzajú na elektrickú (senzory), ako napr. teplota, tlak, svetlo a pod.

  • (slide) Proces prevodu analógového signálu na digitálny je možné v zjednodušenej podobe rozdeliť do troch krokov:

    1. (slide) Vzorkovanie (z angl. sampling) - Vstupný analógový signál sa v procese vzorkovania najprv rozdelí na vzorky na základe vzorkovacej frekvencie. Tým sa získa postupnosť impulzov, ktorých amplitúda zodpovedá analógovému signálu v príslušných časových okamihoch. Čím je hodnota vzorkovacej frekvencie vyššia, tým vstupný analógový signál rozdelíme na väčší počet vzoriek a tým je aj výsledná digitalizácia vernejšia. Napr. pre CD kvalitu sa používa vzorkovacia frekvencia 44.1kHz.

      ADC: Sampling (zdroj)
    2. (slide) Kvantovanie (z angl. quantizing) - V tomto kroku dochádza k prevodu amplitúdy analógového signálu (alebo jej odmeraniu) na diskrétnu (digitálnu) hodnotu. Presnosť tohto procesu je daná rozlíšením prevodníka.

      ADC: Quantizing (zdroj)
    3. Kódovanie (z angl. encodign) - V poslednom kroku dôjde k prevodu diskrétnej hodnoty do binárnej podoby, ktorú následne A/D prevodník vráti ako výstup svojej činnosti.

  • (slide) Prevodník je charakteristický svojim rozlíšením. Rozlíšenie prevodníku je číslo, ktoré je dané počtom rozlíšiteľných úrovní analógového signálu. Udáva sa v bitoch. Ak by bol prevodník napr. 3 bitový, vedel by dokopy rozlíšiť 8 úrovní vstupného analógového signálu (pretože 2^3 = 8)

    ADC: Resolution (zdroj)

Arduino’s ADC

  • (slide) Mikrokontrolér Arduino obsahuje A/D prevodník s rozlíšením 10 bitov (zvykne sa označovať aj ako 10 bitový prevodník). To znamená, že môže nadobúdať hodnoty v rozsahu od 0 po 1023. Jeden “dielik” teda zodpovedá úrovni približne 4.88mV (5V/1024).

  • (slide) Ak teda odčítame z analógového vstupu hodotu 567, tak je napätie na vstupe približne 2.77V.

    \[567 \times \frac{5}{1024} = 567 \times 4.88^{-3} = 2.77V\]

  • Tento prevodník obsahuje 6 kanálov, čomu zodpovedajú vstupy A0A5, nazývané tiež ako analógové vstupy (iné verzie dosiek Arduino môžu mať iný počet kanálov). Analógové vstupy však je možné rovnako použiť ako štandardné digitálne vstupy alebo výstupy.

  • (slide) Na odčítanie analógovej hodnoty pomocou mikrokontroléra Arduino používame funkciu analogRead(), ktorá vráti celé číslo v rozsahu 01023. Jej parametrom je číslo analógového portu, ktorý chceme odčítať. Je možné priamo využiť konštanty, takže miesto písania samotného čísla je možné písať A0A5. Teda použiť identické označenie, aké je priamo na doske.

Reading Analog Values from LDR

  • Pripojme teda LDR senzor cez napäťový delič k analógovému pinu A0. Nasledujúci kód ilustruje ako použitie funkcie analogRead(), tak aj fungovanie LDR senzora:

    #define LDR A0
    
    int main(){
        init();
    
        // superloop
        for(;;){
            int value = analogRead(LDR);
            Serial.println(value);
            delay(1000);
        }
    }
  • My však vo výsledku nechceme vidieť rozsah hodnôt od hodnoty 0 po hodnotu 1023. My potrebujeme tento rozsah previesť na percentá, pretože intenzitu svetla budeme vyhodnocovať týmto spôsobom:

    • 0% - tma
    • 100% - svetlo
  • (slide) Na tento účel môžeme použiť funkciu map(). Táto funkcia premapuje hodnotu z jedného rozsahu do druhého. V našom prípade to bude teda mapovanie z rozsahu 01023 na rozsah 0100. Funkcia vracia celé číslo. V našom prípade funkciu map() použijeme nasledovne:

    value = map(value, 0, 1023, 0, 100);

Code Refactoring

  • (slide) A môžeme sa vrátiť k nášmu projektu nočného svetla. Pokračovať budeme organizáciou kódu.

  • (slide) Keďže tentokrát budeme potrebovať rozlišovať medzi dvoma stavmi, ktoré môžu nastať na základe konrétnej podmienky, budeme potrebovať premennú, ktorej hodnota bude slúžiť na rozlišovanie aktuálneho stavu. Okrem toho si pre zvýšenie čitateľnosti kódu pre reprezentáciu stavu vytvoríme samostatný enumeračný typ s názvom state:

    enum state {
        DAY,
        NIGHT
    };
  • Každá funkcia, ktorá reprezentuje správanie v príslušnom stave, bude po svojom skončení vracať nový stav. Jej deklarácia teda bude po novom vyzerať takto:

    enum state state_night();
  • Samozrejme adekvátne upravíme aj podobu hlavnej slučky programu vo funkcii main(), kde sa aktuálne bude nachádzať switch, ktorý na základe stavovej premennej state zavolá príslušnú funkciu reprezentujúcu príslušný stav nočného svetla:

    switch(state){
        case DAY:
            state = state_day();
            break;
    
        case NIGHT:
            state = state_night();
            break;
    }

Final Solution

  • Výsledný kód bude vyzerať nasledovne:

    #define LED 6
    #define LDR A0
    
    enum state {
      DAY,
      NIGHT
    };
    
    
    enum state state_day(){
      // on enter
      Serial.println(">> State Day Entered.");
      digitalWrite(LED, LOW);
    
      // main loop
      int value;
      do{
        delay(1000);
        value = analogRead(LDR);
        value = map(value, 0, 1023, 0, 100);
      }while(value > 40);
    
      // on leave
      return NIGHT;
    }
    
    
    enum state state_night(){
      // on enter
      Serial.println(">> State Night Entered.");
      digitalWrite(LED, HIGH);
    
      // main loop
      int value;
      do{
        delay(1000);
        value = analogRead(LDR);
        value = map(value, 0, 1023, 0, 100);
      }while(value < 45);
    
      // on leave
      return DAY;
    }
    
    
    int main(){
      // taken from original main.cpp
      init();
    
      // setup
      Serial.begin(9600);
      pinMode(LED, OUTPUT);
      enum state state = DAY;
    
      // superloop
      for(;;){
        switch(state){
          case DAY:
              state = state_day();
            break;
    
          case NIGHT:
              state = state_night();
              break;
        }
      }
    }

Conclusion

  • (slide) Dnes sme si predstavili abstraktný model s názvom stavový stroj a ukázali sme si, že mnoho riešení pre mikrokontrolérov vieme reprezentovať ako jednoduché stavové stroje. Začali sme vytvárať prípadovú štúdiu bezpečnostné nočné svetlo, kde sme stihli vytvoriť dva stavy reprezentujúce správanie tohto stroja.

  • Okrem toho sme si ukázali, ako pracuje A/D prevodník, ukázali sme rozdiel medzi analógovým a digitálnym signálom, predstavili sme si fotorezistor a upratali sme kód riešenia, ktorý sa zďaleka podobá na štandardné Arduino skeče.

  • Riešenie dotiahneme do konca na budúcej prednáške, kde sfunkčníme pohybový senzor a prídame ešte jeden stav.