第三章:软件看门狗实现
大家好,我是你们的老朋友。今天咱们聊聊软件看门狗。说实话,很多工程师一听到「看门狗」三个字,第一反应就是硬件看门狗——外挂一个芯片,喂狗信号丢了就复位。但实际项目中,硬件看门狗有时候真不够用。
为什么?我举个例子。你想想看,硬件看门狗只能检测到「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。
一句话总结:软件看门狗不是万能的,但没有软件看门狗是万万不能的。