第三章 软件看门狗实现:基于定时器的软件看门狗设计、任务超时监控机制、心跳包机制

各位同学,咱们接着聊。上一章讲了硬件看门狗,说白了就是一颗独立的小芯片,你喂它就老实,不喂它就咬人。但硬件看门狗有个问题——它太“死心眼”了。它只知道你有没有在规定时间内拉一下引脚,至于你的程序是不是真的在干活,它根本不知道。

所以,这一章我们来聊聊软件看门狗。嗯,这才是真正考验嵌入式工程师功力的地方。

3.1 基于定时器的软件看门狗设计

软件看门狗,说白了就是利用MCU内部的定时器资源,自己搭一个“监控系统”。我个人习惯用系统滴答定时器(SysTick)或者通用定时器来做这件事。为什么?因为不用额外花钱买芯片,而且灵活性极高。

我刚开始做智能手表那会儿,觉得硬件看门狗就够用了。直到有一次,手表在待机模式下死机了,硬件看门狗居然没复位——后来查了半天,发现是低功耗模式下,硬件看门狗的时钟源被切断了。从那以后,我就开始重视软件看门狗的设计。

来看一个最简单的软件看门狗实现:

// 软件看门狗结构体
typedef struct {
    uint32_t reload_value;    // 重装载值
    uint32_t current_count;   // 当前计数值
    uint8_t  enabled;         // 使能标志
    void (*timeout_callback)(void);  // 超时回调函数
} sw_watchdog_t;

// 全局软件看门狗实例
static sw_watchdog_t sw_wdg;

// 初始化软件看门狗
void sw_watchdog_init(uint32_t timeout_ms) {
    sw_wdg.reload_value = timeout_ms;
    sw_wdg.current_count = timeout_ms;
    sw_wdg.enabled = 1;
    sw_wdg.timeout_callback = system_reset;  // 默认复位
    
    // 启动一个1ms周期的定时器中断
    timer_start(TIMER_ID, 1);  // 1ms中断一次
}

// 定时器中断服务函数(1ms调用一次)
void timer_isr(void) {
    if (sw_wdg.enabled) {
        if (sw_wdg.current_count > 0) {
            sw_wdg.current_count--;
        } else {
            // 超时了!
            if (sw_wdg.timeout_callback) {
                sw_wdg.timeout_callback();
            }
        }
    }
}

// 喂狗函数
void sw_watchdog_feed(void) {
    sw_wdg.current_count = sw_wdg.reload_value;
}

你看,代码其实很简单。核心思想就是:定时器每1ms减一次计数,如果计数减到0还没人喂狗,就执行超时回调。这个回调可以是系统复位,也可以是错误记录,甚至可以是进入安全模式。

我的小技巧: 不要把超时回调直接写成复位。我一般会先记录错误信息到Flash,再复位。这样下次开机可以读取错误日志,分析死机原因。这个习惯帮我解决了好几个棘手的bug。

3.2 任务超时监控机制

好了,有了基础的软件看门狗,我们得想想怎么用它来监控任务。你想想看,一个智能手表里跑着多少任务?显示、传感器采集、蓝牙通信、触控处理...每个任务都有可能卡死。

我设计过一个“任务超时监控”机制,说白了就是给每个任务分配一个“时间片”,如果某个任务在规定时间内没有完成,就认为它出问题了。

具体实现是这样的:

// 任务监控结构体
typedef struct {
    uint32_t task_id;           // 任务ID
    uint32_t max_exec_time;     // 最大执行时间(ms)
    uint32_t start_time;        // 任务开始时间
    uint8_t  is_running;        // 是否正在运行
    char     task_name[16];     // 任务名称,方便调试
} task_monitor_t;

// 任务监控表
static task_monitor_t task_table[] = {
    {0, 10,  0, 0, "Touch"},      // 触控任务,最多10ms
    {1, 50,  0, 0, "Display"},    // 显示任务,最多50ms
    {2, 100, 0, 0, "Sensor"},     // 传感器任务,最多100ms
    {3, 200, 0, 0, "BLE"},        // 蓝牙任务,最多200ms
};

// 任务开始执行时调用
void task_start_monitor(uint32_t task_id) {
    if (task_id < sizeof(task_table)/sizeof(task_table[0])) {
        task_table[task_id].start_time = get_tick_ms();
        task_table[task_id].is_running = 1;
    }
}

// 任务执行完毕时调用
void task_stop_monitor(uint32_t task_id) {
    if (task_id < sizeof(task_table)/sizeof(task_table[0])) {
        task_table[task_id].is_running = 0;
        uint32_t elapsed = get_tick_ms() - task_table[task_id].start_time;
        
        if (elapsed > task_table[task_id].max_exec_time) {
            // 记录超时警告
            log_warning("Task %s timeout! Expected %dms, actual %dms",
                       task_table[task_id].task_name,
                       task_table[task_id].max_exec_time,
                       elapsed);
        }
    }
}

// 系统监控任务(周期性调用)
void system_monitor_task(void) {
    for (int i = 0; i < sizeof(task_table)/sizeof(task_table[0]); i++) {
        if (task_table[i].is_running) {
            uint32_t elapsed = get_tick_ms() - task_table[i].start_time;
            if (elapsed > task_table[i].max_exec_time * 2) {
                // 严重超时!任务可能卡死了
                log_error("Task %s hung! Force reset.", task_table[i].task_name);
                system_reset();
            }
        }
    }
}
注意: 任务超时时间要留有余量。我曾经把触控任务的超时设得太紧,结果用户手指在屏幕上划得慢一点就触发复位了。嗯,这个bug被测试小姐姐骂惨了。建议超时时间设为正常执行时间的2-3倍。

3.3 心跳包机制

心跳包,这个名字很形象。就像人的心跳一样,系统每隔一段时间发出一个“我还活着”的信号。在智能手表里,心跳包机制有两个作用:一是告诉外部设备(比如手机)手表还在正常运行;二是作为系统内部健康检查的依据。

我设计的心跳包机制是这样的:

// 心跳包结构
typedef struct {
    uint32_t heartbeat_id;      // 心跳序号
    uint32_t uptime_ms;         // 系统运行时间
    uint8_t  battery_level;     // 电池电量
    uint8_t  task_status;       // 任务状态位图
    uint16_t error_count;       // 错误计数
} heartbeat_packet_t;

// 心跳包全局变量
static heartbeat_packet_t heartbeat;
static uint32_t last_heartbeat_tick = 0;

// 生成心跳包
void heartbeat_generate(void) {
    heartbeat.heartbeat_id++;
    heartbeat.uptime_ms = get_tick_ms();
    heartbeat.battery_level = battery_get_level();
    heartbeat.task_status = get_task_status_bitmap();
    heartbeat.error_count = get_error_count();
}

// 发送心跳包(通过蓝牙发送到手机)
void heartbeat_send(void) {
    heartbeat_generate();
    ble_send_data((uint8_t*)&heartbeat, sizeof(heartbeat_packet_t));
}

// 心跳包监控(检查是否按时发送)
void heartbeat_monitor(void) {
    uint32_t now = get_tick_ms();
    
    // 如果超过5秒没有发送心跳包
    if (now - last_heartbeat_tick > 5000) {
        log_warning("Heartbeat lost! Last sent %dms ago", 
                   now - last_heartbeat_tick);
        
        // 尝试重新发送
        heartbeat_send();
        last_heartbeat_tick = now;
        
        // 如果连续3次丢失心跳,触发系统检查
        static uint8_t lost_count = 0;
        lost_count++;
        if (lost_count >= 3) {
            log_error("Heartbeat lost 3 times! System check needed.");
            system_health_check();
            lost_count = 0;
        }
    }
}

这里有个细节我想强调一下。心跳包不仅仅是“我还活着”的信号,它还可以携带系统状态信息。比如电池电量、任务状态、错误计数等。这样手机端就能实时了解手表的状态,一旦发现异常可以及时提醒用户。

实战经验: 我在做某款运动手表时,发现用户跑步时手表经常死机。通过分析心跳包数据,发现是GPS模块在信号弱时耗电过大,导致电压跌落触发了复位。后来我们在心跳包里加入了电压监控,一旦发现电压低于阈值就主动降低GPS采样率,问题就解决了。

3.4 三种机制的协同工作

好了,现在我们有三个工具:软件看门狗、任务超时监控、心跳包机制。它们不是孤立的,而是需要协同工作。

我一般这样设计它们的协作关系:

机制 监控对象 响应时间 处理方式
软件看门狗 整个系统 毫秒级 立即复位
任务超时监控 单个任务 秒级 记录日志+尝试恢复
心跳包机制 系统与外部通信 秒级到分钟级 通知用户+系统检查

你看,这三个机制覆盖了不同的时间尺度和监控范围。软件看门狗是最后一道防线,任务超时监控是日常巡检,心跳包是对外沟通的窗口。

举个例子:如果某个传感器任务卡死了,任务超时监控会先发现,记录日志并尝试恢复。如果恢复失败导致系统整体卡死,软件看门狗会触发复位。复位后,手机端因为收不到心跳包,会提醒用户“手表可能异常重启了”。

我的建议: 这三个机制的阈值要合理设置。软件看门狗的超时时间要大于所有任务的最大执行时间之和,任务超时时间要大于单个任务的最大执行时间,心跳包间隔要大于任务超时时间。否则会出现“误杀”的情况。

嗯,这一章的内容就到这里。下一章我们会聊聊如何在实际项目中调试和优化这些机制。记住,看门狗不是万能的,但没有看门狗是万万不能的。