Představte si jednoduchou aplikaci, která má za úkol provádět několik činností a má být schopná relativně svižně reagovat na vnější podněty. Může například přijímat zprávy po UARTu, reagovat na externí přerušení, sledovat úroveň napětí, přijímat zprávy pomocí dálkové ovladače atp. Často se taková aplikace řeší tak, že periferie (UART, AD převodník, časovač) volají rutiny přerušení. Aplikace v nich pak provádí dílčí činnost a jakmile vyžaduje nějakou zdlouhavější reakci, přenechá ji hlavní smyčce. Například přijímá-li aplikace zprávy po UARTu tak si v rutině přerušení ukládá znaky do paměti a kontroluje zda je zpráva kompletní. Teprve kompletní zprávu předá k dekódování a dalšímu zpracování hlavní smyčce (neboť tato činnost bývá typicky zdlouhavější). Postup jsem načrtnul v následujícím pseudokódu.
volatile char zprava_uart=0,zprava_ir=0,prekroceni_napeti=0; void main(void){ inicializace(); while(1){ // stále dokola kontroluj zda některá rutina přerušení nesignalizuje událost if(zprava_uart){ zprava_uart=0; zpracovani_uart_zpravy(); } if(zprava_ir){ zprava_ir=0; zpracovani_ir_zpravy(); } if(prekroceni_napeti){ prekroceni_napeti=0 reakce_na_prekroceni_napeti(); } sleep(); // proč nespat když nás přerušení probudí až se něco stane ? (chyba) } } ISR(UART){ if(zprava_kompletni){ zprava_uart=1; } } ISR(IR){ if(zprava_kompletni){ zprava_ir=1; } } ISR(ADC){ if(napeti_vysoke){ prekroceni_napeti=1; } }
V našem případě zprostředkovávají reakci na všechny vnější události periferie, takže můžeme jádro mikrokontroléru během nečinnosti uspávat. I ten nejmělčí spánek (v našem případě Idle) může snížit spotřebu na polovinu nebo i méně. Zdálo by se tedy že stačí prostě na konec smyčky přidat příkaz sleep() a je hotovo (viz stručná poznámka na konci článku pokud jste se ještě s uspáváním mikrokontrolerů nesetkali). Jenže právě tady začíná drobný problém o němž bude tento příspěvek.
Nejprve si problém demonstrujeme na konkrétním příkladě. Máme demonstrační aplikaci, jejímž úkolem je reagovat na vzestupnou hranu na pinu PA5 tím že vygeneruje krátký impulz na výstupu PA4. Vzestupnou hranu na vstupu snímáme pomocí externího přerušení a v jeho rutině program nastaví vlajku flag1. Hlavní smyčka programu pak na nastavenou flag1 zareaguje (v našem případě vygenerováním zmíněného pulzíku). Modelově je v aplikaci ještě další dvojice vlajek (flag2 a flag3), které představují další činnosti. Pro jednoduchost jsou vlajky flag2 a flag3 vždy nulové a aplikace na ně nijak reagovat nebude.
Pokud bychom na poslední řádek hlavní smyčky programu prostě přidali příkaz sleep (jak je naznačeno v pseudokódu) byla by to chyba. Představme si totiž následující situaci. Program vyhodnotí podmínku if(flag1) s tím, že flag1 je nulová a začne vyhodnocovat podmínku if(flag2). Zrovna v tom okamžiku přijde vzestupná hrana na vstup. Vykoná se rutina přerušení, která nastaví flag1 na jedničku. Po skončení rutiny přerušení se aplikace vrátí kde skončila a pokračuje vyhodnocením podmínky if(flag3) a pak prostě usne ! Usne i když je flag1 nastavená, tedy i když má program za úkol vygenerovat pulzík. Aplikace bude spát do té doby než ji probudí nějaká další událost. V lepším případě tedy zareaguje vygenerováním pulzíku s těžko předvídatelným zpožděním a v horším případě (pokud ji probudíme další vzestupnou hranou na vstupu) dokonce jeden pulz vynechá. Takové chování je naprosto nežádoucí. Takže se rozhodneme "napravit" situaci podmínkou if(!flag1 && !flag2 && !flag3). Pokusíme se čip uspat jedině když jsou všechny vlajky vynulované (tedy aplikace nemá žádnou nevyřízenou událost). Bude to fungovat ?
// Spánek - "Race condition" - ukázka problému (ATTINY416) #define F_CPU 3333333 #include <avr/io.h> #include <avr/sleep.h> #include <avr/interrupt.h> inline void generate_pulse(void); // krátký pulzík pro sledování na osciloskopu volatile uint8_t flag1=0, flag2=0, flag3=0; // vlajky signalizujcí událost 1, událost 2 a událost 3 int main(void){ PORTA.PIN5CTRL = PORT_ISC_RISING_gc; // Externí přerušení vzestupnou hranou na PA5 PORTA.DIRSET = PIN4_bm; // PA4 výstup pro signalizaci "reakce" set_sleep_mode(SLEEP_MODE_IDLE); // volím režim spánku "IDLE" (mělký) sleep_enable(); // povolíme spánek sei(); // globální povolení přerušení while (1) { // reagujeme na události if(flag1){ // pokud se stala událost 1, reagujeme na ni flag1=0; // vyčistíme vlajku generate_pulse(); // užitečná reakce na událost 1 } if(flag2){ flag2=0; asm("nop"); // nějaká užitečná reakce na událost 2 } if(flag3){ flag3=0; asm("nop"); // nějaká užitečná reakce na událost 3 } // proces usínání if(!flag1 && !flag2 && !flag3){ // jdi spát pokud na reakci nečeká žádná událost - bacha tohle není dobře sleep_cpu(); // jdeme spát } } } ISR(PORTA_PORT_vect){ PORTA.INTFLAGS = PORT_INT5_bm; // vyčistíme vlajku externího přerušení flag1=1; // signalizujeme hlavní smyčce že se odehrála událost 1 } inline void generate_pulse(void){ PORTA.OUTSET = PIN4_bm; // vygeneruje pulzík na PA4 asm("nop"); PORTA.OUTCLR = PIN4_bm; }
Vyzkoušíme teď jestli aplikace reaguje správně. Pulzy z generátoru přivedu na vstup našeho mikropočítače a budu sledovat jestli opravdu na každý vstupní impulz zareaguje pulzíkem na výstupu. Na následující animaci (gif) můžete vidět nepříjemný problém. Na první vstupní pulz aplikace zareaguje spolehlivě, ale pokud druhý pulz přijde v nevhodnou chvíli tak se něco stane a aplikace na něj nereaguje. Tohle je chyba která má vysoký "frustrační potenciál". V typických situacích se totiž projeví jen opravdu velmi zřídka (vstupní impulz se musí trefit do velmi úzkého "mrtvého pásma"). Takže se chyba může projevit jednou za den, jednou za týden a nebo jednou za měsíc a vám se ji ani za boha nepodaří vyvolat zrovna když ji chcete pozorovat.
Příčina problému už tu jednou padla. Popisovali jsme co by se stalo kdybychom prostě na konec hlavní smyčky (while(1)) přidali sleep_cpu(). Totéž se děje i v této situaci. Jen je potřeba si uvědomit že se podmínka if(!flag1 && !flag2 && !flag3) přeloží na několik instrukcí a trvá několik strojových cyklů. Během jejich vykonávání opět může přijít přerušení které stav jedné z vlajek změní a aplikace už na to poté nepřijde a usne. Pojďme se podívat na výsledný strojový kód pro mikropočítač, který zmíněnou podmínku realizuje:
if(!flag1 && !flag2 && !flag3){ // jdi spát pokud na reakci nečeká žádná událost - bacha tohle není dobře 00000054 LDS R24,0x3F02 Načti hodnotu flag1 z RAM 00000056 CPSE R24,R1 Zjisti jestli je vlajka nulová 00000057 RJMP PC-0x001D Pokud je nenulová, jdi na začátek hlavní smyčky 00000058 LDS R24,0x3F01 Načti hodnotu flag2 z RAM 0000005A CPSE R24,R1 Zjisti jestli je vlajka nulová 0000005B RJMP PC-0x0021 Pokud je nenulová, jdi na začátek hlavní smyčky 0000005C LDS R24,0x3F00 Načti hodnotu flag3 z RAM 0000005E CPSE R24,R1 Zjisti jestli je vlajka nulová 0000005F RJMP PC-0x0025 Pokud je nenulová, jdi na začátek hlavní smyčky 00000060 SLEEP Jdi spát 00000061 RJMP PC-0x0027 Jdi na začátek hlavní smyčky
Pokud rutina přerušení od sledované události (hrana na PA4) přijde mezi instrukcemi na adresách 0054 až 0060 tak aplikace usne bez ohledu na to, že je flag1 nastavená na jedničku (program totiž operuje s její zastaralou hodnotou). Tento problém je společný pro většinu mikrokontrolérů a jeho řešení se různí podle platformy.
Na mikrokontrolérech AVR se nabízí dvě řešení. Jedno z nich stojí na speciální vlastnosti instrukce sei (globální povolení přerušení). Jakákoli instrukce následující za sei se totiž vždy vykoná bez ohledu na to zda je aktivní nějaké přerušení. Případná rutina přerušení je vyvolána vždy až po vykonání následující instrukce. Jak tuto vlastnost využít vysvětlím na následujícím fragmentu zdrojového kódu.
// proces usínání cli(); // dočasně vypneme přerušení aby se nám "flagy" neměnili pod rukama (chceme provést "atomickou" operaci) if(!flag1 && !flag2 && !flag3){ // zjisti podle stavu vlajek zda můžeme jít spát sei(); // zpět zapneme přerušení - sleep musí následovat hned za touto instrukcí sleep_cpu(); // jdeme bezpečně spát }else{ sei(); // jinak zpět zapneme přerušení }
Program vyhodnocuje stav vlajek s dočasně vypnutým přerušením (pomocí cli). To je vcelku běžný postup v mnoha různých situacích (viz tzv. atomické operace). Pokud je některá z vlajek nastavená, celý proces skončí opětovným povolením přerušení (sei) a čip vůbec nepřejde do režimu spánku. Pokud jsou všechny vlajky vynulované (a čip tedy může jít spát), provede se sekvence dvou instrukcí. Nejprve sei a bezprostředně poté sleep (schovaná v makru sleep_cpu). Pokud by se stalo, že během vyhodnocování podmínky přijde vstupní pulz, aplikace na něj nebude reagovat neboť přerušení jsou dočasně vypnutá. Neobsloužené přerušení bude tedy "čekat" na pozadí. Teprve až poté co program vykoná instrukci sei a následně ještě instrukci sleep a usne. Tak teprve pak se čekající přerušení dostane na řadu a čip okamžitě probudí a umožní mu zareagovat a vygenerovat pulz. Přesně tento postup je zdokumentován i v "oficiální" dokumentaci avr-libc/sleep.h. Výsledné chování demonstruje následující animace. Všimněte si nejprve toho že aplikace už nevynechává pulzy. A poté si můžete všimnout, že pro určitou polohu druhého pulzu aplikace někdy vygeneruje pulzík o pár mikrosekund dřív nebo později. To je dáno právě tím zda stihne rutina přerušení přijít ještě včas před vyhodnocením stavu vlajek. A nebo zda to nestihne a aplikace pak reaguje až se zpožděním odpovídajícím celému vyhodnocení + ještě probuzení ze spánku. Ale to je pouze kosmetická vada, která typicky nečiní problém, neboť k takovému zpoždění dochází běžně vlivem zdržení při obsluze dalších událostí (od ostatních vlajek). Zmiňuji to tedy jen pro zajímavost.
Pokud se vám zdálo předchozí řešení krkolomné, nebo prostě takové křivé. Mohu vám nabídnout přímočařejší. AVRka obsahují bit Sleep Enable. Pokud je bit vynulovaný tak program neusne ani když vykoná instrukci sleep. Někdy se tento bit používá jako bezpečnostní prvek. Těsně před usnutím se nastaví a po probuzení zase vynuluje. Aby se nějakým nedopatřením nemohlo stát, že aplikace omylem vykoná instrukci sleep a usne v okamžiku kdy to program nečeká (ten se pak už ani nemusí probudit, pokud nebude nastavena periferie která by čip probudila). Na druhou stranu pravděpodobnost, že se to stane je asi velmi nízká. Domnívám se tedy, že původní zamýšlený význam tohoto bitu je právě k řešení našeho problému. Necháme naši aplikaci aby atomicky zkontrolovala stav flagů a pokud jsou všechny nulové tak nastaví SE bit a povolí tím spánek. Pak aplikace projde sérií podmínek kterými se testují flagy a případně na ně reaguje. A poté prostě vykoná instrukci sleep, tedy pokusí se usnout (a usne jen pokud byl spánek povolen SE bitem). S každým nastavením jakéhokoli flagu SE bit nulujeme (zakazujeme spánek). Díky tomu je nemožné aby aplikace usnula ve stavu kdy je některý flag s hodnotou jedna a čeká na obsloužení. V takovém případě prostě instrukce sleep neudělá nic a program pokračuje a provede znovu test všech flagů...
int main(void){ PORTA.PIN5CTRL = PORT_ISC_RISING_gc; // Externí přerušení vzestupnou hranou na PA5 PORTA.DIRSET = PIN4_bm; // PA4 výstup pro signalizaci "reakce" set_sleep_mode(SLEEP_MODE_IDLE); // volím režim spánku "IDLE" (mělký) sei(); // globální povolení přerušení while (1) { // zjišťujeme zda smíme či nesmíme usnout (atomicky) cli(); if(!flag1 && !flag2 && !flag3){ sleep_enable(); // pokud smíme usnout, povolíme spánek nastavením SE bitu } sei(); // reagujeme na události if(flag1){ // pokud se stala údálost 1, reagujeme na ni flag1=0; // vyčistíme vlajku generate_pulse(); // užitečná reakce na událost 1 } if(flag2){ flag2=0; asm("nop"); // nějaká užitečná reakce na událost 2 } if(flag3){ flag3=0; asm("nop"); // nějaká užitečná reakce na událost 3 } // proces usínání - zkusíme usnout (čip neusne pokud není spánek povolen) sleep_cpu(); } } ISR(PORTA_PORT_vect){ PORTA.INTFLAGS = PORT_INT5_bm; // vyčistíme vlajku externího přerušení sleep_disable(); // zakáže spánek flag1=1; // signalizujeme hlavní smyčce že se odehrála událost 1 }
Pokud někdo používáte ještě jiný způsob jak zajistit bezpečné usnutí / neusnutí, pošlete mi ho i s komentářem na mail a já ho rád zveřejním.
Home
| V1.01 29.8.2021 (edited 17.7.2022)/
| By Michal Dudka (m.dudka@seznam.cz) /