Až na vzácné výjimky budete ve většině praktických aplikací chtít aby mikrokontrolér prováděl několik "činností" nezávisle na sobě (řekněme "zároveň"). Například aby kontroloval stav baterie, obsluhoval displej, hlídal čas, sledoval uživatelské vstupy, řídil otáčky motoru, sbíral data ze snímačů a podobně. Jenže mikrokontrolér může v jednom okamžiku vykonávat jen jednu instrukci, jinak řečeno on vlastně neumí dělat více věcí zároveň. Je na tom podobně jako člověk, vy také nezvládnete zároveň umývat nádobí, sekat trávu, číst si knihu a hrát u toho tenis. Což ale neznamená že to nějak nejde. V mikrokontroléru jsou periferie které umí pracovat samostatně. Například časovač umí naprosto sám generovat různé průběhy potřebné například k řízení či měření otáček motoru. AD převodník umí sám měřit napětí a vyhodnocovat jeho velikost. Obvod reálného času umí sám tikat a skládat sekundy do minut, hodin, dnů, měsíců a roků. Některé specifické úlohy může tedy mikrokontrolér naložit na bedra periferií a ony se o ně postarají samy. Periferie pak pracují vcelku nezávisle a opravdu dělají více věcí zároveň. Jenže periferie ani zdaleka nezvládnou všechno - většinou umí pouze triviální (ale velmi užitečné) úlohy. Ke zvládnutí ostatních činností většinou využíváme toho že je mikrokontrolér velmi rychlý. Tedy necháme ho milisekundu pracovat na jedné věci, milisekundu pak na další věci atd. Navenek se pak zdá že se vše odehrává zároveň. A my se teď budeme učit jak psát programy právě takovým způsobem.
Jedna hojně rozšířená technika (cooperative multitasking) využívá běžícího časovače, který počítá milisekundy od startu mikrokontroléru. Váš program si může čas kdykoli zjistit a využívá toho aby plánoval kdy se má která úloha vykonat. My si teď na několika příkladech ukážeme jak se to dělá. Funkci milis jsem pro vás připravil a o tom jak je vytvořená se pobavíme někdy později. Je součástí našeho vzorového projektu a postup jeho importu si můžete přečíst v jednom z prvních dílů tutoriálu. Všimněte si že ve vzorovém projektu je hned ze začátku programu volána funkce init_milis(). Ta spustí časovač TIM4 kerý každou milisekundu volá přerušení a inkrementuje počítadlo času (16bit proměnnou nesoucí informaci o počtu milisekund). Funkce milis() vám pak vrátí vždy aktuální hodnotu tohoto "počítadla". Jak brzy uvidíte, bude to velmi užitečný pomocník.
Začneme triviální aplikací, která má jen jeden smysluplný úkol - blikat LEDkou (na Nucleo kitu na PC5). Naše strategie bude následující. Připravíme si funkci process_led(), která se bude starat jen o blikání a nebude obsahovat žádné "delay" funkce a bude velmi rychlá. Musíme ji napsat tak aby "neblokovala" program na delší dobu (tedy aby trvala ideálně řádově desítky až stovky us). Nesmí tedy obsahovat žádné "delay" funkce, ani jiná různá blokující čekání. Tuto funkci budeme stále dokola volat v hlavní smyčce (while(1)). Proč to děláme zrovna takhle uvidíte v dalších příkladech. Celý program bude vypadat třeba nějak tahle:
// Blikáme neblokujícím způsobem #include "stm8s.h" #include "milis.h" #include "stdio.h" //#include "spse_stm8.h" //#include "stm8_hd44780.h" const uint16_t blink_period=500/2; // perioda blikání LED1 500ms (250ms rozsvíceno, 250ms zhasnuto) void process_led(void); void main(void){ CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1); // taktovat MCU na 16MHz GPIO_Init(GPIOC,GPIO_PIN_5,GPIO_MODE_OUT_PP_LOW_SLOW); // PC5 jako výstup pro LED init_milis(); // spustit časovač millis while (1){ process_led(); // obsluž proces blikání LEDkou // obsluhuj další procesy - lteré tu teď zrovna nemáme } } // obsluhuje blikání LEDkou (pracuje rychle a většinou nic nedělá a hned skončí) void process_led(void){ static uint16_t last_time=0; // tady si budeme pamatovat čas kdy jsme naposledy přepnuli stav LEDky // milis() - last_time = počet milisekund od minulého přepnutí LEDky // a ten porovnáme s požadovanou periodou blikání, pokud je to víc, je čas přepnout LEDku if(milis() - last_time >= blink_period){ last_time = milis(); // uložíme si čas abychom věděli kdy přepnout LED příště GPIO_WriteReverse(GPIOC,GPIO_PIN_5); // přepneme stav LEDky } }
Chování naší funkce process_led() je docela jednoduché. V těle funkce si deklarujeme proměnnou last_time ve které si udržujeme informaci o čase kdy jsme naposledy přepnuli stav LEDky. Díky němu si vždy můžeme snadno dopočítat kdy máme její stav přepnout příště. Tato proměnná má paměťovou třídu static což znamená že její obsah zůstává zachován i po skončení funkce (je to tedy globální proměnná, ale není viditelná jinde než v těle funkce). V podmínce máme výraz (milis()–last_time). Ten spočítá rozdíl mezi aktuálním časem a posledním časem kdy jsme přepnuli stav LEDky a porovnává ho s konstantou blink_period. Tedy jen testuje jestli od posledního přepnutí LEDky uplynulo alespoň blink_period (tedy zde 250) milisekund. Pokud ještě neuplynulo, není potřeba s LEDkou nic dělat a funkce končí. Pokud čas uplynul, funkce přepne stav LEDky příkazem GPIO_WriteReverse a uloží si aktuální čas do last_time aby věděla kdy přepnout LED příště.
Cvičení 1: Napište program, který rozbliká tři LEDky. Jedna bude blikat s frekvencí 0.5Hz, druhá s frekvencí 1.5Hz a třetí s frekvencí 4Hz. LEDky si připojte na libovolné piny (např PG5,PG6,PG7).
Řešení:
Stačí rozšířit předchozí ukázku. Protože řešíme tři nezávislé procesy, připravíme si tři funkce pro blikání LEDkou a všechny tři voláme stále dokola v hlavní smyčce.
// Blikáme trojicí LEDek neblokujícím způsobem #include "stm8s.h" #include "milis.h" #include "stdio.h" //#include "spse_stm8.h" //#include "stm8_hd44780.h" // makra definující piny LEDek #define LEDA_GPIO GPIOG #define LEDA_PIN GPIO_PIN_7 #define LEDB_GPIO GPIOG #define LEDB_PIN GPIO_PIN_6 #define LEDC_GPIO GPIOC #define LEDC_PIN GPIO_PIN_5 // makra pro přepnutí stavu LEDek (jen pro zlepšení čitelnosti programu) #define LEDA_TGL GPIO_WriteReverse(LEDA_GPIO,LEDA_PIN) #define LEDB_TGL GPIO_WriteReverse(LEDB_GPIO,LEDB_PIN) #define LEDC_TGL GPIO_WriteReverse(LEDC_GPIO,LEDC_PIN) // periody blikání LEDek const uint32_t leda_period=2000/2; // perioda blikání LED1 2s (1s rozsvíceno, 1s zhasnuto) const uint32_t ledb_period=666/2; // perioda blikání LED1 666ms (333ms rozsvíceno, 333ms zhasnuto) const uint32_t ledc_period=250/2; // perioda blikání LED1 250ms (125ms rozsvíceno, 125ms zhasnuto) void process_leda(void); void process_ledb(void); void process_ledc(void); void main(void){ CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1); // taktujeme na 16MHz GPIO_Init(LEDA_GPIO,LEDA_PIN,GPIO_MODE_OUT_PP_LOW_SLOW); // inicializace výstupů pro LED GPIO_Init(LEDB_GPIO,LEDB_PIN,GPIO_MODE_OUT_PP_LOW_SLOW); GPIO_Init(LEDC_GPIO,LEDC_PIN,GPIO_MODE_OUT_PP_LOW_SLOW); init_milis(); // spouštíme millis časovač while (1){ process_leda(); // obsluž blikání LEDky A process_ledb(); // obsluž blikání LEDky B process_ledc(); // obsluž blikání LEDky C } } // funkce obsluhující blikání LEDky A void process_leda(void){ static uint16_t last_time=0; // zde si ukládáme poslední čas přepnutí LED // pokud uplynul čas "leda_period" přepneme stav LEDky if(milis() - last_time >= leda_period){ last_time = milis(); // uložíme si aktuální čas abychom věděli kdy přepnout příště LEDA_TGL; } } // funkce obsluhující blikání LEDky B void process_ledb(void){ static uint16_t last_time=0; if(milis() - last_time >= ledb_period){ last_time = milis(); LEDB_TGL; } } // funkce obsluhující blikání LEDky C void process_ledc(void){ static uint16_t last_time=0; if(milis() - last_time >= ledc_period){ last_time = milis(); LEDC_TGL; } }
Zřídka a v opravdu velmi jednoduchých aplikacích najde uplatnění blokující delay funkce. Vy už ji máte pod názvem delay_ms() připravenou v knihovně milis.h a už jste ji používali. Ta je ve skutečnosti také založena na funkci milis(). Není nijak zvlášť přesná (+- 1ms) a slouží výhradně k jednoduchým pokusům. V praxi se k ničemu jinému nepoužívá.
Jak už jsme napsali, funkce milis vrací 16bit číslo (tedy hodnotu v rozsahu 0 až 65535) odpovídající času v milisekundách. To ale znamená, že po uplynutí 65.535 sekundy hodnota "počítadla" přeteče (začne počítat znovu od nuly). Zdá se to jako nepříjemný problém, ale ve skutečnosti není. Všimněte si, že v předchozích příkladech jsme vždy počítali rozdíl hodnoty kterou vrátila funkce milis() oproti předešlé hodnotě. Tento způsob výpočtu se s přetečením milis umí vyrovnat. Každopádně nemůžeme "realizovat" časy delší než oněch 65535ms. Něco takového ale v praxi potřebujete jen velmi výjimečně. Mějte tedy na paměti, že naši funkci milis() můžeme používat k odměřování časů kratších jak 65s. Změnou definice v souboru milis.h lze funkci přepnout do režimu 32bit čísel (maximální čas který pak lze přímo odměřovat je přibližně 49 dní). V takovém případě ale všechny proměnné které pracují s hodnotou funkce milis() musíme definovat jako uint32_t.
Home
| V2.00 20.10.2020 /
| By Michal Dudka (m.dudka@seznam.cz) /