第三章 软件故障定位:程序跑飞与看门狗复位分析

软件故障,说白了就是代码跑偏了。你写的时候觉得逻辑完美,一上电就各种抽风。我做了十几年单片机,最头疼的不是硬件短路,而是这种“看起来没问题,跑起来全乱套”的软故障。这一章,咱们就专门啃这块硬骨头。

3.1 程序跑飞与看门狗复位分析

程序跑飞,就是CPU执行了不该执行的指令。指针跳到未初始化区域、堆栈被破坏、中断嵌套乱掉,都会导致跑飞。看门狗复位,是系统最后的救命稻草——但如果你连它怎么复位的都搞不清,那这根稻草就是根草绳。

核心判断方法:

  • 检查MCU复位标志寄存器(RCC_CSR、RSTSRC等),确认复位源
  • 在main函数入口处设置GPIO翻转,看复位后是否执行到该点
  • 利用空闲中断或HardFault_Handler捕获异常现场

我遇到过最典型的一个案例:某款智能锁产品,用户反映偶尔死机。我查了三天,发现是看门狗喂狗位置不对——喂狗代码放在了一个条件分支里,某些路径根本执行不到。嗯,这种坑,你不看汇编是发现不了的。

看门狗复位分析流程

  1. 确认复位源:读取复位状态寄存器,看是POR、BOR还是IWDG/WWDG复位
  2. 定位跑飞点:在HardFault_Handler中保存LR、PC、SP到备份寄存器或RAM保留区
  3. 分析堆栈回溯:通过SP指针回溯调用栈,找到跑飞前的最后函数调用
  4. 检查喂狗路径:确认所有主循环路径、中断路径、低功耗路径都覆盖了喂狗

注意:看门狗复位后,MCU会重新初始化。如果你在初始化代码里把复位标志清掉了,那就永远不知道是谁复位的。我建议在启动代码最前面就保存复位标志,等系统稳定后再处理。

// 示例:STM32复位源检测
void CheckResetSource(void)
{
    uint32_t reset_src = RCC->CSR;
    
    if (reset_src & RCC_CSR_LPWRRSTF) {
        // 低功耗管理复位
        Error_Handler("LPWR Reset");
    }
    if (reset_src & RCC_CSR_WWDGRSTF) {
        // 窗口看门狗复位
        Error_Handler("WWDG Reset");
    }
    if (reset_src & RCC_CSR_IWDGRSTF) {
        // 独立看门狗复位
        Error_Handler("IWDG Reset");
    }
    if (reset_src & RCC_CSR_SFTRSTF) {
        // 软件复位
        Error_Handler("SW Reset");
    }
    
    // 清除复位标志
    RCC->CSR |= RCC_CSR_RMVF;
}

3.2 堆栈溢出检测与定位

堆栈溢出,是嵌入式开发里最隐蔽的杀手。你想想看,局部变量、函数调用、中断嵌套,全挤在那一小块RAM里。一旦溢出,数据就写到隔壁变量家里去了——表现就是各种莫名其妙。

我个人习惯,在每个任务或主循环里都做堆栈水位检测。不是等到溢出了再查,而是提前预警。

堆栈检测三板斧:

  • 填充法:初始化时用0xDEAD或0x5A5A填充整个堆栈区,运行时从栈底往上扫描,看还有多少没被覆盖
  • 边界哨兵:在堆栈底部和顶部各放一个哨兵变量,定时检查是否被改写
  • MPU保护:如果MCU支持MPU,直接给堆栈区域设置访问权限,溢出就触发异常

我曾经在一个RTOS项目里,发现某个任务总是跑着跑着就挂了。用填充法一查,好家伙,堆栈用了95%。那个任务里有个递归函数,深度一深就爆了。后来我把递归改成了迭代,问题解决。

// 堆栈填充检测示例
#define STACK_SIZE   512
#define STACK_FILL   0x5A5A5A5A

uint32_t stack_area[STACK_SIZE];

void Stack_Init(void)
{
    for (int i = 0; i < STACK_SIZE; i++) {
        stack_area[i] = STACK_FILL;
    }
}

uint32_t Stack_GetUsage(void)
{
    uint32_t used = 0;
    for (int i = STACK_SIZE - 1; i >= 0; i--) {
        if (stack_area[i] != STACK_FILL) {
            used = STACK_SIZE - i;
            break;
        }
    }
    return used;
}

3.3 中断优先级配置错误排查

中断优先级配置错了,系统表现就是“偶尔卡死”、“按键不灵”、“通信丢包”。说白了,就是高优先级中断把低优先级的活给抢了,或者两个中断互相抢导致死锁。

我见过最离谱的配置:有人把定时器中断和串口中断设成同一优先级,结果两个中断同时触发时,CPU不知道先处理谁,直接进HardFault。

错误类型 现象 排查方法
优先级相同冲突 中断嵌套时系统卡死 检查NVIC配置,确保关键中断优先级唯一
高优先级中断耗时过长 低优先级中断响应延迟 在中断服务函数里加GPIO翻转,用示波器测时长
中断优先级分组不一致 中断行为异常 确认所有模块使用相同的优先级分组(如NVIC_PriorityGroup_4)
未使能中断 外设不工作 检查NVIC_EnableIRQ和中断向量表

避坑指南:我曾经在一个多中断项目里,把SysTick中断优先级设成了0(最高),结果所有其他中断都被它堵死了。SysTick只是用来做系统滴答,优先级设成最低反而更合理。记住:越频繁的中断,优先级应该越低,除非你有实时性要求。

3.4 死循环与任务挂起分析

死循环,就是代码卡在一个地方出不来了。任务挂起,是RTOS里任务被阻塞了,但没人去唤醒它。这两种情况,表现都是“系统不响应了”。

怎么区分?看电流。死循环的CPU还在跑,电流正常;任务挂起的CPU可能进了低功耗模式,电流会降下来。

死循环定位方法

  • 断点法:在可疑循环前后加断点,看能不能跑过去
  • LED闪烁法:在关键路径上放LED翻转,看哪个灯不闪了
  • 串口打印法:在循环体里打印计数器,看输出是否停止
  • JTAG/SWD暂停法:用调试器暂停CPU,看PC指针停在哪里

任务挂起分析步骤

  1. 检查任务状态寄存器,看是READY、BLOCKED还是SUSPENDED
  2. 如果是BLOCKED,检查等待的信号量、消息队列、互斥锁是否被释放
  3. 如果是SUSPENDED,检查是否有其他任务调用了vTaskSuspend()
  4. 检查任务优先级,看是否有更高优先级的任务一直占用CPU

实战技巧:我习惯在每个任务里加一个“心跳计数器”,任务每执行一次就加1。如果某个任务的心跳计数器长时间不变,那它肯定挂了。这个办法在RTOS调试里特别好用。

// 任务心跳检测示例
uint32_t task_heartbeat[10] = {0};

void Task1(void *param)
{
    while(1) {
        task_heartbeat[0]++;
        // 任务实际代码
        vTaskDelay(10);
    }
}

void Task2(void *param)
{
    while(1) {
        task_heartbeat[1]++;
        // 任务实际代码
        vTaskDelay(10);
    }
}

// 监控任务
void MonitorTask(void *param)
{
    uint32_t last_beat[10] = {0};
    while(1) {
        for (int i = 0; i < 2; i++) {
            if (task_heartbeat[i] == last_beat[i]) {
                // 任务挂起,记录错误
                Error_Handler("Task %d hung", i);
            }
            last_beat[i] = task_heartbeat[i];
        }
        vTaskDelay(100);
    }
}
软件故障定位知识体系 软件故障定位 程序跑飞 看门狗复位 堆栈溢出 中断优先级 复位标志检测 喂狗路径检查 填充法检测 NVIC配置检查 优先级分组 核心原则 先确认现象 → 再定位根因 → 最后修复验证 不要跳过任何一步,否则你会反复踩同一个坑

软件故障定位,说白了就是“猜-验证-再猜”的过程。你经验越多,猜得越准。但不管经验多丰富,工具和方法才是根本。这一章讲的方法,我建议你在项目里都试一遍,找到最适合自己的那一套。