做嵌入式开发,HardFault几乎是每个人都会遇到的问题。第一次遇到的时候基本都懵,因为程序直接停了,连断点都打不上。今天把我在STM32上排查HardFault的经验整理一下,都是实打实踩坑踩出来的。
一、HardFault是什么
简单来说就是CPU遇到了”不知道怎么处理”的异常。比如访问了非法地址、除零、对齐错误、栈溢出等等。STM32的异常优先级里HardFault属于最低优先级(-2),但它一旦触发基本就意味着程序跑飞了。
二、先分清是哪种Fault
STM32有好几种Fault,触发后要先看CFSR(Configurable Fault Status Register)和HFSR(HardFault Status Register)寄存器来判断类型:
void HardFault_Handler(void)
{
uint32_t cfsr = SCB->CFSR;
uint32_t hfsr = SCB->HFSR;
uint32_t mmfar = SCB->MMFAR;
uint32_t bfar = SCB->BFAR;
// CFSR各字段含义:
// [7:0] - ICPUTTERR: Instruction cache tag parity error
// [15:8] - IACCVIOL: Instruction access violation
// [23:16] - IARERR: Instruction fetch exception
// [31:24] - CPACERR: Coprocessor access error
// [31:24] - USTKERR: Underflow stack fault (MSP)
// [23:16] - USGERR: Usage fault (div by zero, unaligned, etc)
// [15:8] - UNSTKERR: Unstacking fault (MSP)
// [7:0] - STKERR: Stacking fault (MSP)
// HFSR:
// [31] - FORCED: Force HardFault (比如NMI被配置成HardFault)
// [30] - VECTTBL: Vector table read fault
while(1)
{
// 死循环,用调试器查看寄存器
}
}
三、最常见的原因和排查方法
1. 栈溢出(Stack Overflow)
这是我最常遇到的。STM32的栈是从高地址往低地址增长的,如果栈空间不够用就会覆盖掉全局变量甚至代码区。
排查方法:
• 在Keil里设置Stack Size,然后跑程序看会不会进HardFault
• 用Magic Number法:在栈初始化的时候用特定值填充栈空间,程序跑一会儿后检查有多少值被覆盖了
• 看MSP寄存器的值,如果MSP已经小于你设置的栈底地址,那就是栈溢出了
2. 访问空指针或非法地址
int *p = NULL;
*p = 10; // 直接HardFault
这种问题在调试器里可以直接定位到出错行。但有时候指针被意外修改了,指向了随机地址,就比较难找。
3. 未对齐访问
STM32F1的某些外设寄存器要求4字节对齐访问。比如:
typedef struct {
uint8_t a;
uint32_t b; // 这个成员可能未对齐
} MyStruct;
MyStruct s;
s.b = 10; // 如果s的地址不是4字节对齐,可能触发UsageFault
解决:用__attribute__((packed))或者调整结构体成员顺序。
4. 中断里操作了非原子变量
这个坑比较隐蔽。比如一个全局变量在中断和主循环里都被访问,但没有做原子保护:
volatile uint32_t counter = 0;
void TIM2_IRQHandler(void)
{
counter++; // 32位变量在Cortex-M3上是原子操作(一条LDR/STR指令)
}
其实32位变量在M3上是原子的(一条指令搞定),但64位变量或者结构体就不是了。这个要看具体情况。
四、一个实用的调试技巧
在HardFault_Handler里不要直接while(1)死循环,把出错时的寄存器现场保存下来,这样用调试器连接上去可以看到程序停在哪一行:
void HardFault_Handler(void)
{
// 保存出错时的上下文
hardfault_context.psr = __get_PSR();
hardfault_context.pc = __get_PC();
hardfault_context.lr = __get_LR();
hardfault_context.r0 = __get_R0();
hardfault_context.r1 = __get_R1();
hardfault_context.r2 = __get_R2();
hardfault_context.r3 = __get_R3();
hardfault_context.r12 = __get_R12();
hardfault_context.sp = __get_MSP();
// 用串口把信息发出去,或者存在Flash里
// 然后用J-Link连接,查看hardfault_context结构体
// 从pc值就能定位到出错的源代码行
while(1);
}
五、几个经验之谈
1. 调试HardFault的时候,先把Keil的Options->Debug里的”Use: J-Link Debugger”选对,然后在Options->Debugger里勾选”Flash”和”Verify”,这样能看到Flash编程是否成功。
2. 如果你用了FreeRTOS之类的操作系统,HardFault_Handler要改成FreeRTOS的宏定义形式:void HardFault_HandlerC(void),不然会递归中断把自己搞死。
3. 有时候HardFault不是你的代码问题,是启动文件里中断向量表没排好。特别是用了多个中断的时候,检查一下vector table的offset对不对。
4. 最后一条:别怕HardFault。每次排查出一个HardFault,你对STM32的理解就加深一层。我当年被HardFault折磨了半年,现在看到HardFault反而觉得亲切了。
我要赚赏金
