Přeskočit na hlavní obsah

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í.
Běžný postup, ne jediný zákon vesmíru

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í:

  1. inicializační funkce připraví hardware,
  2. sei() zapne globální přerušení,
  3. 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 .c soubor.
Držte detaily uvnitř modulu

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 programutypický obsah
main.cpořadí inicializace, zapnutí přerušení, hlavní smyčka
clock.cnastavení hodin
gpio.cnastavení pinů, LED, tlačítek
uart.csériová komunikace
sensors.cměření a zpracování hodnot ze senzorů
.h souboryrozhraní 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ří.