6 Luglio 2026 di Alessandro Colucci
Se hai cercato questo articolo, probabilmente hai già visto uno di questi nel monitor seriale:
E (5231) task_wdt: Task watchdog got triggered. The following tasks/users did not reset the watchdog in time:
E (5231) task_wdt: - loopTask (CPU 1)
E (5231) task_wdt: Tasks currently running:
E (5231) task_wdt: CPU 0: IDLE0
E (5231) task_wdt: CPU 1: loopTask
E (5231) task_wdt: Aborting.
O peggio: nessun errore, solo un riavvio silenzioso sul campo, segnalato da un cliente che dice che il device "si riavvia da solo" ogni tanto. Questo articolo copre entrambi i casi — come funziona davvero il watchdog su ESP32, i tre errori più comuni che causano riavvii indesiderati, e cosa dovrebbe fare davvero un firmware in produzione quando scatta.
Si parla del "watchdog" come se fosse una cosa sola. Su ESP32 ce ne sono tre, e confonderli è la prima fonte di errori:
loopTask) e si aspetta che ognuno "faccia il check-in" periodicamente. È quello dietro l'errore sopra.portDISABLE_INTERRUPTS() lunga, o un task che monopolizza un core senza cedere il controllo). Scatta con un panic "Guru Meditation Error: IWDT", non con il messaggio task_wdt di sopra.Nel 90% dei casi di "il mio ESP32 si riavvia da solo" in un firmware di produzione, il colpevole è il Task Watchdog — è il focus del resto dell'articolo.
In ESP-IDF, il task watchdog si configura tramite esp_task_wdt_config_t e si inizializza una volta sola:
#include "esp_task_wdt.h"
esp_task_wdt_config_t twdt_config = {
.timeout_ms = 5000,
.idle_core_mask = (1 << 0) | (1 << 1), // monitora entrambi i task idle
.trigger_panic = true,
};
esp_task_wdt_init(&twdt_config);
esp_task_wdt_add(NULL); // iscrive il task corrente
Ogni task che iscrivi con esp_task_wdt_add() deve chiamare esp_task_wdt_reset() prima che scada il timeout, altrimenti viene segnalato. Sul core Arduino-ESP32, il loop task è iscritto automaticamente con un timeout di default (storicamente 5s, ma controlla la versione del tuo core — è cambiato tra le release), motivo per cui gli sketch Arduino semplici raramente vedono questo errore finché non aggiungono qualcosa di bloccante.
È la causa più comune in assoluto. Ognuna di queste può bloccare abbastanza a lungo da far scattare il watchdog:
delay() o una lettura sensore bloccante più lunga del timeout, dentro un task iscritto al watchdogwhile(1) in polling che aspetta una condizione senza mai cedere il controllo allo schedulerLa soluzione non è sempre "nutrirlo più spesso" — spesso la vera soluzione è spostare del tutto il lavoro bloccante fuori dal task monitorato (un task FreeRTOS dedicato non iscritto al watchdog, o un'API asincrona/a callback invece che bloccante).
Se configuri manualmente idle_core_mask e monitori solo il core 0 mentre il tuo codice bloccante gira sul core 1 (o viceversa, a seconda di come hai assegnato i task con xTaskCreatePinnedToCore), il watchdog semplicemente non vede mai il problema — finché non lo vede, in un modo diverso e più difficile da riprodurre. Fai coincidere la configurazione del watchdog con l'assegnazione reale dei task ai core, non con un default copiato e incollato.
Scritture OTA, commit NVS voluminosi e handshake TLS sono tutti operazioni legittimamente lente — a volte più lente di un timeout impostato in modo conservativo. Invece di disabilitare il watchdog per queste operazioni (il che ne vanifica lo scopo), il pattern usato in produzione è chiamare esp_task_wdt_reset() in checkpoint sicuri dentro l'operazione lenta stessa, oppure spostarla su un task non monitorato se è rara e ben isolata.
Prevenire i riavvii è solo metà del lavoro — un device che arriva dal cliente deve sopravviverne uno con eleganza e dirti perché. All'avvio, controlla il motivo del reset:
#include "esp_system.h"
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_TASK_WDT || reason == ESP_RST_INT_WDT || reason == ESP_RST_WDT) {
// Loggalo, su Serial, in NVS per recuperarlo dopo, o direttamente sul
// tuo backend se hai connettività.
}
E assicurati che un watchdog che scatta ripetutamente non diventi un loop di riavvio infinito — se il device si riavvia per il watchdog, si riconnette al WiFi, e rifà subito la stessa chiamata bloccante che ha causato il primo riavvio, hai costruito un mattone che si riavvia ogni 5 secondi per sempre. Una guardia semplice (conta i riavvii in una finestra breve via RTC memory, passa a una modalità sicura/degradata dopo N tentativi) evita questo scenario.
idle_core_mask corrisponde alla reale assegnazione dei task ai coreNiente di esotico — è il tipo di boilerplate che ogni firmware ESP32 di produzione finisce per aggiungere, e che ogni progetto reinventa leggermente diverso. Se preferisci specificare il comportamento e ottenere codice funzionante invece di ricablare tutto questo a mano un'altra volta, è esattamente lo scopo di PleaseDontCode.