跳至内容
HardFault定位排查

HardFault定位排查

2026-07-27

1. HardFault

Cortex-M内核定义的一种严重异常,发生后意味着已无法继续正常运行,会进入HardFault_Handler中断进行处理。

常见发生原因:

  1. 访问无效内存
  2. 栈溢出
  3. 非法指令

想要排查这个故障,一般都要从寄存器出发。

2. 内核寄存器

这里相关的Cortex-M内核寄存器主要有以下几个:

  • R0-R12:通用寄存器,用于保存数据等操作
  • R13(MSP、PSP):主堆栈指针(MSP)、进城堆栈指针(PSP)
  • R14:连接寄存器(LR)
  • R15:程序计数器(PC)

这里主要需要知道的寄存器

  • R13:堆栈指针
  • R14:保存下一条需要运行的指令的地址
  • R15:当前程序运行到的地址

当 HardFault 等异常触发时,CPU 会立即暂停当前任务,并自动将 8 个关键寄存器的值压入当前使用的堆栈(MSP 或 PSP)中,形成一个固定的“异常堆栈帧”。

8个寄存器分别为:

  1. stacked_pc:它保存了触发 HardFault 的那条指令的地址。这是所有线索中最核心的一条
  2. stacked_lr :它记录了异常发生前,程序是从哪个函数调用过来的,即返回地址
  3. stacked_r0 ~ stacked_r3:这些寄存器通常用于传递函数参数或存放临时变量
  4. 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指针地址定位到发生错误的行。

1.1.2

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

可以看到运行后会打印异常命令具体是在哪个函数中调用的、在代码中的哪一行。