4、软件看门狗实现:任务级看门狗、中断级看门狗、基于时间戳的监控、心跳机制设计

硬件看门狗虽然可靠,但有个硬伤——它只能检测整个系统有没有「死透」。如果CPU还在跑,只是某个关键任务卡住了,硬件看门狗是喂不死的。这时候,就得靠软件看门狗上场了。

我个人习惯把软件看门狗分成四类:任务级、中断级、时间戳监控、心跳机制。它们各有各的适用场景,也各有各的坑。下面我一个一个说。

4.1 任务级看门狗

任务级看门狗,说白了就是监控每个任务是否按时完成了它的「规定动作」。比如一个数据采集任务,要求每10ms采集一次。如果超过15ms还没采,那肯定出问题了。

实现思路其实很简单:每个任务在关键节点「打卡」,一个监控任务定期检查这些打卡记录。

核心思想:每个任务在完成关键操作后,更新一个共享的时间戳或计数器。监控任务检查这些值是否在预期时间内被更新。

我在项目中遇到过这样一个案例:一个电机控制任务,要求每5ms执行一次PID计算。结果有一次因为中断频繁,任务被延迟了20ms才跑完。电机当场就抖起来了。后来加了任务级看门狗,一旦检测到超时,立刻切换到安全模式。

/* 任务级看门狗示例 */
typedef struct {
    TaskHandle_t taskHandle;
    uint32_t lastFeedTime;
    uint32_t maxInterval;  /* 最大允许间隔,单位ms */
    uint8_t  isAlive;
} TaskWatchdog_t;

TaskWatchdog_t wdTasks[MAX_TASKS];

void Task_FeedWatchdog(int taskId) {
    wdTasks[taskId].lastFeedTime = GetSysTick();
    wdTasks[taskId].isAlive = 1;
}

void Task_WatchdogMonitor(void *param) {
    while(1) {
        for(int i = 0; i < MAX_TASKS; i++) {
            uint32_t now = GetSysTick();
            uint32_t elapsed = now - wdTasks[i].lastFeedTime;
            
            if(elapsed > wdTasks[i].maxInterval) {
                /* 任务超时!执行恢复 */
                Task_Recovery(i);
            }
        }
        vTaskDelay(pdMS_TO_TICKS(100));  /* 每100ms检查一次 */
    }
}

注意:监控任务的优先级不能太低。我曾经见过一个系统,监控任务被其他高优先级任务饿死了,结果所有任务都超时了也没人管。监控任务本身也需要被监控,这是个递归问题。

4.2 中断级看门狗

中断级看门狗,用来监控中断服务程序是否正常执行。你想想看,如果某个中断被意外关闭了,或者中断里死循环了,系统会怎么样?

实现方法:在中断服务程序中「喂狗」,在另一个地方(比如一个高优先级定时器中断)检查。

嗯,这里要注意:中断级看门狗不能放在同一个中断里检查自己,那没意义。得用两个不同的中断源互相监控。

/* 中断级看门狗示例 */
volatile uint32_t isrWatchdogCount = 0;

/* 被监控的中断服务程序 */
void TIM2_IRQHandler(void) {
    /* 清除中断标志 */
    TIM2->SR = 0;
    
    /* 执行关键操作 */
    DoCriticalWork();
    
    /* 喂狗 */
    isrWatchdogCount++;
}

/* 监控中断(更高优先级) */
void TIM3_IRQHandler(void) {
    static uint32_t lastCount = 0;
    
    TIM3->SR = 0;
    
    if(isrWatchdogCount == lastCount) {
        /* TIM2中断没有执行!执行恢复 */
        System_Recovery();
    }
    lastCount = isrWatchdogCount;
}

避坑指南:我曾经把监控中断的优先级设得和被监控中断一样高,结果两个中断互相抢占,计数器更新乱套了。记住:监控中断的优先级一定要高于被监控中断。

4.3 基于时间戳的监控

时间戳监控,其实是一种更精细化的看门狗。它不只看任务有没有跑,还看任务跑得「对不对」。比如一个通信任务,不仅要检查它有没有接收数据,还要检查接收的数据是不是在合理的时间范围内。

实现方式:每个关键操作都记录一个时间戳,监控逻辑检查这些时间戳的「合理性」。

监控对象 时间戳记录点 合理范围 超时处理
CAN通信接收 收到一帧数据时 10ms内应有新帧 重启CAN控制器
传感器采样 采样完成时 5ms内完成 重新初始化传感器
看门狗喂食 喂狗操作时 50ms内喂一次 系统复位

为什么会这样?因为很多故障不是「死机」,而是「慢死」。系统还在跑,但响应越来越慢,最后超时。时间戳监控能提前发现这种「慢性死亡」。

/* 时间戳监控示例 */
typedef struct {
    uint32_t lastTimestamp;
    uint32_t expectedInterval;
    char     name[16];
} TimestampMonitor_t;

int CheckTimestamp(TimestampMonitor_t *mon) {
    uint32_t now = GetSysTick();
    uint32_t diff = now - mon->lastTimestamp;
    
    if(diff > mon->expectedInterval * 2) {
        /* 严重超时 */
        Log_Error("%s 严重超时: %d ms", mon->name, diff);
        return -2;
    } else if(diff > mon->expectedInterval) {
        /* 轻微超时,记录警告 */
        Log_Warning("%s 轻微超时: %d ms", mon->name, diff);
        return -1;
    }
    return 0;
}

4.4 心跳机制设计

心跳机制,是分布式系统里最常用的健康检查手段。每个节点定期发送一个「我还活着」的信号,监控节点如果收不到,就认为这个节点挂了。

在单芯片系统里,心跳机制同样有用。比如一个系统有多个核心模块,每个模块都发心跳,一个中心监控模块统一管理。

我个人习惯把心跳分成三级:

  • 一级心跳(快速):每1ms发送,用于中断级监控
  • 二级心跳(正常):每10ms发送,用于任务级监控
  • 三级心跳(慢速):每100ms发送,用于模块间通信

设计要点:心跳信号不能太频繁,否则浪费CPU;也不能太稀疏,否则故障检测延迟太大。一般取任务周期的2-3倍作为心跳间隔。

/* 心跳机制示例 */
typedef struct {
    uint32_t heartbeatId;
    uint32_t lastBeatTime;
    uint32_t beatInterval;
    uint8_t  missedBeats;
    uint8_t  maxMissedBeats;  /* 允许丢失的心跳数 */
} Heartbeat_t;

void Heartbeat_Send(Heartbeat_t *hb) {
    hb->lastBeatTime = GetSysTick();
    hb->missedBeats = 0;
}

int Heartbeat_Check(Heartbeat_t *hb) {
    uint32_t now = GetSysTick();
    
    if((now - hb->lastBeatTime) > hb->beatInterval) {
        hb->missedBeats++;
        
        if(hb->missedBeats >= hb->maxMissedBeats) {
            /* 节点失联!执行恢复 */
            return -1;  /* 节点死亡 */
        }
        return 1;  /* 丢了一拍,但还能忍 */
    }
    return 0;  /* 正常 */
}

/* 使用示例 */
void ModuleA_Task(void *param) {
    Heartbeat_t hb = {
        .beatInterval = 10,    /* 10ms发一次心跳 */
        .maxMissedBeats = 3,   /* 允许连续丢3次 */
        .missedBeats = 0
    };
    
    while(1) {
        /* 执行模块A的工作 */
        DoModuleAWork();
        
        /* 发送心跳 */
        Heartbeat_Send(&hb);
        
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

我曾经踩过的坑:心跳机制里有个经典问题——「心跳风暴」。当系统负载高时,心跳任务本身可能被延迟,导致误报。后来我加了个「容忍次数」的机制,允许连续丢失2-3次心跳才判定故障。这个缓冲很重要。

最后说一句:软件看门狗不是万能的。它依赖CPU和操作系统本身正常工作。如果系统已经严重损坏(比如栈溢出导致PC指针乱飞),软件看门狗也救不了。这时候还得靠硬件看门狗兜底。两者配合使用,才是王道。