第三章:软件看门狗实现

大家好,我是你们的老朋友。今天咱们聊聊软件看门狗。说实话,很多工程师一听到「看门狗」三个字,第一反应就是硬件看门狗——外挂一个芯片,喂狗信号丢了就复位。但实际项目中,硬件看门狗有时候真不够用。

为什么?我举个例子。你想想看,硬件看门狗只能检测到「CPU完全死掉」这种极端情况。如果程序跑飞了,但定时器中断还在正常触发,硬件看门狗根本不会复位。这时候,软件看门狗就派上用场了。

3.1 软件模拟看门狗的原理

软件看门狗,说白了就是「自己监控自己」。它不依赖外部硬件,而是利用软件逻辑来判断系统是否还在正常工作。

核心思路很简单:

  • 设定一个「心跳计数器」
  • 主循环或关键任务定期「喂狗」——把计数器清零
  • 如果计数器超时没被清零,说明系统卡住了
  • 触发复位或错误处理

嗯,这里要注意:软件看门狗最大的问题在于——如果CPU完全死掉,连定时器中断都进不去,那软件看门狗也救不了你。所以它和硬件看门狗是互补关系,不是替代关系。

重要概念:软件看门狗检测的是「逻辑死锁」,硬件看门狗检测的是「物理死机」。两者结合才是王道。

我在项目中遇到过一种情况:系统跑着跑着,某个任务卡在while循环里出不来,但中断还能正常响应。硬件看门狗一直喂狗成功,系统就是不干活。后来加了软件看门狗,才把这个问题揪出来。

3.2 基于定时器的软件看门狗

最常用的实现方式,就是利用一个硬件定时器来做「超时监控」。具体怎么做?我给大家画个流程图:

// 伪代码示例:基于定时器的软件看门狗
// 假设我们有一个1ms中断的定时器

uint32_t g_watchdog_counter = 0;
uint32_t g_watchdog_timeout = 1000;  // 1000ms超时

// 定时器中断服务函数(每1ms执行一次)
void Timer_ISR(void)
{
    g_watchdog_counter++;
    
    // 检查是否超时
    if (g_watchdog_counter > g_watchdog_timeout)
    {
        // 超时了!系统可能卡住了
        System_Reset();  // 软件复位
    }
}

// 喂狗函数——主循环或关键任务调用
void Watchdog_Feed(void)
{
    g_watchdog_counter = 0;  // 清零计数器
}

// 主循环
void main(void)
{
    while(1)
    {
        // 执行各种任务...
        Do_Task_A();
        Do_Task_B();
        
        // 喂狗——告诉系统我还活着
        Watchdog_Feed();
    }
}

你看,代码其实很简单。但有几个坑,我踩过,你们别踩:

避坑指南:

  • 喂狗位置要选对:我曾经把喂狗放在中断里,结果主循环卡死了,中断还在喂狗,软件看门狗永远不触发。正确做法:喂狗必须放在主循环或关键任务中。
  • 超时时间要合理:设太短,正常执行也会误触发;设太长,死锁了要等半天才复位。一般建议是正常执行周期的2-3倍。
  • 计数器溢出问题:如果定时器中断频率很高,计数器可能溢出。建议用32位变量,或者定期清零。

3.3 多任务环境下的看门狗策略

到了RTOS(实时操作系统)环境下,事情就复杂了。你想想看,一个系统里有好几个任务,每个任务执行时间不一样,有的快有的慢。怎么判断系统整体是否健康?

我常用的策略有三种:

策略名称 原理 适用场景 缺点
单一喂狗点 只在空闲任务中喂狗 任务简单,优先级固定 高优先级任务死锁检测不到
多级看门狗 每个任务有自己的超时计数器 任务较多,要求高可靠性 实现复杂,占用资源多
心跳任务法 创建一个高优先级心跳任务,定期喂狗 大多数RTOS项目 心跳任务本身可能死锁

我个人习惯用「多级看门狗」策略。具体做法是这样的:

// 多级看门狗实现示例
#define TASK_NUM  5  // 假设有5个任务

typedef struct {
    uint32_t last_feed_time;  // 上次喂狗时间
    uint32_t timeout;         // 超时阈值
    uint8_t  is_alive;        // 是否还活着
} TaskWatchdog_t;

TaskWatchdog_t g_task_watchdogs[TASK_NUM];

// 每个任务在自己的循环里调用这个函数
void Task_Feed(uint8_t task_id)
{
    g_task_watchdogs[task_id].last_feed_time = Get_Current_Tick();
    g_task_watchdogs[task_id].is_alive = 1;
}

// 监控任务——定期检查所有任务的状态
void Monitor_Task(void)
{
    while(1)
    {
        for (int i = 0; i < TASK_NUM; i++)
        {
            uint32_t elapsed = Get_Current_Tick() - g_task_watchdogs[i].last_feed_time;
            
            if (elapsed > g_task_watchdogs[i].timeout)
            {
                // 任务i超时了!
                Log_Error("Task %d is dead!", i);
                System_Reset();
            }
        }
        
        Delay(100);  // 每100ms检查一次
    }
}

我的经验:多级看门狗里,监控任务本身也要被监控。我一般会再设一个硬件看门狗,专门监控监控任务。这样形成「监控链」,谁也别想偷懒。

还有一个细节:在多任务环境下,喂狗操作要考虑互斥。如果两个任务同时喂狗,可能会造成数据竞争。我建议用关中断或者信号量来保护喂狗操作。

嗯,说到这,我想起一个真实案例。之前做一个医疗设备,用了FreeRTOS,有6个任务。一开始只用了一个看门狗,结果某个低优先级任务卡住了,高优先级任务还在正常运行,看门狗一直没触发。后来改成多级看门狗,每个任务独立监控,问题才解决。

你想想看,医疗设备出问题可不是闹着玩的。从那以后,我对看门狗的设计就特别较真。

小结

软件看门狗这东西,说难不难,说简单也不简单。核心就三点:

  • 原理:用计数器模拟超时检测
  • 实现:基于定时器,注意喂狗位置
  • 多任务:用多级看门狗策略,别偷懒

下一章咱们聊聊硬件看门狗的设计,到时候再对比一下两者的优缺点。今天就到这,大家回去可以试试在自己的项目里加个软件看门狗,看看能不能抓到一些隐藏的bug。

一句话总结:软件看门狗不是万能的,但没有软件看门狗是万万不能的。