04 · Watchdog Timers & Reliability¶
Not flashed to hardware
Reasoned through against the documented ESP8266 core software
watchdog (ESP.wdtEnable/ESP.wdtFeed and its always-on hardware
watchdog) and the ESP32 core's esp_task_wdt API. Not compiled or
flashed to physical hardware in this environment.
Devices hang. Watchdogs recover them.¶
A device left in the field will eventually hit a bug you didn't reproduce on the bench: a library blocking forever, a heap allocation failure, a sensor that stops acknowledging on I2C. Without a way to detect "the main loop stopped responding," the only fix is a human pulling power. A watchdog timer (WDT) is a hardware countdown that resets the chip unless the firmware "feeds" it regularly — silence past the timeout means something went wrong, so it reboots you back to a known state.
ESP8266: hardware WDT is always running¶
The ESP8266 documents a hardware watchdog that's active by default and
resets the chip (~8s timeout) if loop() never returns control to the
core — this alone catches most infinite loops for free. The core also
documents a software watchdog layered on top, tunable with
ESP.wdtEnable()/ESP.wdtFeed():
// esp8266-watchdog.ino
void setup() {
Serial.begin(115200);
// wdtEnable() with no interval argument uses the documented default;
// this sketch relies mostly on the always-on hardware WDT.
}
void loop() {
doWork();
yield(); // documented as feeding both the software and hardware WDT;
// required in any loop with a long-running block
}
void doWork() {
for (int i = 0; i < 1000; i++) {
// Any loop long enough to starve the WDT needs an explicit yield()
// or ESP.wdtFeed() inside it, not just at the top of loop().
if (i % 100 == 0) yield();
delayMicroseconds(500);
}
}
ESP32: the task watchdog API¶
The ESP32 core documents esp_task_wdt_init/add/reset for supervising
specific FreeRTOS tasks (the Arduino loop() runs as one such task):
// esp32-watchdog.ino
#include <esp_task_wdt.h>
const int WDT_TIMEOUT_S = 5;
void setup() {
Serial.begin(115200);
// esp_task_wdt_init(timeout_s, panic_on_timeout) is the documented
// ESP32 core API; panic=true reboots on timeout.
esp_task_wdt_init(WDT_TIMEOUT_S, true);
esp_task_wdt_add(NULL); // NULL registers the calling task (loop's task)
}
void loop() {
doWork();
esp_task_wdt_reset(); // documented "feed" call; must run within the timeout window
}
void doWork() {
// Simulate a bounded unit of work per loop iteration.
delay(50);
}
Catching a hang mid-block, not just at loop's top¶
A single slow call buried inside doWork() can still starve the
watchdog if nothing resets it until doWork() returns. Feed from inside
long-running sections too:
void doLongOperation() {
for (int i = 0; i < 200; i++) {
processChunk(i);
if (i % 20 == 0) {
esp_task_wdt_reset(); // ESP32
// yield(); // ESP8266 equivalent
}
}
}
Detecting why the last reset happened¶
Both cores document a reset-reason API, useful for logging "was this a watchdog recovery or a normal power-up?":
// ESP8266
#include <ESP8266WiFi.h>
Serial.println(ESP.getResetReason()); // e.g. "Watchdog", "Power on", "External System"
// ESP32
#include <esp_system.h>
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_TASK_WDT || reason == ESP_RST_WDT) {
Serial.println("Recovered from watchdog reset");
}
Publishing this on boot (as part of the status payload from Level 1/2's capstones) turns silent field reboots into a diagnosable signal instead of a mystery gap in your data.
A software "deadman" check as a second line of defense¶
For logic that legitimately calls slow libraries (e.g. flash writes),
pair the hardware WDT with an application-level check that something
useful is actually progressing, not just that loop() is spinning:
unsigned long lastGoodPublish = 0;
const unsigned long MAX_SILENCE_MS = 60000;
void loop() {
if (publishStatus()) {
lastGoodPublish = millis();
}
if (millis() - lastGoodPublish > MAX_SILENCE_MS) {
Serial.println("No successful publish in 60s — forcing restart");
ESP.restart(); // documented on both cores
}
esp_task_wdt_reset(); // or yield() on ESP8266
}
How It Actually Works¶
The hardware watchdog timer (WDT) is a free-running counter clocked independently of your main code path, wired directly to the chip's reset logic — if that counter reaches its terminal value without being reset ("fed"/"kicked") by a specific register write from your firmware, it triggers exactly the same electrical reset sequence as pressing the physical RST pin, which is why a watchdog-triggered restart looks identical to a power cycle in your boot logs (no exception, no stack trace, just a fresh setup()). These chips actually run two watchdogs layered on top of each other: a software/RTOS watchdog that checks whether each registered task (including the hidden Wi-Fi/lwIP task) has yielded within its allotted time slice, and beneath that a true hardware WDT that fires if even the software watchdog handler itself hangs — this is why a truly wedged CPU (stuck in a hard fault loop, say) still eventually recovers even when the software layer meant to catch it has itself failed.
ESP.wdtFeed()/the automatic feed inside Arduino core's background yield calls only resets the counter, not the underlying cause — a watchdog reset happening reliably at the same point in your code is the WDT correctly doing its job of surfacing a real blocking operation (a busy-wait loop, a hung I2C clock-stretch, a DNS lookup with no timeout) that starved the scheduler long enough to miss its own required feed interval; silencing the watchdog via ESP.wdtDisable() doesn't fix that underlying hang, it just removes the safety net that would otherwise force a recovery from it.
(These examples were written and reasoned through at the register/protocol level but were not flashed to a physical board for this pass — verify timing-sensitive details against your exact chip datasheet before relying on them in production.)
Exercise¶
- Explain the difference between the ESP8266's hardware WDT and its software WDT, and why the hardware one alone doesn't guarantee your application logic is healthy.
- Write the ESP32 task-watchdog setup with a 3-second timeout and a
loop()that legitimately takes 2 seconds per iteration. - Add reset-reason logging to a
setup()on each core and describe what value each would report after a watchdog-triggered reboot. - Implement the "deadman" check pattern around a hypothetical
sendTelemetry()function that can silently fail without throwing.