一、问题背景:为什么高优先级任务卡死更危险

在RTOS中,高优先级任务一旦进入死循环或永久阻塞,会抢占CPU或占用关键资源,导致低优先级任务饿死,系统看似运行实则瘫痪。硬件看门狗(IWDG)只能检测主循环是否喂狗,若高优先级任务卡死但中断仍正常,硬件看门狗可能被中断服务程序误喂,从而掩盖故障。因此,必须引入任务级监控,在软件层面检测每个任务的运行状态。

二、检测原理:心跳表与软件看门狗

核心思想:每个任务周期性更新自己的“心跳”时间戳,监控任务(通常为低优先级或专用任务)定期检查所有心跳,若某个任务超过阈值未更新,则判定为卡死。

  • 心跳表:全局数组,每个任务对应一个last_heartbeat变量,记录上次正常运行的Tick值。
  • 监控任务:以固定周期(如100ms)扫描心跳表,计算当前Tick与last_heartbeat的差值,若大于max_idle_ticks(如500ms),则触发恢复策略。
  • 喂狗操作:每个任务在关键循环或阻塞等待后更新自己的心跳,注意避免在中断中更新(除非使用中断安全版本)。

三、FreeRTOS实现步骤

1. 定义心跳表结构

#include "FreeRTOS.h"
#include "task.h"

#define MAX_TASKS 10
#define HEARTBEAT_TIMEOUT_MS 500
#define MONITOR_PERIOD_MS 100

typedef struct {
    TaskHandle_t handle;
    TickType_t last_heartbeat;
    const char *name;
} HeartbeatEntry;

static HeartbeatEntry heartbeat_table[MAX_TASKS];
static int num_tasks = 0;

2. 注册任务与更新心跳

void TaskMonitor_Register(TaskHandle_t handle, const char *name) {
    if (num_tasks < MAX_TASKS) {
        heartbeat_table[num_tasks].handle = handle;
        heartbeat_table[num_tasks].name = name;
        heartbeat_table[num_tasks].last_heartbeat = xTaskGetTickCount();
        num_tasks++;
    }
}

void TaskMonitor_Heartbeat(TaskHandle_t handle) {
    for (int i = 0; i < num_tasks; i++) {
        if (heartbeat_table[i].handle == handle) {
            heartbeat_table[i].last_heartbeat = xTaskGetTickCount();
            break;
        }
    }
}

3. 监控任务实现

void vTaskMonitor(void *pvParameters) {
    TickType_t last_wake = xTaskGetTickCount();
    const TickType_t period = pdMS_TO_TICKS(MONITOR_PERIOD_MS);
    
    for (;;) {
        vTaskDelayUntil(&last_wake, period);
        
        TickType_t now = xTaskGetTickCount();
        for (int i = 0; i < num_tasks; i++) {
            TickType_t diff = now - heartbeat_table[i].last_heartbeat;
            if (diff > pdMS_TO_TICKS(HEARTBEAT_TIMEOUT_MS)) {
                // 任务卡死,执行恢复策略
                configASSERT(0); // 或调用系统复位、任务删除等
                // 示例:打印错误并复位
                printf("Task %s stuck!\n", heartbeat_table[i].name);
                NVIC_SystemReset();
            }
        }
    }
}

4. 在任务中调用心跳更新

以高优先级任务为例,在其主循环或关键点调用:

void vHighPriorityTask(void *pvParameters) {
    TaskMonitor_Register(xTaskGetCurrentTaskHandle(), "HighTask");
    
    for (;;) {
        // 执行任务工作
        // ...
        
        // 更新心跳,放在循环末尾或阻塞等待前
        TaskMonitor_Heartbeat(xTaskGetCurrentTaskHandle());
        
        // 可能阻塞等待事件,但阻塞时间不能超过超时阈值
        if (xQueueReceive(xQueue, &data, pdMS_TO_TICKS(200)) == pdTRUE) {
            // 处理数据
        }
        // 注意:阻塞时间需小于HEARTBEAT_TIMEOUT_MS,否则会被误判
    }
}

5. 创建监控任务

void App_Init(void) {
    xTaskCreate(vTaskMonitor, "Monitor", 256, NULL, 1, NULL); // 低优先级
    // 创建其他任务...
}

四、关键注意事项

  • 优先级设置:监控任务应设为最低优先级,避免影响实时性,但需确保其能周期性运行。若所有任务都卡死,监控任务也无法运行,此时需依赖硬件看门狗兜底。
  • 阻塞时间控制:任务在阻塞等待(如队列、信号量)时,若阻塞时间超过超时阈值,会被误判。解决方案:将阻塞时间拆分为多个小段,每段后更新心跳;或使用xTaskGetTickCount在阻塞前后计算差值,但更推荐前者。
  • 中断安全:若在中断中更新心跳,需使用portYIELD_FROM_ISR或临界区保护,但通常不建议,因为中断频繁会干扰监控逻辑。
  • 时间戳溢出:FreeRTOS的TickCount是32位无符号,溢出周期约49天(1ms tick),差值计算采用无符号减法,自动处理溢出,无需额外处理。
  • 恢复策略:检测到卡死后,可采取:
    • 系统复位(最安全)
    • 删除卡死任务并重建(需确保资源释放)
    • 记录日志后进入安全模式
  • 与硬件看门狗协同:软件看门狗负责定位,硬件看门狗负责最终兜底。监控任务每次正常扫描后喂硬件看门狗,若监控任务本身卡死,硬件看门狗将复位系统。

五、扩展:优先级反转与监控盲区

高优先级任务卡死可能由低优先级任务持有资源导致(优先级反转)。监控任务无法直接检测,但可通过心跳表发现高优先级任务长时间未更新,从而触发恢复。另外,若监控任务被更高优先级任务抢占,可能导致监控延迟,因此监控任务优先级应低于所有被监控任务,但高于空闲任务。

六、总结

基于心跳表的软件看门狗是RTOS任务监控的有效手段,能快速定位卡死任务,配合硬件看门狗形成双重保障。实际工程中需根据任务实时性要求调整超时阈值,并充分测试阻塞场景,避免误报。该机制已在多个工业控制项目中验证,显著提升了系统可靠性。