3、软件看门狗与任务监控:基于FreeRTOS的任务看门狗,任务执行时间监控,死锁与卡死检测

各位工程师朋友,咱们接着聊。上一章讲了硬件看门狗,那是系统最后一道防线。但说实话,硬件看门狗有个硬伤——它只能检测整个系统有没有“喂狗”,至于哪个任务卡死了、哪个函数跑飞了,它根本不知道。

所以,真正要解决工业现场的疑难杂症,还得靠软件看门狗。说白了,就是给每个关键任务配一个“专属保镖”。今天我就把我在FreeRTOS上折腾多年的经验,一次性倒给你们。

3.1 为什么硬件看门狗不够用?

我遇到过这样一个案例:某款PLC控制器,偶尔会出现输出继电器不动作。硬件看门狗没复位,系统看起来一切正常。查了三天,最后发现是一个优先级较低的任务,因为某个信号量没释放,卡死在了那里。

硬件看门狗只会在主循环或空闲任务里喂狗。如果高优先级任务正常,低优先级任务卡死,狗照样喂得欢。你想想看,这多坑人?

所以,我们需要一个能监控每个任务运行状态的机制。这就是软件看门狗的核心思想。

3.2 基于FreeRTOS的任务看门狗设计

我的做法很简单:给每个关键任务分配一个“心跳计数器”。任务正常运行时,定期更新这个计数器。然后由一个监控任务(优先级最高)去检查这些计数器有没有“过期”。

先看数据结构:

/* 任务看门狗控制块 */
typedef struct {
    TaskHandle_t    task_handle;        /* 任务句柄 */
    const char*     task_name;          /* 任务名称 */
    uint32_t        heartbeat;          /* 当前心跳值 */
    uint32_t        last_heartbeat;     /* 上次检查时的心跳值 */
    uint32_t        timeout_ms;         /* 超时时间(ms) */
    uint32_t        check_count;        /* 连续超时次数 */
    uint32_t        max_failures;       /* 允许的最大连续失败次数 */
    void          (*on_failure)(void*); /* 故障处理回调 */
} TaskWatchdog_t;

每个任务在它的主循环里,调用一个“喂狗”函数:

/* 任务喂狗函数 */
void TaskWD_Feed(const char* task_name) {
    TaskWatchdog_t* wd = FindWatchdogByName(task_name);
    if (wd) {
        wd->heartbeat++;
        /* 这里用原子操作,防止中断干扰 */
        __disable_irq();
        wd->last_heartbeat = wd->heartbeat;
        __enable_irq();
    }
}

嗯,这里要注意:喂狗操作一定要快,不能在里面做复杂计算。我见过有人把喂狗函数里塞了个printf,结果串口阻塞,反而导致看门狗误报。

3.3 任务执行时间监控

光知道任务有没有跑还不够,还得知道它跑了多久。有些任务平时正常,但遇到极端数据时,执行时间会暴涨,拖累整个系统。

我个人习惯用FreeRTOS的uxTaskGetSystemState()来获取任务运行时间。但更精确的做法,是在任务入口和出口打时间戳:

/* 任务执行时间测量宏 */
#define TASK_EXEC_TIME_START() \
    uint32_t __task_start_tick = xTaskGetTickCount()

#define TASK_EXEC_TIME_END(task_name, max_time_ms) \
    do { \
        uint32_t __elapsed = xTaskGetTickCount() - __task_start_tick; \
        if (__elapsed > pdMS_TO_TICKS(max_time_ms)) { \
            /* 记录超时日志 */ \
            Log_Error("Task %s exec timeout: %d ms", task_name, \
                      __elapsed * portTICK_PERIOD_MS); \
        } \
    } while(0)

使用方法:

void vCommunicationTask(void* pvParameters) {
    while(1) {
        TASK_EXEC_TIME_START();

        /* 实际业务逻辑 */
        ProcessIncomingData();
        SendResponse();

        TASK_EXEC_TIME_END("CommTask", 50);  /* 允许最大50ms */
        TaskWD_Feed("CommTask");
    }
}

我曾经在一个项目中,发现某个任务偶尔会跑到200ms,而设计指标是20ms。查到最后,是底层驱动里有个while循环在等硬件标志位,没加超时保护。这种问题,没有执行时间监控,根本抓不到。

3.4 死锁与卡死检测

死锁是嵌入式开发中最头疼的问题之一。两个任务互相等待对方释放资源,谁也动不了。FreeRTOS本身不提供死锁检测,但我们可以自己实现一个简单的方案。

我的思路是这样的:监控任务定期检查每个任务的“阻塞状态”。如果一个任务长时间处于“阻塞”状态,且没有正常切换,就怀疑它死锁了。

具体实现:

/* 死锁检测任务 */
void vDeadLockDetectTask(void* pvParameters) {
    TaskWatchdog_t* wd_list = (TaskWatchdog_t*)pvParameters;
    uint32_t wd_count = GetWatchdogCount();

    while(1) {
        for (int i = 0; i < wd_count; i++) {
            TaskWatchdog_t* wd = &wd_list[i];
            eTaskState state = eTaskGetState(wd->task_handle);

            /* 检查心跳是否更新 */
            if (wd->heartbeat == wd->last_heartbeat) {
                wd->check_count++;

                if (wd->check_count >= wd->max_failures) {
                    /* 任务卡死!执行故障处理 */
                    Log_Fatal("Task %s stuck! State: %d, Heartbeat: %lu",
                              wd->task_name, state, wd->heartbeat);

                    if (wd->on_failure) {
                        wd->on_failure(wd->task_handle);
                    }
                }
            } else {
                /* 任务正常,清零计数 */
                wd->check_count = 0;
                wd->last_heartbeat = wd->heartbeat;
            }
        }

        /* 每100ms检查一次 */
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}
⚠️ 重要提醒: 监控任务本身也要被监控!我建议把监控任务的优先级设为最高,并且由硬件看门狗来兜底。万一监控任务自己挂了,硬件看门狗还能拉一把。

3.5 故障处理策略

检测到任务卡死后,怎么处理?我总结了三种策略:

策略 方法 适用场景
温和恢复 删除并重建任务 非关键任务,允许短暂中断
强制恢复 调用vTaskDelete()后重新创建 关键任务,但允许重启
系统复位 触发硬件看门狗或NVIC_SystemReset() 安全关键任务,必须保证绝对可靠

我个人推荐“温和恢复”作为首选。因为很多卡死是偶发的,删除重建任务就能解决,没必要大动干戈。

💡 实战技巧: 在故障处理回调函数里,先保存现场信息(任务栈内容、寄存器值、当前消息队列状态),然后再做恢复操作。这些数据对后续分析问题原因至关重要。我习惯用一个小型的环形缓冲区来保存最近的故障记录。

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 喂狗位置不对:我曾经把喂狗放在任务循环的开头,结果任务在中间卡死,狗照样喂。正确的做法是放在任务循环的末尾,或者关键操作之后。
  • 超时时间设置太紧:系统负载波动时,任务执行时间会有变化。建议把超时时间设为正常执行时间的2-3倍,避免误报。
  • 忘记考虑中断:如果任务因为等待中断信号而阻塞,看门狗会误判。我的做法是:在进入阻塞等待前,先喂一次狗,告诉监控“我是主动等的,不是卡死的”。

好了,这一章的内容就到这里。软件看门狗这东西,说白了就是给系统装了个“心电图机”。平时看着没啥用,但真出问题的时候,它能帮你快速定位到是哪个任务、哪段代码出了问题。下一章,咱们聊聊如何把这些监控数据记录下来,形成故障日志。