嵌入式踩坑:printf 未重定向引发 Semihosting BKPT 异常导致 HardFault 深度排查
项目场景:
工况:移动电源进行AC充电测试。
环境:GD32F305 + FreeRTOS,故障出在 BmsManageTask 任务。
问题描述
移动电源在AC充电时偶发性出现屏幕停止刷新并在几秒后重启。
移动电源是新开发的还没有测试过稳定性。
排查步骤:
步骤一:通过问题现象大概推测导致此问题的原因。
可能的原因:1. 内存踩踏或任务栈越界导致程序进入Hardfault(),最终看门狗复位系统。
2. 喂狗任务和显示任务异常阻塞得不到运行,最终看门狗复位系统。
3. 程序不断进入中断导致喂狗任务和显示任务得不到运行,最终看门狗复位系统。
步骤二:通过增加打印日志信息、仿真或其它方式来验证复位原因。
1. 使用Cmbacktrace来排查程序异常时是否进入了Hardfault()和分析出Hardfault()的原因。
当程序进入Hardfault()时会打印进入Hardfault()的原因和进入之前的调用了那些函数。
2. 在每个业务任务入口打印独立标记,例如,温度、电压、电流采样任务:LOG_ADC; Bms通信任务:LOG_BMS;显示任务:LOG_LCD等。当某个任务被永久阻塞时它的独立 标记不会被打印。
3. 在可能连续进入的中断里添加电平翻转测试函数。当连续进入中断时电平会以极快速度进 行翻转。
步骤三: 还原当时设备运行的工况尝试复现问题。
我当时复现出问题时,设备打印了进入Hardfault()的原因和进入前的函数调用关系。
进入Hardfault()原因:
函数调用关系:
步骤四: 根据现象和打印的信息分析原因
原因分析:
进入Hardfault()的原因是程序调用printf()打印接口但程序缺失printf()串口底层重定向;标准库启用半主机机制,执行BKPT指令触发DebugFault异常,因未做对应异常拦截处理,工程未实现Debug_HardFault异常处理函数,故障级联升级进入HardFault中断,同时造成JLINK仿真会话断开。
为什么调用了printf,当时是对系统添加系统日志,使用log输出接口替换printf()函数,但是由于printf()分散在各个文件比较零散,bms的iic通信相关.c文件的printf()没有替换干净。当IIC受干扰出现偶发通信异常时,程序会调用printf()打印IIC通信异常。
补充:
1. semihosting(半主机调试):
ARM 单片机专属调试功能,仅插上 JLINK 仿真器时才能正常工作。作用:没有自定义串口打印时,C 标准库借助仿真器,把printf文字打印到电脑 Keil 调试控制台。 实现方式:库内部自动插入BKPT硬件断点指令和调试器通信。
2. BKPT
ARM 汇编硬件断点指令BKPT,专门给调试器用:
- 正常调试:JLINK 识别这条指令,接收 printf 数据,程序继续跑;
- 无仿真器 / 调试配置不对:CPU 识别到 BKPT,但没人处理,直接抛出异常。
3. DebugFault
单片机内核调试专用异常,只要执行 BKPT 断点指令,就会进入这个异常。 工程没有写DebugFault_Handler专门处理半主机断点,这个异常无法正常释放。
4. 级联进入HardFault
嵌入式内核异常有优先级:DebugFault 处理失败 → 异常升级,统一进入总故障中断HardFault_Handler,这就是调试看到进 HardFault 的根本原因。
解决:
方案一:
检查工程里调用了printf()的地方,删除干净并使用log打印接口替代。
方案二:
自定实现fputc()输出函数。
