ESP32 · FreeRTOS · Debugging

ESP32 Watchdog e Core Dump: trasformare gli stalli FreeRTOS in evidenza

2026-05-25 · Davide Carrese
Commenti

Un reset da watchdog non è una correzione, e disabilitare il watchdog non è debugging. Nei progetti ESP32 il punto utile sta in mezzo: far fallire il firmware in modo abbastanza esplicito da lasciare evidenza al reboot successivo — quale task si è bloccato, quale core era coinvolto, e se il problema reale era latenza interrupt, starvation dello scheduler o deadlock.

Il problema pratico

Molti guasti ESP32 sul campo si presentano allo stesso modo: il dispositivo smette di pubblicare dati, BLE o Wi‑Fi diventano instabili, un protocollo UART va in timeout, oppure un controllo motore perde una deadline. Poi l’unità si riavvia e l’applicazione sembra tornare sana. Se il firmware conserva solo un contatore generico di reset, il team non ha imparato quasi nulla.

ESP-IDF offre meccanismi che conviene trattare come parte dell’architettura di prodotto, non come interruttori temporanei di debug: interrupt watchdog, task watchdog, panic handler, backtrace, GDB stub e core dump su flash o UART. La domanda non è “abilitiamo il watchdog?”. La domanda migliore è: quale evidenza preserviamo quando il watchdog scatta?

Due watchdog, due famiglie di bug diverse

Interrupt watchdog: sezioni critiche lunghe e ISR bloccate

L’interrupt watchdog intercetta situazioni in cui il sistema non riesce a servire gli interrupt in tempo. Cause tipiche: sezioni critiche troppo lunghe, codice che disabilita gli interrupt intorno a troppo lavoro, ritardi flash/cache nel contesto sbagliato, o carico interrupt ad alta priorità che impedisce al sistema di fare housekeeping. Su ESP32 dual-core può essere fuorviante guardare solo i log applicativi: un task può essere innocente mentre il core da cui dipende non schedula normalmente.

Quando scatta questo watchdog, cerco subito punti in cui il firmware tratta “atomico” come “fai tutto con gli interrupt mascherati”. Aggiornare registri o piccoli indici di ring buffer può starci. Formattare stringhe, aspettare flag periferici, copiare payload grandi o chiamare driver dentro una sezione critica no.

Task watchdog: starvation, deadlock e ownership sbagliata

Il task watchdog è più utile per il progresso applicativo. In ESP-IDF può monitorare task o user iscritti e rilevare quando non resettano il watchdog entro il tempo previsto. Questo serve a catturare un task che tecnicamente è ancora “vivo”, ma non sta più facendo progresso: aspetta per sempre un mutex, gira su un bit di stato periferico, oppure viene affamato da un task troppo aggressivo alla stessa priorità o più alta.

Un errore comune è alimentare il watchdog da un timer o da un supervisor task non correlato. Questo dimostra che lo scheduler è vivo, ma non che il lavoro critico sta avanzando. Il reset va fatto nel percorso che rappresenta progresso reale: un’iterazione di control loop completata, una coda drenata, una transazione di protocollo conclusa o uno stato idle sicuro della macchina a stati.

Un pattern minimo basato sul progresso

La forma esatta delle API dipende dalla versione ESP-IDF e dalla configurazione del progetto, ma la regola di design resta stabile: iscrivi il task che possiede il loop critico e resetta il watchdog solo dopo lavoro utile completato. Evita di resettarlo prima di una chiamata bloccante che potrebbe non tornare mai.

#include "esp_task_wdt.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

static QueueHandle_t telemetry_q;

typedef struct {
    uint32_t sensor_id;
    int32_t value;
} telemetry_msg_t;

static bool process_one_message(const telemetry_msg_t *msg)
{
    /* Tenere questo percorso bounded: niente retry infiniti,
       niente tempeste di printf, niente attese eterne su periferiche. */
    return app_store_sample(msg->sensor_id, msg->value) == ESP_OK;
}

static void telemetry_task(void *arg)
{
    ESP_ERROR_CHECK(esp_task_wdt_add(NULL));   // iscrive il task corrente

    for (;;) {
        telemetry_msg_t msg;

        if (xQueueReceive(telemetry_q, &msg, pdMS_TO_TICKS(500)) == pdTRUE) {
            if (process_one_message(&msg)) {
                /* Feed solo dopo progresso reale. */
                ESP_ERROR_CHECK(esp_task_wdt_reset());
            }
        } else {
            /* Heartbeat idle accettabile solo se “nessun lavoro” è sano. */
            ESP_ERROR_CHECK(esp_task_wdt_reset());
        }
    }
}

L’esempio è volutamente noioso. Il punto non è la queue; è la posizione del reset. Se process_one_message() può bloccarsi per sempre, il task watchdog deve scattare. Se un’inversione su mutex impedisce al task di girare, deve scattare. Se la queue è vuota e questo è uno stato valido, il task può resettare il watchdog sul timeout. Se invece la queue vuota è un fault, non fare feed lì: registra uno stato d’errore e lascia decidere alla policy di supervisione.

I core dump rendono utile il watchdog dopo il reboot

Nei prodotti remoti, l’output di panic visibile su UART in laboratorio raramente basta. I core dump ESP-IDF possono essere scritti su flash o UART e decodificati dopo. La flash è spesso l’opzione migliore sul campo: dopo il reset, l’applicazione può caricare il dump o esporlo tramite comando di servizio prima di cancellarlo.

I core dump non sono gratuiti. Devi riservare storage, decidere quanta stack data includere, proteggere dati sensibili se i dump escono dal dispositivo, e assicurarti che il percorso di raccolta crash non comprometta l’affidabilità del boot. Però anche un dump piccolo con snapshot dei task e backtrace spesso fa la differenza tra indovinare e correggere.

Regola operativa: una policy watchdog senza raccolta post-mortem è solo una feature di disponibilità. Una policy con reset reason, backtrace, core dump, versione firmware, build ID e breadcrumb dell’ultimo sottosistema è un sistema di debugging.

Cosa loggo prima del feed

Mi piacciono breadcrumb leggeri che sopravvivono fino al crash successivo senza richiedere logging continuo. Ogni task critico può aggiornare una piccola struttura retained con enum di stato, ultimo codice errore, contatore monotono e identificativo dello step corrente. Deve restare statica, semplice e aggiornabile senza allocazioni.

Non trasformarla in un framework di logging dentro path real-time. L’obiettivo è un “ultimo stato utile” compatto, leggibile al boot insieme alla reset reason. Combinato con un core dump decodificato, permette di separare rapidamente quattro casi: CPU bloccata in sezione critica, task bloccato su sincronizzazione, corruzione heap/stack che poi causa panic, oppure chiamata driver fatta in un contesto non valido.

Esempio pratico: gateway di telemetria sul campo

Immagina un gateway ESP32 che legge una scheda sensori via UART, salva campioni in locale e pubblica batch via Wi‑Fi/MQTT. Il prodotto può restare mesi dentro un quadro, quindi “si è riavviato una volta” non basta. Tratterei il task telemetria, il task MQTT, lo storage e il percorso OTA/update come domini di progresso separati.

Per il task telemetria, il task watchdog va alimentato solo dopo aver parsato un frame valido o dopo un timeout idle deliberatamente sano. Per il task MQTT, dopo che un tentativo di publish si è concluso, non prima di aspettare lo stato rete. Per lo storage, dopo una transazione di scrittura bounded. Se l’unità resetta, al boot successivo deve riportare: reset reason, task/core coinvolto, ultimo dominio di progresso, build ID firmware e breadcrumb retained. Così un generico “il dispositivo remoto si è bloccato” diventa una pista concreta: parser UART bloccato, scrittura flash fuori budget o task rete che affama il sistema.

Checklist pratica

Come lo affronterei su un progetto cliente

Prima mapperei il firmware in domini di progresso: comunicazioni, acquisizione/controllo, storage, update path e supervisione safety. Per ciascun dominio definirei cosa significa “progresso sano” in termini misurabili. Poi configurerei i watchdog ESP32 intorno a quelle definizioni, non intorno a timeout copiati da un esempio.

Secondo, aggiungerei una piccola interfaccia di crash record: reset reason, sorgente watchdog quando disponibile, build metadata, breadcrumb selezionati e gestione core dump. Su un prodotto connesso, il boot successivo caricherebbe il record con rate limit. Su un prodotto offline, sarebbe recuperabile via UART, USB, BLE o comando service/manufacturing.

Infine aggiungerei test di fault injection al piano firmware. Un design watchdog non è completo finché qualcuno non ha causato intenzionalmente il trigger e verificato che l’evidenza punti al fault iniettato. È questo passaggio che trasforma “l’unità si è riavviata” in un report tecnico azionabile.

Fonti consultate

Commenti

Hai commenti? Mandami una mail.