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.
Obecne sa je možné stretnúť s dvoma variantami:
bez použitia pohybového senzora - vtedy dochádza k zapnutiu svetla pri poklese intenzity vonkajšieho svetla pod konkrétnu úroveň.
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álLOW
odpory
(neskôr aj tlačidlo)
Poznámka
Ak budete hľadať PIR senzor v sade, ktorú sme vám odporúčali, tak ho tam nenájdete. Nemusíte však zúfať - dá sa jednoducho nahradiť pomocou tlačidla. To preto, že PIR senzor je digitálny vstupný senzor, ktorý dá vedieť, že došlo k pohybu len zmenou stavu z
LOW
naHIGH
. A to je jednoduché napodobniť práve pomocou tlačidla.
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:
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žstvomif
-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).
Poznámka
V rozličnej literatúre sa môžete stretnúť s rozličnou terminológiou. Tak sa napríklad môžete stretnúť okrem termínu stavový stroj aj s označením konečnostavový stroj (z angl. Finite-State Machine (FSM)), konečnostavový automat (z angl. Finite-State Automaton (FSA)) alebo konečný automat (z angl. Finite Automaton). Tieto termíny môžeme považovať za ekvivalentné.
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:
O tomto stavovom diagrame a teda o stavovom stroji, ktorý je ním modelovaný, vieme povedať nasledovné:
- pozostáva z dvoch stavov:
locked
aunlocked
- jeho počiatočným stavom (tzv. pseudostavom), do ktorého sa
stroj dostane pri zapnutí, je stav
locked
- prejsť zo stavu
locked
do stavuunlocked
vieme len vhodením mince - do stavu
unlocked
zo stavulocked
neprejdeme tým, že budeme na turniket len tlačiť (použitím sily ;) - zo stavu
unlocked
do stavulocked
sa budeme vedieť spätne dostať zatlačením na turniket (nepotrebujeme do neho hodiť žiadnu ďalšiu mincu)
- pozostáva z dvoch stavov:
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é.(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 }
Poznámka
Takúto štruktúru nebude mať len funkcia reprezentujúca stav
Day
, ale každá funkcia reprezentujúca stav správania nášho zariadenia.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 (LED, LOW); digitalWrite // main loop for(;;){ } // on leave } int main(){ // taken from original main.cpp (); init // setup (LED, OUTPUT); pinMode // 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))
obyčajne sa prenáša 8b, ku ktorým sa na začiatku pripojí
START bit
a na konciSTOP bit
s paritným bitom (môže a nemusí byť)
Upozornenie
Na doske Arduino Uno sa pre sériovú komunikáciu používajú
piny 0
a 1
. A preto vo svojich zapojeniach
tieto piny (pokiaľ nemusíte) nepoužívajte!
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 funkciisetup()
). To zabezpečíme volaním metódySerial.begin()
, ktorej jediným parametrom je rýchlosť komunikácie (tiež známy ako baudrate).Poznámka
Aj keď môže byť teoreticky prenosová rýchlosť ľubovoľná, väčšina UART (Universal Asynchronous Receiver/Transmitter) zariadení/mikročipov ponúka max. rýchlosť 230400 b/s.
(slide) Príklad nastavenia rýchlosti prenosu po sériovej linke na hodnotu 9600 b/s bude vyzerať takto:
.begin(9600); Serial
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() { .begin(9600); Serial} void loop() { .println("Hello world"); Serial}
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()
- funkcie na zapisovanie - napr.
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 .println(">> State Day Entered."); Serial(LED, LOW); digitalWrite // main loop for(;;){ } // on leave } int main(){ // taken from original main.cpp (); init // setup .begin(9600); Serial(LED, OUTPUT); pinMode // 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 stavuDay
sa stavový stroj dostane vtedy, ak intenzita svetla stúpne nad konkrétnu úroveň.Poznámka
Môžete si všimnúť, že intenzita svetla pri prechode zo stavu
Day
do stavuNight
nie je rovnaká, ako v prípade prechodu zo stavuNight
do stavuDay
. To preto, aby nedochádzalo k “blikavému stavu”, keď sa bude deň lámať na noc a opačne a zmena na úrovni 1% nemusí byť spôsobená len vychádzajúcim/zapadajúcim slnkom, ale napr. aj mrakmi.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.
(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äčší.
(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ť.
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.
(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:
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:
(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.
(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.
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)
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
po1023
. 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
A0
ažA5
, 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 rozsahu0
až1023
. 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ťA0
ažA5
. 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 funkcieanalogRead()
, tak aj fungovanie LDR senzora:#define LDR A0 int main(){ (); init // superloop for(;;){ int value = analogRead(LDR); .println(value); Serial(1000); delay} }
My však vo výsledku nechceme vidieť rozsah hodnôt od hodnoty
0
po hodnotu1023
. My potrebujeme tento rozsah previesť na percentá, pretože intenzitu svetla budeme vyhodnocovať týmto spôsobom:0%
- tma100%
- 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 rozsahu0
až1023
na rozsah0
až100
. Funkcia vracia celé číslo. V našom prípade funkciumap()
použijeme nasledovne:= map(value, 0, 1023, 0, 100); value
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 premennejstate
zavolá príslušnú funkciu reprezentujúcu príslušný stav nočného svetla:switch(state){ case DAY: = state_day(); state break; case NIGHT: = state_night(); state 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 .println(">> State Day Entered."); Serial(LED, LOW); digitalWrite // main loop int value; do{ (1000); delay= analogRead(LDR); value = map(value, 0, 1023, 0, 100); value }while(value > 40); // on leave return NIGHT; } enum state state_night(){ // on enter .println(">> State Night Entered."); Serial(LED, HIGH); digitalWrite // main loop int value; do{ (1000); delay= analogRead(LDR); value = map(value, 0, 1023, 0, 100); value }while(value < 45); // on leave return DAY; } int main(){ // taken from original main.cpp (); init // setup .begin(9600); Serial(LED, OUTPUT); pinModeenum state state = DAY; // superloop for(;;){ switch(state){ case DAY: = state_day(); state break; case NIGHT: = state_night(); state 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.