HardFault定位排查
1. HardFault
Cortex-M内核定义的一种严重异常,发生后意味着已无法继续正常运行,会进入HardFault_Handler中断进行处理。
常见发生原因:
- 访问无效内存
- 栈溢出
- 非法指令
- …
想要排查这个故障,一般都要从寄存器出发。
2. 内核寄存器
这里相关的Cortex-M内核寄存器主要有以下几个:
- R0-R12:通用寄存器,用于保存数据等操作
- R13(MSP、PSP):主堆栈指针(MSP)、进城堆栈指针(PSP)
- R14:连接寄存器(LR)
- R15:程序计数器(PC)
这里主要需要知道的寄存器
- R13:堆栈指针
- R14:保存下一条需要运行的指令的地址
- R15:当前程序运行到的地址
当 HardFault 等异常触发时,CPU 会立即暂停当前任务,并自动将 8 个关键寄存器的值压入当前使用的堆栈(MSP 或 PSP)中,形成一个固定的“异常堆栈帧”。
8个寄存器分别为:
stacked_pc:它保存了触发 HardFault 的那条指令的地址。这是所有线索中最核心的一条stacked_lr:它记录了异常发生前,程序是从哪个函数调用过来的,即返回地址stacked_r0~stacked_r3:这些寄存器通常用于传递函数参数或存放临时变量stacked_psr:它包含了处理器状态、中断优先级等关键信息。通过解析 PSR,可以确认异常发生时处理器处于线程模式还是处理模式,以及是否启用了浮点单元等,帮助排除一些环境配置问题。
3. Debug验证
创建一个空白工程,添加以下代码,编译后进行debug测试。
int main(void)
{
// cubemx生成的初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART3_UART_Init();
// 用户代码
volatile int test = 10;
// 访问非法地址
test = *(int *)0x56785678;
while(1)
{
}
}当程序运行到异常段后。会进入HardFault_Handler,这里添加一段汇编指令,获取异常发生时的寄存器数据,并使用串口进行打印。
void HardFault_Handler(void)
{
/* USER CODE BEGIN HardFault_IRQn 0 */
__asm volatile (
"TST lr, #4 \n" // 检查LR的bit[2],判断使用MSP还是PSP
"ITE EQ \n"
"MRSEQ r0, MSP \n" // 若相等,使用主堆栈指针(MSP)
"MRSNE r0, PSP \n" // 若不等,使用进程堆栈指针(PSP)
"B hard_fault_handler_c \n" // 跳转到C函数处理
);
/* USER CODE END HardFault_IRQn 0 */
while (1)
{
/* USER CODE BEGIN W1_HardFault_IRQn 0 */
/* USER CODE END W1_HardFault_IRQn 0 */
}
}打印的C函数如下
void hard_fault_handler_c(unsigned int *hardfault_args)
{
uint32_t stacked_r0 = hardfault_args[0]; // 堆栈偏移+0字节 = R0
uint32_t stacked_r1 = hardfault_args[1]; // 堆栈偏移+4字节 = R1
uint32_t stacked_r2 = hardfault_args[2]; // 堆栈偏移+8字节 = R2
uint32_t stacked_r3 = hardfault_args[3]; // 堆栈偏移+12字节 = R3
uint32_t stacked_r12 = hardfault_args[4]; // 堆栈偏移+16字节 = R12
uint32_t stacked_lr = hardfault_args[5]; // 堆栈偏移+20字节 = LR
uint32_t stacked_pc = hardfault_args[6]; // 堆栈偏移+24字节 = PC
uint32_t stacked_psr = hardfault_args[7]; // 堆栈偏移+28字节 = PSR
printf("HardFault!\r\n");
printf("R0 = 0x%08X \r\n", stacked_r0);
printf("R1 = 0x%08X \r\n", stacked_r1);
printf("R2 = 0x%08X \r\n", stacked_r2);
printf("R3 = 0x%08X \r\n", stacked_r3);
printf("R12 = 0x%08X \r\n", stacked_r12);
printf("LR = 0x%08X \r\n", stacked_lr);
printf("PC = 0x%08X \r\n", stacked_pc);
printf("PSR = 0x%08X \r\n", stacked_psr);
while(1); // 停在此处,方便调试器连接
}比如这里打印结果如下
HardFault!
R0 = 0x56785678
R1 = 0x20000110
R2 = 0x00000000
R3 = 0x00000000
R12 = 0x01FFFFFF
LR = 0x08003819
PC = 0x0800482C
PSR = 0x01000000
可以通过PC指针地址定位到发生错误的行。

4. addr2line工具
找问题的时候总不能常年连着调试器,有可能只有个串口或者其他接口能把异常命令地址记录下来,因此我们需要想办法根据地址直接定位到故障的地方。
第一步:准备elf文件
正常代码编译后,我们用到的主要是hex文件,如果OTA可能用到bin文件,有的 IDE 也会默认生成elf文件(Keil 中生成的是axf文件,其实是一样的)
- bin:纯二进制机器码
- hex:带地址的十六进制文本
- elf:包含完整的调试信息
程序烧录,基本都是使用的hex文件,也有的会用elf文件。这里调试需要对应的elf文件,软件不能修改哦。
第二步:安装add2line工具
安装addr2line工具,这个工具是包含在ARM交叉编译工具里面的。
下载地址:https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
安装方法参考vscode开发stm32的文章。
可以看到版本号表示安装完成
C:\Users\XCHARGE-2024Q2-LT003>arm-none-eabi-addr2line.exe --version
GNU addr2line (Arm GNU Toolchain 15.2.Rel1 (Build arm-15.86)) 2.45.1.20251203
Copyright (C) 2025 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) any later version.
This program has absolutely no warranty.第三步:测试
首先直接运行程序,并在串口中打印了异常命令地址,记录下PC指针的值。
HardFault!
R0 = 0x56785678
R1 = 0x00000014
R2 = 0x00000000
R3 = 0x00000000
R12 = 0x01FFFFFF
LR = 0x08004849
PC = 0x08004762
PSR = 0x01000200 查找异常位置arm-none-eabi-addr2line.exe -e <调试文件> -a -f <命令地址>
C:\Users\XCHARGE-2024Q2-LT003\Desktop>arm-none-eabi-addr2line.exe -e threadx_test.axf -a -f 0x08004762
0x08004762
func_A
C:\Users\XCHARGE-2024Q2-LT003\Desktop\threadx_test\MDK-ARM/../Core/Src/main.c:60可以看到运行后会打印异常命令具体是在哪个函数中调用的、在代码中的哪一行。