Fyzikální kabinet FyzKAB

Watchdog na Arduinu – když se program sekne…

Asi každý bastlíř zná tu situaci dobře. Postavíme si vlastní projekt na modulu Arduino. Je vlastně jedno, jestli jde o automatické zalévání, teploměr do skleníku nebo nějaké jiné zařízení do naší chytré domácnosti. V rámci závěrečného testování si s tím vším ještě chvíli pohrajeme – vyladíme detaily, zkoušíme různé situace, pozorujeme chování… A ono to nakonec funguje skvěle – zkrátka přesně tak, jak má. Tak zařízení namontujeme tam, kam patří, a necháme ho běžet.

A pak přijde ten moment. Ne hned… Ne při testování, ale až v reálném provozu. Za pár hodin. Možná za den. Možná ještě později. Zkrátka zjistíme, že tady něco „nehraje“.

Displej možná stále svítí. LEDka pořád bliká. Ale jinak?

Nic. Arduino nereaguje. Nečte senzory. Neodesílá data na zvolený server… Nedělá to, kvůli čemu jsme si to celé stavěli.

Zkusíme reset. A najednou všechno zase funguje. (Aspoň na chvíli!)

Začneme pátrat – a rychle narazíme na pár nepříjemných zjištění. Příčin může být hned několik a je jen velmi těžké předvídat, co je právě tím problém, který nás aktuálně trápí. Vždyť při testování na stole všechno fungovalo bez problémů! Jak má člověk poznat, co je skutečná příčina?

A těch příčin může být celá řada, například:

  • komunikace se senzorem se „kousne“ a program čeká donekonečna,
  • čekáme na událost, která ve skutečnosti nikdy nepřijde,
  • po nějaké době přetekl interní časovač modulu a nyní je najednou číselná hodnota „budoucnosti“ nižší než aktuální okamžik,
  • nebo se projeví jiná drobná chyba v logice – ale jen za velmi specifických podmínek.

Při programování mikrokontroléru si zkrátka musíme uvědomit, že se pohybujeme na trochu jiném hřišti než při tvorbě programu pro klasický počítač. Mikrokontroléry, jako je Arduino, totiž fungují poměrně specificky. Na první pohled to tak možná nevypadá, ale ve skutečnosti jsme o několik vrstev níže.

Je třeba si uvědomit, že nyní pod sebou už moc nemáme k dispozici nějakou „záchrannou síť“ – tedy, že zde neexistují některé mechanismy, na které jsme zvyklí z PC:

  • žádný operační systém,
  • žádná vestavěná ochrana proti chybám,
  • žádný „správce úloh“, který by hlídal běh programu,
  • žádné automatické ukončení programu, když se něco pokazí.

Můžeme se spolehnout na jedno jediné pravidlo: Jakmile se program dostane do špatného stavu… už se z něj sám nedostane!

Program se zasekne, zůstane viset – bez varování a občas i bez chybové hlášky. Takže to na první pohled může klidně vypadat, že „něco běží“… ale ve skutečnosti je program de facto mrtvý.

Nechceme se teď pouštět do toho, jak správně strukturovat program, nebo jak myslet na všechny možné „nepředvídatelné“ situace. To je samostatné a poměrně široké téma. Místo toho se podíváme na něco praktičtějšího. Existuje totiž jednoduchý mechanismus, který nám může v podobných situacích výrazně pomoct – i když program není úplně dokonalý nebo se vlivem čehokoliv dostane do problémů.

Tento mechanismus se jmenuje: watchdog. A právě na něj se nyní zaměříme.

WATCHDOG – Někdo, kdo tu hlídá…

Watchdog (někdy česky označovaný jako „hlídací pes“) je speciální hardwarový mechanismus, který je součástí většiny mikrokontrolérů. Jeho úkolem je hlídat, zda program běží tak, jak má. V principu funguje jako časovač, který neustále odpočítává určitý interval. Program mu musí v pravidelných chvílích „dát vědět“, že je stále v pořádku – typicky jeho vynulováním. Pokud to ale program z nějakého důvodu neudělá (například proto, že se zasekl, uvízl v nekonečné smyčce nebo čeká na něco, co nepřijde), watchdog vyhodnotí situaci jako chybu a provede restart celého mikrokontroléru. Díky tomu se zařízení dokáže samo „probrat“ z problémového stavu a začít fungovat znova – takový automatický reset bez zásahu uživatele.

Je ale dobré si hned na začátku říct jednu důležitou věc. Watchdog rozhodně není náhrada za nesprávně napsaný program. Není to zázračný lék, který vyřeší všechny problémy s chybami v kódu. Správná filozofie by měla být přesně opačná – program by měl být navržený tak, aby běžel spolehlivě i bez něj. Watchdog by měl sloužit jen jako pojistka pro situace, které nedokážeme snadno ošetřit, nebo které nastanou jen velmi zřídka. Jak říká docent Chocholoušek: „Jednou, maximálně dvakrát, za deset let!“ 😊

Pokud bychom se na watchdog spoléhali jako na hlavní mechanismus řízení programu, znamenalo by to, že náš návrh zdaleka není ideální. V praxi si tedy můžeme watchdog představit spíš jako bezpečnostní pojistku nebo záchrannou brzdu – něco, co zasáhne až ve chvíli, kdy se všechno ostatní nepovede.

Watchdog na Arduinu

Teď, když víme, co watchdog dělá a k čemu je dobrý, pojďme se podívat na to, jak to funguje konkrétně na modulu Arduino – respektive na mikrokontrolérech řady AVR, na kterých je například Arduino UNO postavené.

Na rozdíl od složitějších mikrokontrolérů (například ESP32), kde existuje watchdogů hned několik, je situace u „klasického“ Arduina velmi jednoduchá: Máme k dispozici jeden jediný watchdog.

Je nutné si připomenout, že watchdog je přímo hardwarová součást mikrokontroléru (např. ATmega328P u Arduino UNO) a funguje zcela nezávisle na našem programu. To je mimochodem ta jeho velká výhoda – i když se náš kód „rozbije“, watchdog běží nezávisle dál.

Princip i základní filozofii watchdogu jsme si trochu již vysvětlili v obecném úvodu, tak to jen trochu shrneme:

Watchdog je v podstatě časovač, který:

  1. spustíme s nějakým časovým intervalem,
  2. watchdog začne odpočítávat,
  3. dosáhne-li zadaného času – mikrokontrolér se resetuje.

Je to taková „časovaná bomba“. 😊

My jako programátoři máme jeden jediný úkol: pravidelně ten časovač resetovat (tzv. „krmit watchdog“). V naší pyrotechnické metafoře by to bylo neustále nastavat časovacího mechanismu na počáteční hodnotu. Tím dokážeme „výbuch“ oddálit do okamžiku, až to jednou neuděláme.

Jak dlouho watchdog může u Arduina čekat?

Watchdog na mikrokontrolérech AVR má přednastavené časové intervaly, které lze v programu zadat pomocí předdefinovaných konstat knihovny, která nám pomáhá watchdog obsluhovat – viz následující tabulka č. 1. Pomocí výchozího natavení si zvolíme, jak rychle má watchdog zasáhnout.

  • krátký čas … rychlá reakce, ale větší riziko nechtěného resetu,
  • dlouhý čas … pomalejší reakce, ale větší tolerance.
Konstanta Přibližný čas Popis
WDTO_15MS 15 ms Velmi krátký interval, používá se spíše výjimečně
WDTO_30MS 30 ms Krátká reakční doba
WDTO_60MS 60 ms
WDTO_120MS 120 ms
WDTO_250MS 250 ms
WDTO_500MS 500 ms
WDTO_1S 1 sekunda Častá volba pro jednoduché projekty
WDTO_2S 2 sekundy Velmi často používané
WDTO_4S 4 sekundy Vhodné pro pomalejší operace
WDTO_8S 8 sekundy Nejdelší dostupný interval

Tab. č. 1 – Časové intervaly watchdogu na Arduino (AVR)

Jak watchdog na Arduinu použít v praxi

Teď k tomu nejdůležitějšímu – co vlastně musíme udělat v programu, abychom mohli watchdog začít používat? Dobrá zpráva je, že je to překvapivě jednoduché.

Přidání knihovny

Na začátek programu přidáme knihovnu, které nám s tím celým pomůže. Tím získáme přístup k funkcím pro práci s watchdogem:

#include <avr/wdt.h>

Zapnutí watchdogu:

Abychom watchdog aktivovali, musíme ho zapnout – typicky v sekci setup():

void setup() {
    wdt_enable(WDTO_2S);   // watchdog s časem 2 sekundy
}

Tímto jsme řekli: „Pokud se watchdog neresetuje déle než 2 sekundy, proveď restart.“

Pravidelné resetování watchdogu

Watchdog máme aktivovaný a čas běží. A teď nás čeká to nejdůležitější – musíme watchdog pravidelně „nakrmit“:

void loop() {
    // náš kód

    wdt_reset();   // informujeme watchdog, že vše běží OK
    }

Pokud se wdt_reset() nebude čas volat, watchdog zasáhne a resetuje mikrokontrolér.

Na co si dát pozor hned na začátku

Už v této fázi je dobré si uvědomit jednu důležitou věc: Umístění wdt_reset() v programu je docela klíčové.

  • pokud ho budeme volat příliš často … watchdog ztratí smysl
  • pokud se k němu program nedostane … přijde reset

A právě na tom stojí většina chyb…

Jednoduchý úvodní příklad:

#include <avr/wdt.h>
#define CEKEJ 500

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    Serial.begin(9600);

    wdt_enable(WDTO_2S);
}

void loop() {
    // simulace běžícího programu
    Serial.println("rozsvitim LED");
    digitalWrite(LED_BUILTIN, HIGH);
    delay(CEKEJ);
    Serial.println("zhasinam LED");
    digitalWrite(LED_BUILTIN, LOW);
    delay(CEKEJ);

    Serial.println("resetuji watchdog");
    wdt_reset();
}

Tento program poběží bez problému – watchdog se resetuje každých 500 ms, takže se nikdy nespustí. Tento průběh vidíme na následujícím obrázku v okně sériového monitoru prostředí Arduino.

Pravidelné resetování watchdogu
Obrázek č. 1 – Pravidelné resetování watchdogu při nastavení čekání na hodnotu 500 ms

Stačí ale drobná změna – například zkusme změnit čekací čas (makro CEKEJ) pro funkci delay() na hodnotu 5000 ms a watchdog začne zasahovat.

A přesně to můžeme vidět na obrázku č. 2, který ukazuje výstup v  prostředí Arduino IDE pro nastavení čekacího času 5000 milisekund.

watchdog zasahuje
Obrázek č. 2 – Ve výstupu vidíme, že kód proběhne jen do okamžiku prvního čekání, pak dojde k resetu a kód se rozbíhá znova

Předešlý příklad nám ukázal, že budeme muset moc dobře promýšlet celkový „timing“ programu, abychom se někde zbytečně nezdrželi.

Když watchdog nepomůže

Teď, když už víme, jak watchdog zapnout a jak funguje a trochu tušíme, že tu asi bude nějaký problém s časem, ukážeme si jednu velmi typickou situaci. Situaci, která na první pohled vypadá naprosto nevinně. A přitom je přesně tím místem, kde začínají problémy.

Představme si, že chceme reagovat na stisk tlačítka. To je přece úplný základ – nic složitého. Navíc už jsme staří mazáci, kteří přece vědí, že reálné tlačítko „kmitá“, takže použijeme i jednoduchý debouncing.

Na koncio našeho úsilí vznikne třeba takovýto kód:

#include <avr/wdt.h>
#define BUTTON_PIN 2 void setup() {     pinMode(BUTTON_PIN, INPUT_PULLUP);     Serial.begin(9600);     wdt_enable(WDTO_2S); } void loop() {     if (digitalRead(BUTTON_PIN) == LOW) {         delay(50);   // debounce         // čekáme, dokud držíme tlačítko         while (digitalRead(BUTTON_PIN) == LOW) {             // nic neděláme         }         Serial.println("tlačítko stisknuto");         // tady by byla nějaká akce     }     wdt_reset(); }

Na první pohled všechno vypadá správně.

Program čeká na stisk tlačítka, ošetří zákmit a pak čeká, dokud tlačítko nepustíme. Přesně tak, jak to známe z mnoha jednoduchých příkladů.

Co se ale stane v praxi?

Zkuste si představit situaci, kdy tlačítko trochu podržíme déle. Třeba 3 sekundy!

A už asi tušíme ten průšvih – náš watchdog má timeout jen 2 sekundy!

Program se dostane do části:

while (digitalRead(BUTTON_PIN) == LOW) {

}

A dokud tlačítko nepustíme, tak tam zůstane. Program se „točí“ v této smyčce, než se dostane zpět do sekce loop(), kde by se probvedlo wdt_reset(), uteče nám čas. Výsledek? Watchdog vyhodnotí situaci jako chybu a restartuje Arduino.

Je to chyba watchdogu?

Na první pohled by se mohlo zdát, že watchdog „zlobí“. Vždyť přece program se nezasekl, jen čekáme na tlačítko. Ale ve skutečnosti watchdog funguje naprosto správně!

A víte, co je na tomhle příkladu ještě zrádnější?

Že při běžném testování všechno funguje naprosto bez problémů. Klidně to projde i tím nejpečlivějším „laděním“, protože v tu chvíli hlavně řešíme debouncing. A upřímně – koho při testování napadne držet tlačítko tři sekundy nebo snad déle?

Ale vraťme se zpět k problému s watchdogem.

Proč je z hlediska watchdogu vlastně vše správné? Z jeho pohledu je situace následující: Program přestal z jeho pohledu reagovat, protože watchdog nedostal žádnou informaci o tom, že běží dál. Tak zasáhl! Přesně tak, jak byl navržen.

POZNÁMKA:
Je důležité si uvědomit, že watchdog nerozlišuje, PROČ program neběží – vidí jen, že jej nikdo „nenakrmil“, tak z jeho pohledu program neběží správně.

Rychlá „oprava“, která ale není řešením

Někdo by mohl přijít s nápadem: „Tak watchdog prostě budeme resetovat i uvnitř té smyčky.“

while (digitalRead(BUTTON_PIN) == LOW) {
    wdt_reset();
}

Na první pohled problém zmizí. Arduino se už nerestartuje, watchdog je spokojený.

Jenže…
    …skutečný problém jsme tím nevyřešili!

Program je stále zablokovaný:

  • nedělá nic jiného,
  • nereaguje na okolí,
  • neobsluhuje žádné další části programu.

Watchdog jsme sice „uklidnili“, ale program jsme nespravili.

Správná cesta: neblokovat program

Řešení je v tom, že program nesmí „čekat na jednom místě“. Musí běžet plynule dál. Místo blokujících konstrukcí (while, dlouhé delay()) musíme použít řízení pomocí času.

Například pomocí funkce millis():

#include <avr/wdt.h>
#define BUTTON_PIN 2

unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;
int lastButtonState = HIGH;
int buttonState = HIGH;

void setup() {
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    Serial.begin(9600);

    wdt_enable(WDTO_2S);
}

void loop() {
    int reading = digitalRead(BUTTON_PIN);

    if (reading != lastButtonState) {
        lastDebounceTime = millis();
    }

    if ((millis() - lastDebounceTime) > debounceDelay) {
        if (reading != buttonState) {
            buttonState = reading;

            if (buttonState == LOW) {
                // akce při stisku
                Serial.println("tlačítko stisknuto");
            }
        }
    }

    lastButtonState = reading;

    wdt_reset();   // watchdog stále dostává signál
}

Na první pohled může tenhle kód působit o něco složitěji než předchozí příklad. Ve skutečnosti ale dělá přesně to samé – jen z pohledu obsluhy watchdogu mnohem „šetrnějším“ způsobem.

Obrázek č. 3 ukazuje vývojový diagram tohoto řešení:

Vývojový diagram obsluhy tlačítka
Obrázek č. 3 – Vývojový diagram obsluhy tlačítka zohledňující funkci watchdogu

Program opět sleduje stav tlačítka a reaguje na jeho stisk. Rozdíl je v tom, že už nikde nečeká na jednom místě. Pojďme si ukázat, jak to ten program dělá.

Čtení aktuálního stavu

V každém průchodu funkcí loop() se do proměnné reading nejdříve načte aktuální stav tlačítka:

int reading = digitalRead(BUTTON_PIN);

To znamená, že program stále dokola zjišťuje, jestli je tlačítko stisknuté nebo ne.

Detekce změny stavu

Dalším krokem programu je testování stavu tlačítek. A to je k nějaké aktuálně zadané hodnotě, ale k hodnotě předešlého stavu (uloženo v proměnné lastButtonState):

if (reading != lastButtonState) {
    lastDebounceTime = millis();
}

Jakmile se stav tlačítka změní, uložíme si aktuální čas této změny. Tím si poznamenáme okamžik, kdy ke změně došlo.

Zatím „nepanikaříme“ a okamžitě nereagujeme – tlačítko po stisku „kmitá“ a hodnoty se krátce mění sem a tam.

Debounce pomocí času

Předešlá podmínka může být při každém dalším průchodu smyčkou loop() opětovně splněná, či nesplněna – to záleží na kmitání tlačítka. Při každé vhodné změně stále posouváme čas stisknutí. Jakmile dojde k ustálení hodnoty, začneme čekat, jestli je změna definitivní – tedy, zda vydrží dostatečně dlouho:

if ((millis() - lastDebounceTime) > debounceDelay)

Čekáme, ale nečekáme „na místě“ jako u delay() nebo nějaké testovací smyčce. Místo toho se ptáme: „Uplynulo od změny už dost času?“ Pokud ano, bereme změnu jako platnou.

Potvrzení změny

Pak zkontrolujeme, jestli se stav opravdu změnil:

if (reading != buttonState) {
    buttonState = reading;

Jestli ano, nastavíme proměnnou buttonState na stav tlačítka, aby se tato podmínka při dalším průběhu opět neplnila. Tím se vyhneme opakované reakci na stejný stav.

Reakce na stisk

A konečně:

if (buttonState == LOW) {
    // akce při stisku
}

Zde provedeme akci – například rozsvítíme LEDku, odešleme data, sepne se relé apod. V tomto příkladu jen vypíšeme text na sériový port.

Uložení předchozího stavu

Na konci si uložíme poslední naměřený stav do proměnné lastButtonState:

lastButtonState = reading;

To nám umožní při dalším průchodu zjistit, jestli došlo ke změně od stavu, kdy došlo k nějaké akci.

Watchdog zůstává „nakrmený“

A to nejdůležitější z pohledu watchdogu:

wdt_reset();

Tato funkce se volá při každém průchodu smyčkou. Díky tomu watchdog dostává pravidelný signál, program nikdy „nezamrzne“ v jedné smyčce a celý systém zůstává stabilní.

Co je na tomto přístupu zásadní – celý program je postavený tak, že nikdy nikde nečeká.

Místo toho neustále běží dokola, postupně vyhodnocuje stav a reaguje jen tehdy, když je to potřeba. A právě to je ten hlavní rozdíl oproti blokujícímu řešení.

Možná si říkáte, že je tenhle kód o něco složitější.

A máte pravdu.

Ale zároveň je stabilnější, předvídatelnější a hlavně bezpečný z pohledu watchdogu. A přesně o to nám jde.

Co je na tom jiné?

Watchdog nás v tomto případě vlastně donutil napsat lepší program. Možná jste si všimli, že takový kód začíná být o něco složitější. A jakmile přidáme další funkce – třeba více tlačítek, senzory nebo komunikaci – může se z toho stát nepřehledná změť podmínek.

Právě tady přichází na scénu jiný přístup – tzv. stavové automaty (FSM)

Závěr

Jak jsme si ukázali, že samotné použití watchdogu na Arduinu vlastně není nijak složité. Zapnout ho, pravidelně resetovat a zvolit vhodný časový interval zvládneme během pár minut. Ten „skutečný oříšek“ ale leží jinde. Tím nejtěžším problémem je totiž změna způsobu uvažování nad samotným programem.

Zpravidla nestačí jen přidat wdt_reset() na správné místo. Je potřeba navrhnout program tak, aby:

  • nikdy zbytečně neblokoval běh,
  • nezasekával se v čekacích smyčkách,
  • a měl šanci se pravidelně „ozvat“ watchdogu, že je všechno v pořádku.

Jinými slovy – watchdog nás nenutí jen přidat pár řádků kódu, ale přemýšlet o programu jako o neustále běžícím procesu, který se pravidelně vrací do hlavní smyčky, kde je watchdog resetován. A právě zde se dostáváme k přístupu, který se v embedded světě používá velmi často – stavové automaty (FSM – Finite State Machine).

Program pak není psaný jako posloupnost blokujících kroků, ale jako smyčka, která:

  • stále běží dokola,
  • podle aktuálního „stavu“ vykonává malý kousek práce,
  • a postupně přechází mezi jednotlivými stavy

Dalo by se říct, že program „navštěvuje“ jednotlivé části své logiky po malých úsecích – a mezi tím vším má vždy prostor obsloužit watchdog.

Tento přístup je z počátku možná trochu nezvyklý – zvlášť pokud přicházíme ze světa klasického programování na PC. Na druhou stranu právě on vede k mnohem robustnějším a spolehlivějším aplikacím. A hlavně: watchdog si s takovým programem „skvěle rozumí“.

Jak tedy stavové automaty fungují v praxi a jak je použít právě na Arduino?

To už je téma, které si rozhodně zaslouží samostatný článek