Pojďme programovat elektroniku

Programování elektroniky: Arduino, multitasking a práce s více jádry CPU

  • Jak v programu pracovat s různými úlohami
  • Od primitivního blokovacího přístupu k hardwarovému čítači
  • A jako bonus multitasking na dvou jádrech čipu ESP32

V dnešním pokračování našeho seriálu o programování elektroniky si ukážeme základní principy spouštění a plánování úloh počínaje primitivním blokovacím přístupem pomocí funkce delay a konče využitím mikrokontrolerů s více jádry CPU. To vše jednoduše a v prostředí Arduino.

Přesuňte se na konkrétní sekci v článku:

Kód, který blokuje

Každý kutil, který programuje v Arduinu, dobře ví, že periodické úlohy můžeme vložit do funkce loop. Její obsah se opakuje stále dokola a tak rychle, jak to jen dovolí výpočetní výkon našeho čipu.

Nejjednodušší kód, který v každém průběhu smyčky na 500 ms rozsvítí vestavěnu LED třeba na desce Arduino Uno a dalších 1 000 ms počká, může vypadat třeba takto:

// Funkce setup se spusti jako prvni
void setup() {
  // Nastav pin s vestavenou LED na zapis
  pinMode(LED_BUILTIN, OUTPUT);
}

// Smycka loop se opakuje stale dokola
void loop() {
  digitalWrite(LED_BUILTIN, HIGH); // Nastav na pinu s vestavenou LED pracovni napeti
  delay(500); // Pockej 500 ms
  digitalWrite(LED_BUILTIN, LOW); // Nastav na pinu s vestavenou LED napeti 0V
  delay(1000); // Pockej 1 000 ms
}

Má to jeden háček. Náš kód bude kvůli použití delay blokovat celou funkci loop, která po tu dobu nebude moci provádět nic dalšího. Zkušenější začátečníci přitom už dobře vědí, že je v takových případech mnohem vhodnější neblokující přístup (non-blocking).

Namísto blokování budeme kontrolovat čas

Namísto toho, abychom na celou sekundu zastavili průběh funkce, se v ní budeme naopak stále dokola ptát, jestli už uplynul dostatečný čas od posledního rozsvícení LED:

uint32_t prodleva = 1000; // Prodleva v ms
uint32_t ms = 0; // Cas posledniho rozsvíceni LED

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  // Pokud od posledniho rozsviceni LED uplynulo vice milisekund,
  // nez je hodnota ulozena v promenne prodleva
  // Funkce millis vraci pocet ms od startu cipu
  if(millis() - ms > prodleva){
    digitalWrite(LED_BUILTIN, HIGH);
    delay(500);
    digitalWrite(LED_BUILTIN, LOW);
    // Aktualizuj cas posledniho rozsviceni
    ms = millis();
  }
}

Zkrátili jsme tedy blokování funkce o celou sekundu a pozastavujeme ji pouze na 500 ms, když rozsvítíme a poté zhasneme LED. i tuto rutinu bychom ale mohli upravit podobným způsobem pro neblokující průběh funkce.

Synchronní správce úloh

Dejme tomu, že bychom si chtěli neblokující úryvek kódu výše přepsat do naprosto primitivního správce periodických úloh. Tedy úloh, které se budou spouštět ve svůj stanovený čas a zpracují třeba obsah nějaké funkce. Vytvoříme si tedy naprosto primitivní synchronní multitasking, kdy se úlohy v seznamu zpracovávají vždy jedna po druhé.

90ac7258-54ba-4b97-be48-5113c1a34f47
Synchronní správce se dvěma úlohami. Každá z nich s různou frekvencí (1 a 3 sekundy) vypisuje zprávu do sériové linky 

Kód níže zarezervuje v RAM paměť pro 10 úloh, přičemž my použijeme dvě. První úloha se bude spouštět na čipu každou sekundu a bude volat funkci prvniFunkce. Druhá úloha se bude spouštět každou třetí sekundu a bude volat funkci druhaFunkce:

#define MAX_ULOH 10 // Nas spravce uloh ma pevnou pamet na 10 uloh

// Struktura Uloha s popisem ulohy
typedef struct {
  uint32_t prodleva; // Prodleva ulohy v ms
  uint32_t ms; // Cas posledniho zpracovani
  bool povoleno; // Povoleni/zakazani ulohy
  void (*funkce)(void); // Ukazatel na funkci, ktera se ma zpracovat. Funkce nema zadny parametr a nic nevraci
} Uloha;

// Pole uloh
Uloha ulohy[MAX_ULOH];

// Funkce, kterou bude spoustet prvni uloha
void prvniFunkce() {
  Serial.println("Bobik s Pindou vari gulas");
}

// Funkce, kterou bude spoustet druha uloha
void druhaFunkce() {
  Serial.println("Fifinka s Myspulinem montuji termonuklearni bombu");
}

void setup() {
  Serial.begin(115200);

  // Nastaveni prvni ulohy, ktera se bude spoustet kazdou sekundu
  ulohy[0].ms = 0;
  ulohy[0].povoleno = true;
  ulohy[0].prodleva = 1000;
  ulohy[0].funkce = prvniFunkce; // Do ukazatele na funkci ulozime adresu funkce prvniFunkce

  // Nastaveni druhe ulohy, ktera se bude spoustet kazde 3 sekundy
  ulohy[1].ms = 0;
  ulohy[1].povoleno = true;
  ulohy[1].prodleva = 3000;
  ulohy[1].funkce = druhaFunkce; // Do ukazatele na funkci ulozime adresu funkce druhaFunkce

}

void loop() {
  // Projdeme seznam uloh a zkontrolujeme,
  // jestli uz nastal cas na jejich zpracovani
  for (int i = 0; i < MAX_ULOH; i++) {
    if (ulohy[i].povoleno && millis() - ulohy[i].ms > ulohy[i].prodleva) {
      ulohy[i].ms = millis();
      (ulohy[i].funkce)();
    }
  }
}

Co se stane po spuštění čipu? Funkce prvniFunkce vypisuje do sériového terminálu zprávu „Bobik s Pindou vari gulas" a druhaFunkce pak „Fifinka s Myspulinem montuji termonuklearni bombu.“

Každá z úloh má i vlastnost povoleno typu bool, takže úlohu můžeme v kódu libovolně zakazovat a zase povolovat podle potřeby. 

No dobrá, náš správce úloh sice čip téměř neblokuje, protože jeho veškerý management ve smyčce loop zabere pokaždé nejvýše pár mikrosekund, ale co ty cílové funkce, které volá?

Pokračování článku patří k prémiovému obsahu pro předplatitele

Chci Premium a Živě.cz bez reklam Od 41 Kč měsíčně
Diskuze (5) Další článek: Vozidla Tesla již nebudou disponovat ultrazvukovými senzory

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , , ,