1. Тема дня

Watchdog в ESP32/ESP-IDF и STM32: зачем он нужен, какие бывают виды, почему он срабатывает, как правильно “кормить” watchdog и почему плохая идея вставлять esp_task_wdt_reset() куда попало. Главная мысль: Watchdog - не костыль от зависаний, а диагностический механизм, который показывает, что архитектура задачи нарушена.

2. Зачем это нужно

Опасные места:

text
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

Упрощённо:

text
Interrupt Watchdog / IWDT
  Следит, что система не застряла с отключенными прерываниями
  или в слишком длинной критической секции.
Task Watchdog / TWDT
  Следит, что задачи не захватили CPU слишком надолго
  и что idle task получает время на выполнение.

Если high-priority задача крутится бесконечно без yield/blocking wait, idle task не выполняется, и TWDT это обнаружит.

4. Почему watchdog срабатывает на “рабочем” коде

Код может вроде работать:

c
while (1) {
    modem_poll();
    parse_uart();
    send_mqtt();
    check_inputs();
    print_logs();
}

Но с точки зрения RTOS он плохой:

  • нет vTaskDelay();
  • нет xQueueReceive(..., timeout);
  • нет uart_read_bytes(..., timeout);
  • нет yield;
  • много логов;
  • длинная критическая секция;
  • ожидание модема сделано busy-wait.

5. Неправильное и правильное кормление

Плохо:

c
while (1) {
    do_big_work();
    esp_task_wdt_reset();
}

Так можно кормить watchdog даже тогда, когда задача логически зависла. Лучше: Кормить watchdog только после успешного прохождения полезного цикла работы.

c
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.

c
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:

c
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

Плохо:

c
send_at("AT+QMTOPEN...");
while (!got_response) {
    // ждём
}

Лучше state machine:

text
state = MODEM_WAIT_QMTOPEN_RESPONSE
deadline = now + 60 секунд
while task runs:
  - читаем UART с timeout
  - парсим URC/ответы
  - если ответ пришёл - следующий state
  - если deadline прошёл - error/retry state
  - задача регулярно отдаёт управление

Watchdog не заменяет timeout. Нужны оба:

text
timeout AT-команды - штатное восстановление модема
watchdog - защита от ошибки архитектуры

8. STM32: IWDG и WWDG

text
IWDG - Independent Watchdog
WWDG - Window Watchdog

Практически:

text
IWDG:
  - последний рубеж защиты
  - тактируется независимо
  - после старта часто выключается только reset
WWDG:
  - требует refresh в заданном временном окне
  - ловит слишком поздний и иногда слишком ранний refresh

Архитектурный принцип одинаковый: Refresh только если ключевые подсистемы подтвердили здоровье.

9. Типичные ошибки

  • кормить watchdog из timer interrupt;
  • кормить watchdog в начале цикла;
  • бесконечно ждать модем;
  • использовать watchdog вместо нормальной обработки ошибок;
  • не сохранять причину reset;
  • ставить слишком маленький timeout.

10. Практическое задание

Создай WATCHDOG_POLICY.md:

markdown
# 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:

c
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

text
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 должен подтверждать здоровье системы, а не факт, что какой-то цикл всё ещё крутится.

text
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 / записать диагностику
Модем не ответил на AT-команду в установленный срок, но остальные подсистемы здоровы. Какое действие относится к штатному восстановлению?

Задание

Для условной input_task нормальный период 5 мс, максимальная тишина 100 мс. Последний полезный heartbeat был в 1000 мс, сейчас 1101 мс. Решите, свежий ли heartbeat; опишите, когда обновлять heartbeat, какие данные сохранить и чего не доказывает работающий timer interrupt.

Критерии самопроверки: Вычислите 101 мс, сравните с 100 мс, свяжите heartbeat с полезной работой и назовите диагностические данные.

Показать ответ автора

Тишина 1101−1000=101 мс, что больше 100 мс: heartbeat просрочен. Обновляйте его после подтверждённой полезной работы, а не при любом вращении цикла. Сохраните ID подсистемы, время последнего успеха, ошибку/счётчики и причину reset при его наступлении. Работа таймерного прерывания сама по себе не доказывает, что критичные задачи выполняют обязанности. Политика fault/recovery и конфигурация TWDT определяются отдельно; пример — вычисление, не аппаратный тест.