1. Тема дня
Watchdog в ESP32/ESP-IDF и STM32: зачем он нужен, какие бывают виды, почему он срабатывает, как правильно “кормить” watchdog и почему плохая идея вставлять esp_task_wdt_reset() куда попало. Главная мысль: Watchdog - не костыль от зависаний, а диагностический механизм, который показывает, что архитектура задачи нарушена.
2. Зачем это нужно
Опасные места:
EC25 modem:
- AT-команда не вернула ответ
- UART-парсер застрял
- QMTOPEN/QMTCONN долго не завершается
- сеть пропала
Traffic light inputs:
- цикл чтения стал тяжёлым
- фильтр/ADC/ADS1115 блокирует быстрый путь
- логирование забивает UART
Transport:
- MQTT/TCP/UDP отправка блокирует задачу
- ожидание ACK сделано в неправильном месте
- reconnect держит mutex слишком долго
CLI:
- длинная команда печатает огромный вывод
- CLI вызывает тяжёлую функцию сервисаWatchdog должен отвечать: система ещё жива и выполняет обязанности или застряла?
3. Watchdog в ESP-IDF
Упрощённо:
Interrupt Watchdog / IWDT
Следит, что система не застряла с отключенными прерываниями
или в слишком длинной критической секции.
Task Watchdog / TWDT
Следит, что задачи не захватили CPU слишком надолго
и что idle task получает время на выполнение.Если high-priority задача крутится бесконечно без yield/blocking wait, idle task не выполняется, и TWDT это обнаружит.
4. Почему watchdog срабатывает на “рабочем” коде
Код может вроде работать:
while (1) {
modem_poll();
parse_uart();
send_mqtt();
check_inputs();
print_logs();
}Но с точки зрения RTOS он плохой:
- нет vTaskDelay();
- нет xQueueReceive(..., timeout);
- нет uart_read_bytes(..., timeout);
- нет yield;
- много логов;
- длинная критическая секция;
- ожидание модема сделано busy-wait.
5. Неправильное и правильное кормление
Плохо:
while (1) {
do_big_work();
esp_task_wdt_reset();
}Так можно кормить watchdog даже тогда, когда задача логически зависла. Лучше: Кормить watchdog только после успешного прохождения полезного цикла работы.
while (1) {
input_snapshot_t snapshot = {0};
bool ok = input_driver_get_snapshot(&snapshot);
if (ok) {
xQueueSend(input_queue, &snapshot, 0);
esp_task_wdt_reset();
}
vTaskDelay(pdMS_TO_TICKS(5));
}6. Heartbeat-архитектура
Промышленный вариант: задачи не кормят watchdog напрямую, а обновляют heartbeat.
typedef enum {
HEALTH_TASK_INPUT = 0,
HEALTH_TASK_PHASE,
HEALTH_TASK_MODEM,
HEALTH_TASK_TRANSPORT,
HEALTH_TASK_CLI,
HEALTH_TASK_COUNT
} health_task_id_t;
typedef struct {
int64_t last_ok_us;
uint32_t ok_count;
uint32_t error_count;
} health_task_state_t;
void health_report_ok(health_task_id_t id)
{
g_health[id].last_ok_us = esp_timer_get_time();
g_health[id].ok_count++;
}health_task:
void health_task(void *arg)
{
while (1) {
int64_t now = esp_timer_get_time();
bool all_critical_ok = true;
if (now - g_health[HEALTH_TASK_INPUT].last_ok_us > 100000) {
all_critical_ok = false;
}
if (now - g_health[HEALTH_TASK_MODEM].last_ok_us > 10000000) {
all_critical_ok = false;
}
if (all_critical_ok) {
esp_task_wdt_reset();
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}Смысл: Watchdog кормится только если критичные подсистемы реально живы.
7. Blocking operations
Плохо:
send_at("AT+QMTOPEN...");
while (!got_response) {
// ждём
}Лучше state machine:
state = MODEM_WAIT_QMTOPEN_RESPONSE
deadline = now + 60 секунд
while task runs:
- читаем UART с timeout
- парсим URC/ответы
- если ответ пришёл - следующий state
- если deadline прошёл - error/retry state
- задача регулярно отдаёт управлениеWatchdog не заменяет timeout. Нужны оба:
timeout AT-команды - штатное восстановление модема
watchdog - защита от ошибки архитектуры8. STM32: IWDG и WWDG
IWDG - Independent Watchdog
WWDG - Window WatchdogПрактически:
IWDG:
- последний рубеж защиты
- тактируется независимо
- после старта часто выключается только reset
WWDG:
- требует refresh в заданном временном окне
- ловит слишком поздний и иногда слишком ранний refreshАрхитектурный принцип одинаковый: Refresh только если ключевые подсистемы подтвердили здоровье.
9. Типичные ошибки
- кормить watchdog из timer interrupt;
- кормить watchdog в начале цикла;
- бесконечно ждать модем;
- использовать watchdog вместо нормальной обработки ошибок;
- не сохранять причину reset;
- ставить слишком маленький timeout.
10. Практическое задание
Создай WATCHDOG_POLICY.md:
# Watchdog policy
| Subsystem | Normal period | Max allowed silence | Recovery before WDT | Critical? |
|---|---:|---:|---|---|
| input_task | 5 ms | 100 ms | restart input driver / raise fault | yes |
| phase_task | event-driven | 500 ms if input active | clear queue / raise fault | yes |
| modem_task | event-driven | 10 s | AT retry, reconnect, modem reset | yes |
| transport_task | event-driven | 5 s | retry/degraded mode | yes |
| cli_task | event-driven | 60 s | not critical | no |
| telemetry_task | 10 s | 60 s | skip telemetry | no |
| health_task | 1 s | 3 s | WDT reset | yes |Добавь heartbeat API:
typedef enum {
HEALTH_ID_INPUT = 0,
HEALTH_ID_PHASE,
HEALTH_ID_MODEM,
HEALTH_ID_TRANSPORT,
HEALTH_ID_CLI,
HEALTH_ID_COUNT
} health_id_t;
void health_report_ok(health_id_t id);
void health_report_error(health_id_t id);
bool health_all_critical_ok(void);11. EC25 watchdog rule
Watchdog must not be the normal modem recovery mechanism.
Normal recovery order:
1. AT command timeout.
2. Retry command.
3. Reopen MQTT/TCP/UDP session.
4. Reinitialize PDP context.
5. Toggle modem PWRKEY/RESET.
6. Reboot ESP32 only if firmware health monitor fails.12. Короткий итог
Watchdog должен подтверждать здоровье системы, а не факт, что какой-то цикл всё ещё крутится.
input_task -> health_report_ok(INPUT)
phase_task -> health_report_ok(PHASE)
modem_task -> health_report_ok(MODEM)
transport_task -> health_report_ok(TRANSPORT)
health_task:
если все critical heartbeat свежие -> esp_task_wdt_reset()
иначе -> не кормить watchdog / raise fault / записать диагностикуЗадание
Для условной input_task нормальный период 5 мс, максимальная тишина 100 мс. Последний полезный heartbeat был в 1000 мс, сейчас 1101 мс. Решите, свежий ли heartbeat; опишите, когда обновлять heartbeat, какие данные сохранить и чего не доказывает работающий timer interrupt.
Критерии самопроверки: Вычислите 101 мс, сравните с 100 мс, свяжите heartbeat с полезной работой и назовите диагностические данные.
Показать ответ автора
Тишина 1101−1000=101 мс, что больше 100 мс: heartbeat просрочен. Обновляйте его после подтверждённой полезной работы, а не при любом вращении цикла. Сохраните ID подсистемы, время последнего успеха, ошибку/счётчики и причину reset при его наступлении. Работа таймерного прерывания сама по себе не доказывает, что критичные задачи выполняют обязанности. Политика fault/recovery и конфигурация TWDT определяются отдельно; пример — вычисление, не аппаратный тест.