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倍,避免误报。
- 忘记考虑中断:如果任务因为等待中断信号而阻塞,看门狗会误判。我的做法是:在进入阻塞等待前,先喂一次狗,告诉监控“我是主动等的,不是卡死的”。
好了,这一章的内容就到这里。软件看门狗这东西,说白了就是给系统装了个“心电图机”。平时看着没啥用,但真出问题的时候,它能帮你快速定位到是哪个任务、哪段代码出了问题。下一章,咱们聊聊如何把这些监控数据记录下来,形成故障日志。