A watchdog is a supervision mechanism that resets or alerts if the system stops responding for longer than allowed.
In embedded systems, this is very important. A PC can show an error window and wait for someone to click. A microcontroller in a machine, a remote station, or a home automation installation must recover on its own.
What problem does a watchdog solve
Firmware with FreeRTOS can fail in many rather unpleasant ways:
- A task enters an infinite loop.
- A high-priority task does not yield the CPU.
- Two tasks become blocked waiting for resources from each other.
- A communication waits forever.
- An ISR takes too long and blocks the system.
In all these cases, the system may appear “alive” from the outside, but an important part has stopped working.
The watchdog acts as a simple sentinel: if no one periodically confirms that everything is fine, it assumes something has broken and takes action.
Hardware watchdog and task watchdog
There are several watchdog layers.
The hardware watchdog is usually a peripheral of the microcontroller. If not petted in time, it resets the entire chip.
The task watchdog monitors that specific tasks are still running. On ESP32, the Task Watchdog Timer usually monitors the Idle tasks and also allows subscribing specific tasks to detect excessive periods without resetting it.
The practical difference is this:
| Type | What it monitors |
|---|---|
| Hardware watchdog | That the entire system does not freeze |
| Task watchdog | That specific tasks keep yielding and running |
Both are useful, but they do not replace good design. A Task Watchdog focused on Idle, for example, will not detect a deadlock where the involved tasks are blocked but Idle continues running. A watchdog does not fix the bug; it only helps detect the failure or recover from it.
Do not pet the watchdog from just anywhere
A typical mistake is to pet the watchdog in a global timer or in any random task.
void BadWatchdogTask(void *pvParameters) {
for(;;) {
petWatchdog();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}This confirms that this particular task is alive, but it does not confirm that the rest of the system is working. The sensor task could be hung, the communication task dead, and the watchdog would be happy as if everything were perfectly fine.
The correct approach is for each critical task to report its status, and a supervisor task to pet the watchdog only if all have given signs of life.
Supervisor task pattern
We can use bits to mark which tasks have passed their checkpoints.
#define BIT_SENSOR (1 << 0)
#define BIT_WIFI (1 << 1)
#define BIT_CONTROL (1 << 2)
EventGroupHandle_t watchdogEvents;
void SensorTask(void *pvParameters) {
for(;;) {
readSensor();
xEventGroupSetBits(watchdogEvents, BIT_SENSOR);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void WifiTask(void *pvParameters) {
for(;;) {
processWifi();
xEventGroupSetBits(watchdogEvents, BIT_WIFI);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void SupervisorTask(void *pvParameters) {
const EventBits_t expectedBits = BIT_SENSOR | BIT_WIFI | BIT_CONTROL;
for(;;) {
// Start a new window without accepting old signals.
xEventGroupClearBits(watchdogEvents, expectedBits);
EventBits_t bits = xEventGroupWaitBits(
watchdogEvents,
expectedBits,
pdTRUE,
pdTRUE,
pdMS_TO_TICKS(2000)
);
if ((bits & expectedBits) == expectedBits) {
petWatchdog();
}
else {
logFailure(bits);
// Stop petting the watchdog or apply a controlled recovery
}
}
}Each critical task sets its bit when it completes a healthy work cycle. The supervisor waits for all bits to appear within the maximum time. If any is missing, we know which part of the system is not responding.
Choosing checkpoints wisely
It is not enough to put the “I’m alive” at the beginning of the loop. It must be placed after completing the important work.
void ControlTask(void *pvParameters) {
TickType_t lastWakeTime = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(20);
for(;;) {
readInputs();
calculateOutput();
applyOutput();
// Checkpoint after completing the useful cycle
xEventGroupSetBits(watchdogEvents, BIT_CONTROL);
vTaskDelayUntil(&lastWakeTime, period);
}
}This way we confirm that the task not only runs, but has completed its main cycle.
Timeouts everywhere
A robust system should not wait indefinitely on operations that can fail.
Instead of this:
xQueueReceive(queue, &data, portMAX_DELAY);in a critical task it may make more sense:
if (xQueueReceive(queue, &data, pdMS_TO_TICKS(500)) != pdTRUE) {
logTimeout();
recoverState();
}portMAX_DELAY is fine when we truly want to wait forever. But in tasks that must periodically demonstrate health, timeouts are part of the design.