Un watchdog es un mecanismo de supervisión que reinicia o avisa si el sistema deja de responder durante más tiempo del permitido.
En sistemas embebidos, esto es muy importante. Un PC puede mostrar una ventana de error y esperar a que alguien haga clic. Un microcontrolador en una máquina, una estación remota o una instalación domótica tiene que recuperarse solo.
Qué problema resuelve un watchdog
Un firmware con FreeRTOS puede fallar de muchas formas bastante antipáticas:
- Una tarea entra en un bucle infinito.
- Una tarea de alta prioridad no cede CPU.
- Dos tareas quedan bloqueadas esperando recursos entre sí.
- Una comunicación se queda esperando para siempre.
- Una ISR tarda demasiado y bloquea el sistema.
En todos esos casos, el sistema puede parecer “vivo” desde fuera, pero una parte importante ha dejado de funcionar.
El watchdog actúa como un vigilante sencillo: si nadie le confirma periódicamente que todo va bien, asume que algo se ha roto y toma medidas.
Watchdog hardware y watchdog de tareas
Hay varias capas de watchdog.
El watchdog hardware suele ser un periférico del microcontrolador. Si no se alimenta a tiempo, reinicia el chip completo.
El watchdog de tareas supervisa que determinadas tareas sigan ejecutándose. En ESP32, el Task Watchdog Timer suele vigilar las tareas Idle y también permite suscribir tareas concretas para detectar periodos excesivos sin resetearlo.
La diferencia práctica es esta:
| Tipo | Qué vigila |
|---|---|
| Hardware watchdog | Que el sistema completo no se quede congelado |
| Task watchdog | Que tareas concretas sigan cediendo y ejecutándose |
Los dos son útiles, pero no sustituyen a un buen diseño. Un Task Watchdog centrado en Idle, por ejemplo, no detectará un deadlock en el que las tareas implicadas están bloqueadas pero Idle sigue ejecutándose. Un watchdog no arregla el bug; solo ayuda a detectar el fallo o recuperarse.
No alimentar el watchdog desde cualquier sitio
Un error típico es alimentar el watchdog en un timer global o en una tarea cualquiera.
void TareaWatchdogMala(void *pvParameters) {
for(;;) {
alimentarWatchdog();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}Esto confirma que esa tarea sigue viva, pero no confirma que el resto del sistema funcione. Podemos tener la tarea de sensores colgada, la de comunicaciones muerta y el watchdog feliz como si todo estuviera perfecto.
Lo correcto es que cada tarea crítica reporte su estado, y una tarea supervisora alimente el watchdog solo si todas han dado señales de vida.
Patrón de tarea supervisora
Podemos usar bits para marcar qué tareas han pasado por su punto de control.
#define BIT_SENSOR (1 << 0)
#define BIT_WIFI (1 << 1)
#define BIT_CONTROL (1 << 2)
EventGroupHandle_t eventosWatchdog;
void TareaSensor(void *pvParameters) {
for(;;) {
leerSensor();
xEventGroupSetBits(eventosWatchdog, BIT_SENSOR);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void TareaWifi(void *pvParameters) {
for(;;) {
procesarWifi();
xEventGroupSetBits(eventosWatchdog, BIT_WIFI);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void TareaSupervisora(void *pvParameters) {
const EventBits_t bitsEsperados = BIT_SENSOR | BIT_WIFI | BIT_CONTROL;
for(;;) {
// Empezamos una ventana nueva sin aceptar señales antiguas.
xEventGroupClearBits(eventosWatchdog, bitsEsperados);
EventBits_t bits = xEventGroupWaitBits(
eventosWatchdog,
bitsEsperados,
pdTRUE,
pdTRUE,
pdMS_TO_TICKS(2000)
);
if ((bits & bitsEsperados) == bitsEsperados) {
alimentarWatchdog();
}
else {
registrarFallo(bits);
// Dejamos de alimentar el watchdog o aplicamos una recuperación controlada
}
}
}Cada tarea crítica marca su bit cuando completa una vuelta sana de trabajo. La supervisora espera a que todos los bits aparezcan dentro del tiempo máximo. Si falta alguno, sabemos qué parte del sistema no está respondiendo.
Elegir bien los puntos de control
No basta con poner el “estoy vivo” al principio del bucle. Hay que colocarlo después de haber hecho el trabajo importante.
void TareaControl(void *pvParameters) {
TickType_t ultimoDespertar = xTaskGetTickCount();
const TickType_t periodo = pdMS_TO_TICKS(20);
for(;;) {
leerEntradas();
calcularSalida();
aplicarSalida();
// Punto de control después de completar el ciclo útil
xEventGroupSetBits(eventosWatchdog, BIT_CONTROL);
vTaskDelayUntil(&ultimoDespertar, periodo);
}
}Así confirmamos que la tarea no solo se ejecuta, sino que ha completado su ciclo principal.
Timeouts por todas partes
Un sistema robusto no debería esperar indefinidamente en operaciones que pueden fallar.
En vez de esto:
xQueueReceive(cola, &dato, portMAX_DELAY);en una tarea crítica puede tener más sentido:
if (xQueueReceive(cola, &dato, pdMS_TO_TICKS(500)) != pdTRUE) {
registrarTimeout();
recuperarEstado();
}portMAX_DELAY está bien cuando realmente queremos esperar para siempre. Pero en tareas que deben demostrar salud periódicamente, los timeouts son parte del diseño.