为什么需要多任务看门狗

传统的看门狗只管”主循环有没有在跑”。但在多线程 RTOS 里,主循环早就不存在了——每个线程有自己的循环。如果只在某个线程里喂狗,其它线程死锁了完全检测不到。

PipeMonitor 需要一种机制:任一关键线程卡死,整个系统自动复位

设计思路

不直接在业务线程里喂硬件看门狗,而是引入一个 看门狗监督线程,它周期性检查所有关键线程的”心跳”:

  1. 每个业务线程在每轮循环结束时调用 app_watchdog_mark_alive(TASK_ID)
  2. 监督线程每秒检查一次:如果某个线程超过指定时间没上报,就认为它卡死了
  3. 只有所有线程都健康时才喂硬件 IWDG;否则让 IWDG 超时复位
static rt_bool_t app_watchdog_all_healthy(void)
{
    rt_tick_t now = rt_tick_get();

    for (rt_uint32_t i = 0; i < APP_WATCHDOG_TASK_COUNT; i++)
    {
        rt_tick_t timeout = rt_tick_from_millisecond(g_task_cfg[i].timeout_ms);
        if ((rt_int32_t)(now - g_last_alive[i]) > (rt_int32_t)timeout)
        {
            rt_kprintf("[WDG] %s stale, wait for IWDG reset\n", g_task_cfg[i].name);
            return RT_FALSE;
        }
    }
    return RT_TRUE;
}

各线程的超时阈值

不同线程的工作周期不同,超时阈值也不同:

线程超时理由
measure20s采集周期 2s,留 10 倍余量应对 Modbus 偶发超时
display3sLCD 刷新周期 200ms,3s 没响应基本是卡死了
heartbeat3s心跳周期 500ms,3s 无响应说明调度器挂了
uplink20s上行等待云端 ACK 最长 8s,加上补传可能更久
static const app_watchdog_task_cfg_t g_task_cfg[APP_WATCHDOG_TASK_COUNT] =
{
    { "measure",   20000U },
    { "display",    3000U },
    { "heartbeat",  3000U },
    { "uplink",    20000U },
};

IWDG 配置

硬件看门狗使用 STM32H562 的 IWDG,时钟源 LSI 32kHz:

/* IWDG 使用 LSI 32kHz:1250 * 256 / 32000 = 10s */
#define APP_IWDG_PRESCALER   IWDG_PRESCALER_256
#define APP_IWDG_RELOAD      1249U

超时时间 = (1249+1) * 256 / 32000 = 10 秒。监督线程每秒检查一次,所以最坏情况下系统在检测到故障后 10 秒内复位。

复位原因记录

系统启动时读取 RCC 复位标志,打印到串口方便排查:

void app_watchdog_print_reset_reason(void)
{
    rt_kprintf("[WDG] reset reason:");
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST))  rt_kprintf(" IWDG");
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_WWDGRST))  rt_kprintf(" WWDG");
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_SFTRST))   rt_kprintf(" SW");
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_PINRST))   rt_kprintf(" PIN");
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_BORRST))   rt_kprintf(" BOR");
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_LPWRRST))  rt_kprintf(" LPWR");
    rt_kprintf("\n");
    __HAL_RCC_CLEAR_RESET_FLAGS();
}

同时会上报一条 MCU_RESTART 报警事件到云端,让运维知道设备曾经重启过。

看门狗的启动时机

看门狗不是一开机就启动的。在 app_init 线程里,所有业务线程都跑起来之后才启动:

rt_uint32_t watchdog_mask = APP_WATCHDOG_MASK_MEASURE |
                            APP_WATCHDOG_MASK_DISPLAY |
                            APP_WATCHDOG_MASK_HEARTBEAT;

/* 上行服务启动成功后才监控它 */
if (uplink_service_start() == RT_EOK)
    watchdog_mask |= APP_WATCHDOG_MASK_UPLINK;

app_watchdog_start(watchdog_mask);

这样避免了初始化阶段还没来得及上报就被误判为卡死。

故障场景分析

场景 1:Modbus 总线死锁

采集线程卡在 rt_mutex_take 等待串口锁。20 秒后看门狗检测到 measure 超时,停止喂 IWDG,系统自动复位。

场景 2:LCD FMC 总线异常

显示线程卡在 lcd_fill 等待 FSMC 应答。3 秒后看门狗检测到 display 超时,系统复位。

场景 3:云端长时间无响应

上行线程在 wait_cloud_ack 里等待 8 秒超时,然后继续运行。只要它还在调用 mark_alive,看门狗就不会误判。只有真正卡死(比如串口驱动死锁)才会触发复位。

场景 4:栈溢出

RT-Thread 的 RT_USING_OVERFLOW_CHECK 会在栈底放置哨兵值。上下文切换时如果检测到哨兵被踩,打印 stack overflow 并触发 HardFault。看门狗此时已经停止喂狗,10 秒后 IWDG 复位。

为什么不直接喂 IWDG

如果每个线程都直接调用 HAL_IWDG_Refresh,那只要有一个线程还活着,IWDG 就不会超时——完全失去了监控其它线程的能力。

分离”心跳上报”和”硬件喂狗”两层,才能实现”任一线程卡死就复位”的需求。

小结

看门狗的设计哲学是:信任但验证。每个线程按自己的节奏正常运行时上报心跳;监督线程只在异常时介入。这样既不会因为误判频繁复位,又能在真正故障时快速恢复。

对于工业现场来说,“自动复位恢复”比”卡死等人工干预”靠谱得多。

后续阅读