V tomto článku se zaměřím na používané techniky programování embedded systémů. Jako hlavní programovací jazyk budu používat jazyk „C“. Dnešní překladače tohoto jazyka jsou natolik kvalitní, že jimi vygenerovaný kód se svojí efektivitou blíží programům napsaným v assembleru. Kromě toho použití vyššího programovacího jazyka umožňuje programátorovi více se soustředit na algoritmy než na kódování či používání programátorských triků na úrovni assembleru. Moje zkušenost hovoří, že mnohdy lze lepším algoritmem dosáhnout větší efektivity i rychlosti programů. Jako příklad bych uvedl algoritmy pro vyhledávání. Mnoho programátorů v assembleru použije lineární vyhledávací algoritmus, bojíce se složitosti kódování. Programátor v jazyce „C“ lehce napíše a odladí například algoritmus binárního vyhledávání, který bude na deseti řádcích.
Při použití jazyka „C“ při programování embedded systémů číhají na programátora jisté záludnosti. Obzvláště pokud předtím programoval v jazyce „C“ v prostředí klasických operačních systémů, jako jsou například unixové systémy (včetně Linuxu) nebo Windows.
Běžným opomenutím je použití inicializovaných dat bez klíčového slova const. Toto klíčové slovo uvádí, že daná data nemění svůj obsah za běhu programu. Pokud toto klíčové slovo nepoužijeme, daná proměnná zbytečně zabírá vzácnou paměť RAM. Pokud slovo const použijeme, jsou data i za běhu programu uložena v paměti ROM nebo FLASH. Příklad je uveden na obr. 1.
Obr. 1 Příklady použití konstrukcí jazyka „C“
Dalším problémem je skutečnost, že programátor embedded systémů často píše i funkce pro obsluhu přerušení. Pak často hlavní program i funkce přerušení sdílí stejnou proměnnou a pro takové proměnné existuje klíčové slovo volatile. To označuje proměnnou, která může měnit svůj obsah mimo kontrolu překladače. Optimalizující překladač pak předpokládá tuto možnou změnu a nedojde k takovému generování kódu, který může zapříčinit z hlediska programátora chybné chování programu. Stejná technika se týká i například přístupu do registrů různých periferních obvodů.
Programátor by měl mít rovněž na zřeteli obvykle malou kapacitu paměti RAM při realizaci například rekurzivních algoritmů nebo používání příliš velkých lokálních proměnných. Příklady použití výše uvedených technik jsou v kódu na obr. 1.
Paměťová mapa programu je rozložení programů a dat v adresovém prostoru počítače. Program je tvořen jedním nebo více segmenty (někdy nazývanými jako sekce). Segment je kolekce dat nebo instrukcí pro procesor. Obvykle má jméno a další atributy.
Běžný překladač jazyka „C“ vytváří 3 segmenty pojmenované
– .text
– .data
– .bss
Segment .text jsou instrukce programu a konstantní data. Segment .data jsou inicializované proměnné programu. Segment .bss jsou neinicializované proměnné programu. Obr. 2 ukazuje příklad takové paměťové mapy. Důležité je všimnout si, že paměťové mapy jsou dvě. Jedna je vytvořena pro uložení programu a inicializovaných dat do ROM/FLASH paměti a druhá je paměťová mapa za běhu programu. Úkolem startovacího kódu je přemístit inicializovaná data z paměti ROM do paměti RAM na správné adresy. Součástí překladačů jazyka „C“ bývají odpovídající startovací funkce, které jsou volány dříve, než je spuštěna funkce main(), takže se o to programátor ve většině případů nemusí starat. Je dobré si ale tyto mechanismy uvědomovat, případně je možné napsat i vlastní startovací kód.
Obr. 2 Příklad paměťové mapy programu
Mnoho algoritmů lze převést na programový model tzv. Konečného automatu. Konečný automat je abstraktní výpočetní model, založený na teorii automatů. Skládá se z:
Aktuální stav automatu odráží pomyslné místo v algoritmu. Vznik události pak generuje nějakou akci a případný přechod automatu do jiného stavu. Tento typ programování se někdy nazývá událostí řízený algoritmus (Event Driven Algorithm).
Konečný automat bývá popsán buď orientovaným grafem, nebo tzv. přechodovou tabulkou. Orientovaný graf obsahuje stavy automatu, obvykle prezentované elipsou, a přechodem. Ten je reprezentován šipkou mezi dvěma stavy. Dále je tento přechod označen textem, který definuje událost, na základě které se přechod děje a za lomítkem pak akcí (funkcí), která se při přechodu zavolá.
Přechodová tabulka je dvojrozměrná matice, kde v jedné ose jsou všechny stavy a v druhé všechny události navrhovaného automatu. Jednotlivá pole pak definují přechodovou funkci a nový stav, do kterého má automat přejít.
Jako příklad jsem zvolil implementaci algoritmu pro příjem paketu protokolu SLIP. Automat obsahuje 4 stavy a 5 událostí. Každou událostí je příchod jednoho znaku ze sériové linky. Symboly END, ESC, ESC_ESC a ESC_END indikují konkrétní znaky. Symbol OTHER pak znamená jakýkoliv jiný znak kromě vyjmenovaných znaků. Cílem algoritmu je příjem jednoho paketu SLIP protokolu. Orientovaný graf i tabulka přechodů je uvedena na obr. 3.
Obr. 3 Konečný automat pro příjem paketu SLIP
Mezi hlavní výhody takového návrhu algoritmu patří jednoduchost realizace algoritmu automatu a možnost automatického generování kódu pomocí nástrojů. V neposlední řadě je to skutečnost, že programátor je „donucen“ k promyšlení všech variant stavů a událostí, které mohou nastat. Navíc takto realizovaný automat je při dobré implementaci velmi rychlý.
Pro podporu této programovací techniky existují nástroje, jako je například visualState firmy IAR nebo Telelogic Tau firmy Telelogic.
Toto je asi nejběžnější postup tvorby programů pro velmi malé systémy. Je dobře pochopitelný, snadno implementovatelný, nenáročný na zdroje počítače, hlavně velikost jejich operační paměti. Rovněž lze dobře implementovat různé techniky uspávání procesoru a tak vyjít vstříc požadavkům na bateriový provoz aplikace.
Systém pracuje tak, že část programu pracuje v obslužných procedurách přerušení, což je označováno jako background task a zbytek kódu je pak napsán v jedné velké smyčce, která běží jako normální program mimo obsluhu přerušení, což se nazývá foreground Task. Zjednodušený vývojový diagram je uveden na obr. 4.
Obr. 4 Algoritmus techniky „Foreground/background task“
Často je potřeba, aby foreground task volala konkrétní funkce na základě aktivity některého z přerušení. Nejčastější realizace je taková, že v proceduře přerušení je nastaven nějaký bit globální proměnné, a ten je pak testován v hlavní programové smyčce.
Typickým příkladem může být indikace, že došlo k přerušení od časovače, kdy se v obsluze přerušení jen nastaví jeden bit globální proměnné, například flags, a tento bit se pak testuje a v případě nastavení je volána nějaká funkce pro periodické činnosti. Jak již bylo výše uvedeno, taková proměnná musí být označena klíčovým slovem volatile, aby nedošlo během optimalizace překladače ke špatnému generování kódu.
Obr. 5 Příklad robustního kódu pro techniku „Foreground/background task“
Hlavní smyčka programu by pak měla vypadat tak, jak je uvedeno na obr. 5. Doporučuji si povšimnout zákazu přerušení při operaci s globální proměnnou flags a prací s její kopií localFlags.
Poměrně často jsem se za své praxe setkal s odmítáním použití operačních systémů v embedded systémech. Buď většina programátorů píše programy jako jednu velkou smyčku, ve které se programátor snaží řešit vše, nebo na relativně jednoduché úkoly doporučí použít silný procesor s velkou operační pamětí a implementuje Linux nebo Windows CE. Málo programátorů používá operační systémy pro mikrokontroléry, i když jsou zastoupeny v poměrně velkém počtu a jsou dostupné. Namátkou bych jmenoval například FreeRTOS, eCos, μC/OS-III firmy Micrium, SEGGER RTOS nebo CMX.
Použití operačního systému vede k lepší strukturovanosti a lepší údržbě programu. Na návrh systému pomocí víceúlohového operačního systému se dá nahlížet jako na programovací techniku. Tak jako existuje technika návrhu algoritmu pomocí top-down nebo bottom-up metody nebo použitím objektově orientovaných technik, stejně tak lze provést dekompozici algoritmu programu na spolupracující entity (jednotlivé úlohy). Pak je nutné definovat jejich roli a množinu zpráv, kterou si tyto úlohy budou vyměňovat. A právě k implementaci takové techniky je nutný víceúlohový operační systém.
V tomto článku jsem nastínil několik základních technik pro použití jazyka „C“ při programovaní embedded systémů. Rovněž jsem zmínil některé obecné techniky návrhu programu. Tyto techniky nejsou platné jen při programování embedded systémů, ale rovněž i v prostředí tzv. „velkých“ operačních systémů. Velmi vhodná je obzvláště aplikace teorie automatů. Rovněž je žádoucí u víceúlohových operačních systémů zvážit dekompozici problému na několik samostatných úloh, vzájemně spolupracujících. Mým cílem bylo dosáhnout toho, aby programátor embedded systému zvážil použití více formálních metod při návrhu programu. Vede to k lepšímu a mnohdy rychlejšímu kódu, který je lépe udržovatelný, rozšiřitelný a lépe se odlaďuje. V příštím článku se budu věnovat návrhu konkrétního operačního systému pro mikrokontroléry, vhodného i pro velmi malé systémy.