Struktura
Struktura programu je jedna ze základních věcí, které se při návrhu embedded aplikace řeší. Nejde jen o pořádek pro pořádek. Dobrá struktura pomáhá rychle poznat, kde se nastavuje hardware, kde běží hlavní logika a kde jsou schované detaily konkrétních periferií.
U malého programu se dá všechno napsat do jednoho souboru. Chvíli to funguje. Jakmile ale přibude UART, senzor, tlačítko, časovač nebo přerušení, začne být důležité, aby měl kód jasné části. Jinak se z programu stane klubko vodičů, ve kterém se dá něco opravit jen omylem.
Proto se u jednoduchých embedded programů často odděluje:
- inicializace hardwaru,
- zapnutí přerušení,
- hlavní smyčka programu,
- samostatné moduly pro jednotlivé části zařízení.
Tahle kapitola popisuje obecně uznávaný a často používaný postup pro jednodušší bare-metal programy, tedy programy bez operačního systému. Je dobrý jako výchozí styl, protože je přehledný a snadno se v něm hledají chyby.
Neznamená to ale, že embedded program nejde napsat jinak. Když se použije RTOS, struktura programu bývá jiná: místo jedné hlavní smyčky se často vytvoří více úloh a jejich spouštění řídí plánovač.
Funkce main
V jazyce C bývá hlavním vstupem programu funkce main.
U mikrokontroléru obvykle nevypadá jako program,
který něco udělá a skončí.
Mikrokontrolér má po zapnutí běžet pořád.
Typická struktura může vypadat takto:
int main(void)
{
clock_init();
gpio_init();
uart_init();
sensors_init();
sei();
while (1) {
communication_task();
sensors_task();
led_task();
}
}
Všimněte si tří částí:
- inicializační funkce připraví hardware,
sei()zapne globální přerušení,while (1)udržuje program při životě.
Názvy funkcí nejsou povinné.
Důležitý je nápad:
main má být přehledná mapa programu,
ne skládka všeho kódu v jednom souboru.
Inicializace
Inicializační funkce nastaví hardware do známého stavu. Například:
clock_init()nastaví hodiny,gpio_init()nastaví GPIO,uart_init()nastaví komunikaci,sensors_init()připraví senzory,pinmux_init()nastaví pinmux, pokud je potřeba.
Po resetu čipu není dobré spoléhat na pocit, že "to asi nějak je". Program by měl důležité periferie nastavit výslovně. Když má být pin výstup, nastavíme výstup. Když má UART používat konkrétní piny, nastavíme trasu. Když má běžet časovač, nastavíme jeho režim.
Kdy zapnout přerušení
Přerušení se většinou zapínají až po inicializaci. Nejdřív připravíme proměnné, buffery, periferie a jejich registry. Teprve potom dovolíme hardwaru, aby program začal vyrušovat.
clock_init();
uart_init();
buffers_init();
sei();
Kdyby přerušení přišlo dřív, mohlo by sáhnout na data, která ještě nejsou připravená. To je přesně ten typ chyby, který se tváří jako náhoda, ale ve skutečnosti jsme jen pustili startovní výstřel moc brzo.
Hlavní smyčka
Hlavní smyčka bývá nekonečný cyklus:
while (1) {
buttons_task();
communication_task();
sensors_task();
}
Jednotlivé funkce uvnitř by měly být krátké. Neměly by zbytečně dlouho čekat. Když jedna část programu zabere moc času, ostatní se dostanou na řadu pozdě.
To je rozdíl proti jednoduchému ukázkovému programu, kde si někdy můžeme dovolit napsat:
while (1) {
led_on();
delay_ms(500);
led_off();
delay_ms(500);
}
Na blikání LED je to v pořádku. V programu, který má zároveň číst senzory, přijímat zprávy a reagovat na tlačítka, už dlouhé čekání začne překážet.
Rozdělení do modulů
Jakmile program roste,
nemá být všechno v main.c.
Lepší je rozdělit kód podle odpovědnosti.
Například:
src/
main.c
clock.c
clock.h
gpio.c
gpio.h
uart.c
uart.h
sensors.c
sensors.h
Soubor .c obsahuje implementaci:
tedy skutečný kód funkcí.
Soubor .h obsahuje rozhraní:
tedy co smí používat ostatní části programu.
Například uart.h může říkat:
void uart_init(void);
void uart_send_byte(uint8_t byte);
bool uart_read_byte(uint8_t *byte);
A uart.c obsahuje,
jak přesně se to provede přes registry.
Ostatní soubory tak nemusí vědět,
který bit v kterém registru zapíná příjem.
Použijí funkci uart_init()
a detail zůstane uvnitř modulu.
Hlavičkové soubory
Hlavičkový soubor není odkladiště všeho, co se nevešlo jinam. Má popisovat veřejné rozhraní modulu.
Do .h typicky patří:
- deklarace funkcí,
- veřejné datové typy,
- konstanty potřebné zvenku.
Do .h většinou nepatří:
- pomocné privátní funkce,
- lokální proměnné modulu,
- detaily registrů,
které používá jen jeden
.csoubor.
Když celý program přímo sahá do registrů UARTu,
změna UARTu bolí všude.
Když registry řeší jen uart.c,
zbytek programu používá čistší funkce jako uart_send_byte().
Co patří kam
| část programu | typický obsah |
|---|---|
main.c | pořadí inicializace, zapnutí přerušení, hlavní smyčka |
clock.c | nastavení hodin |
gpio.c | nastavení pinů, LED, tlačítek |
uart.c | sériová komunikace |
sensors.c | měření a zpracování hodnot ze senzorů |
.h soubory | rozhraní modulů pro zbytek programu |
Podrobnosti, jak se nastavují jednotlivé bity v registrech, jsou v kapitole Ovládání registrů. Tahle kapitola řeší hlavně to, kam takový kód v programu patří.