STM32跑飞,进入HardFault_Handler如何精准的确定问题

STM32卡死、跑飞、进入HardFault_Handler如何精准的确定问题

引言

我们在使用STM32的时候,代码难免会出现疏忽,导致程序跑飞,不再正常运行,那么都是什么情况会导致STM32程序跑飞呢?或者我们调试的时候,发现代码进入了HardFault_Handler();导致了死循环,我们该如何去找到问题呢?

目录

  • 引言
  • [Part 1 程序跑飞原因](#Part 1 程序跑飞原因)
    • [1.1 软件原因](#1.1 软件原因)
      • [1.1.1 堆栈溢出](#1.1.1 堆栈溢出)
      • [1.1.2 指针操作错误](#1.1.2 指针操作错误)
      • [1.1.3 中断优先级配置错误](#1.1.3 中断优先级配置错误)
      • [1.1.4 外设错误配置](#1.1.4 外设错误配置)
      • [1.1.5 存储器管理问题](#1.1.5 存储器管理问题)
      • [1.1.6 代码逻辑错误](#1.1.6 代码逻辑错误)
      • [1.1.7 时钟配置错误](#1.1.7 时钟配置错误)
      • [1.1.8 Watchdog 超时](#1.1.8 Watchdog 超时)
      • [1.1.9 RTOS 栈与优先级问题](#1.1.9 RTOS 栈与优先级问题)
      • [1.1.10 FPU 与栈对齐](#1.1.10 FPU 与栈对齐)
      • [1.1.11 DMA 与缓存一致性(M7)](#1.1.11 DMA 与缓存一致性(M7))
      • [1.1.12 编译优化与未定义行为](#1.1.12 编译优化与未定义行为)
      • [1.1.13 MPU 缺失](#1.1.13 MPU 缺失)
    • [1.2 硬件原因](#1.2 硬件原因)
    • [1.3 构建与工具问题](#1.3 构建与工具问题)
  • [Part 2 精准定位 HardFault](#Part 2 精准定位 HardFault)
    • [2.1 捕获栈帧与寄存器](#2.1 捕获栈帧与寄存器)
    • [2.2 解码故障寄存器](#2.2 解码故障寄存器)
    • [2.3 将 PC 映射到源码](#2.3 将 PC 映射到源码)
    • [2.4 常见触发与修复](#2.4 常见触发与修复)
    • [2.5 快速排查清单](#2.5 快速排查清单)
    • [2.6 FreeRTOS 中断优先级要点](#2.6 FreeRTOS 中断优先级要点)
    • [2.7 FPU/缓存/MPU 要点](#2.7 FPU/缓存/MPU 要点)
    • [2.8 异常信息持久化与 Trace](#2.8 异常信息持久化与 Trace)
  • 用keil5调试HardFault_Handler并进行仿真
  • 找到程序跑飞位置

Part 1 程序跑飞原因

1.1 软件原因

1.1.1 堆栈溢出

原因:

  • 递归函数未正确退出导致栈空间耗尽
  • 任务栈过小或局部变量占用过大
  • 中断嵌套过深,占用过多的栈空间
    解决方法:
  • 在启动文件和链接脚本中为栈分配足够空间
  • 使用调试器监控 MSP/PSP,确保未越界
  • 避免递归或深度嵌套的函数调用
  • 在 RTOS 中增大任务栈并启用溢出检测
1.1.2 指针操作错误

原因:

  • 空指针被解引用
  • 未初始化指针或悬空指针
  • 指针越界访问内存
    解决方法:
  • 初始化所有指针并在使用前校验
  • 检查数组与指针边界
  • 使用静态分析工具查找潜在错误
1.1.3 中断优先级配置错误

原因:

  • 中断优先级冲突,未遵循 Cortex‑M 规则
  • 高优先级中断执行时间过长
    解决方法:
  • 合理分配优先级,数值越小优先级越高
  • 在中断中保持简短、避免复杂逻辑
1.1.4 外设错误配置

原因:

  • 外设初始化参数错误(定时器、DMA、USART 等)
  • 外设中断未正确配置
    解决方法:
  • 逐项核对外设初始化参数
  • 检查并绑定正确的中断处理程序
1.1.5 存储器管理问题

原因:

  • 动态内存分配失败返回 NULL
  • 超出 SRAM 限制访问非法地址
  • 堆与栈区域重叠
    解决方法:
  • 避免频繁动态分配,优先静态分配
  • 校验链接脚本,确保堆栈不重叠
  • 在失败路径中处理 malloc 返回值
1.1.6 代码逻辑错误

原因:

  • 无意的无限循环
  • 边界条件缺失
  • 异常路径未返回合适地址
    解决方法:
  • 使用单步调试验证流程
  • 加入断点或日志输出校验运行时行为
1.1.7 时钟配置错误

原因:

  • 系统时钟配置不当导致外设或 CPU 异常
  • 时钟频率超出芯片允许范围
    解决方法:
  • 借助 STM32CubeMX 或数据手册验证时钟树
  • 校验 HCLK、PCLK1/PCLK2 等频率范围
1.1.8 Watchdog 超时

原因:

  • 程序未及时喂狗导致复位或跳转
    解决方法:
  • 将喂狗逻辑放在关键路径,确保按期执行
1.1.9 RTOS 栈与优先级问题

原因:

  • configMINIMAL_STACK_SIZE 或任务栈过小
  • configMAX_SYSCALL_INTERRUPT_PRIORITY 设置不当,ISR 误用 API
    解决方法:
  • 使用 uxTaskGetStackHighWaterMark 监控栈余量
  • ISR 仅调用 FromISR 系列 API,遵守优先级限制
1.1.10 FPU 与栈对齐

原因:

  • 启用 FPU 但未正确保存上下文或懒保存配置异常
  • 栈未 8 字节对齐导致异常返回失败
    解决方法:
  • 在启动时开启 CPACR 访问并按需禁用 lazy stacking
  • 使能 SCB->CCR 的栈对齐,保证 8 字节对齐
1.1.11 DMA 与缓存一致性(M7)

原因:

  • DCache 导致缓冲区与外设数据不一致
  • DMA 缓冲区未对齐或放在不可达内存
    解决方法:
  • 对 DMA 缓冲区执行 Cache 清除/失效操作
  • 使用非缓存或专用 SRAM/DTCM 作为缓冲区
1.1.12 编译优化与未定义行为

原因:

  • 激进优化引入未定义行为
  • 未对齐访问或严格别名违规
    解决方法:
  • 保证结构体与访问对齐,必要时降低优化级别
  • 视情况使用 -fno-strict-aliasing
1.1.13 MPU 缺失

原因:

  • 未启用 MPU,野指针难以及早被捕获
    解决方法:
  • 配置基本 MPU 区域,禁止空地址访问并保护关键内存

1.2 硬件原因

  • 电源质量不佳(跌落、纹波、瞬态),导致不稳定
  • NRST 与复位电路受干扰,误复位或异常返回
  • 外部/内部晶振起振失败或时钟切换不当
  • EMI/ESD 干扰引发总线错误或外设异常
  • 外部存储器(QSPI/SDRAM/PSRAM)时序与等待周期错误
  • 高温/环境因素造成运行边界降低
    对应措施:
  • 优化供电与去耦,检查 BOR 设置
  • 清洁复位路径与滤波,验证复位时序
  • 校验 HSE/LSE 配置与 MCO,合理设置等待周期
  • 评估布线屏蔽与接地,进行抗扰设计
  • 外设与存储器遵循数据手册时序与校验

1.3 构建与工具问题

  • 链接脚本段落布局错误导致堆栈重叠或越界
  • 启动文件向量表或 _estack__StackTop 定义不正确
  • LTO/GC‑sections 误删必需符号或段
  • newlib/printf 等库使用不当造成堆内存膨胀
    建议:
  • 仔细审查 .map 文件与段大小,核对中断向量与栈顶
  • 在关键模块禁用过度优化或保留必要符号

Part 2 精准定位 HardFault

2.1 捕获栈帧与寄存器

使用异常入口判断当前使用的栈指针,提取被压栈的寄存器与故障寄存器:

c 复制代码
void HardFault_Handler(void) {
  __asm volatile (
    "TST lr, #4\n"
    "ITE EQ\n"
    "MRSEQ r0, MSP\n"
    "MRSNE r0, PSP\n"
    "B hardfault_c\n"
  );
}

void hardfault_c(uint32_t *sp) {
  volatile uint32_t r0  = sp[0];
  volatile uint32_t r1  = sp[1];
  volatile uint32_t r2  = sp[2];
  volatile uint32_t r3  = sp[3];
  volatile uint32_t r12 = sp[4];
  volatile uint32_t lr  = sp[5];
  volatile uint32_t pc  = sp[6];
  volatile uint32_t psr = sp[7];
  volatile uint32_t cfsr = SCB->CFSR;
  volatile uint32_t hfsr = SCB->HFSR;
  volatile uint32_t mmfar = SCB->MMFAR;
  volatile uint32_t bfar = SCB->BFAR;
  for(;;);
}
  • 入口用极少量汇编判断当前使用的栈指针( MSP 或 PSP ),把故障发生时的"自动压栈"栈帧指针传给 C 函数 hardfault_c 。
  • C 函数从栈帧中取出当时的寄存器值( r0--r3, r12, lr, pc, xPSR ),再读取 SCB 的故障状态寄存器( CFSR, HFSR, MMFAR, BFAR ),用于精准定位异常原因与触发点。
  • 末尾的死循环使系统停住,避免继续执行造成更大破坏;调试时可以在这些 volatile 变量上断点/观察。

2.2 解码故障寄存器

  • CFSR 包含三部分:MMFSR(0--7)、BFSR(8--15)、UFSR(16--31)
  • 常见位:
    • IACCVIOL/DACCVIOL 指令/数据访问违规
    • PRECISERR/IMPRECISERR 精确/非精确总线错误
    • STKERR/UNSTKERR 入栈/出栈错误
    • UNDEFINSTR/INVSTATE/INVPC 指令或状态非法
    • NOCP 协处理器不可用(FPU)
    • UNALIGNED/DIVBYZERO 未对齐访问/除零
  • HFSRFORCED 位表示硬故障被强制触发
  • 对于 PRECISERR 可读取 BFAR 获取精确故障地址

2.3 将 PC 映射到源码

使用编译生成的 ELF 将 PC 地址映射到具体源文件与行号:

bash 复制代码
arm-none-eabi-addr2line -e build/firmware.elf 0x08001234

结合 map 文件与符号信息定位具体函数与语句。

2.4 常见触发与修复

  • 空指针解引用:在关键指针使用前进行校验与断言
  • 非法总线访问:检查外设基址、存储器窗口与等待周期
  • 未对齐访问:保证数据结构与访问对齐,避免跨界拷贝
  • 除零:在除法前做保护或启用除零陷阱用于调试
  • 栈破坏导致异常返回失败:检查函数调用深度与栈对齐

2.5 快速排查清单

  • 打开栈溢出检测与运行期断言
  • 在关键路径加入故障寄存器打印或 RAM 记录
  • 监控任务栈高水位、ISR 执行时长与频率
  • 审查链接脚本、启动文件、时钟配置与中断表
  • 对 DMA 缓冲区进行缓存维护或使用非缓存内存

2.6 FreeRTOS 中断优先级要点

  • 采用一致的优先级分组并理解数值与实际优先级的关系
  • ISR 调用仅限 FromISR API,且优先级不高于阈值
  • SysTick/PendSV 维持合理优先级以保证任务切换

用keil5调试HardFault_Handler并进行仿真

我们插好我们的仿真器,常见的STM32仿真器有很多种,有STLINK或者DAP等等,我这里使用的是STLINK_V2这款仿真器,只能说便宜又好用了。插好仿真器,点击魔术棒->Debug->Setting,如果SW Device下面显示了芯片的ID,说明我们成功用仿真器连接到芯片。

Registers工具

我们可以在这里可以查看CPU寄存器的值。

  1. 通用寄存器(R0R12)R0R7:这些是低组寄存器,所有指令都可以访问。它们的大小为32位,复位后的初始值不定。R8~R12:这些是高组寄存器,只有部分的16位Thumb指令可以访问,而32位Thumb-2指令则不受限制。它们的大小同样为32位,复位后的初始值也不定。
  2. 特殊功能寄存器堆栈指针(SP):也称为R13,在Cortex-M4内核中,有两个堆栈指针------主堆栈指针(MSP)和进程堆栈指针(PSP)。MSP用于异常服务例程和需要特权访问的应用程序代码,而PSP则用于常规的应用代码。 连接寄存器(LR):即R14,主要作用是保存子程序的返回地址,以便在执行完子程序时恢复现场。 程序计数器(PC):即R15,用于指示当前执行的指令地址。在Cortex-M系列中,由于采用指令流水线技术,读取PC会返回当前指令地址+4(以兼容Thumb代码)。
  3. 程序状态寄存器(xPSR)xPSR是程序状态寄存器,它又被分为三个子状态寄存器:应用程序状态寄存器(APSR) 中断状态寄存器(IPSR)PRIMASK:只有1个位,置1时关闭所有可屏蔽的异常。 FAULTMASK:只有1个位,置1时只有NMI(非屏蔽中断)可以响应。 BASEPRI:8位寄存器,定义了被屏蔽优先级的阈值。
  4. 控制寄存器(Control)控制寄存器用于控制FPU(浮点单元)的激活、堆栈指针的选择以及线程模式的特权级等。

Memory工具

Memory Window是Keil调试环境中的一个窗口,它提供了对程序内存的直接访问。

通过这个窗口,你可以查看内存中的字节、字、双字 等数据,并可以实时修改这些数据以测试程序的行为。

这对于调试和验证程序中的内存访问、堆栈使用、变量存储等问题非常有帮助。

Disassembly工具

在Disassembly窗口中,你可以看到程序的反汇编代码。

这些代码是程序在CPU上实际执行的指令的文本表示。

你可以滚动窗口来查看不同的部分,或者使用窗口中的搜索功能来定位特定的代码或地址。

Call Stack工具

Call Stack(调用堆栈)界面是一个关键的调试工具,它允许开发者查看程序执行过程中函数调用的顺序和当前的位置。

Call Stack窗口将显示当前函数调用的堆栈。这包括每个函数的调用顺序、每个函数的名称(如果可用)以及调用该函数的地址。

你可以看到程序是如何从main函数开始,逐步调用其他函数,直到达到当前执行点的。

找到程序跑飞位置

为什么会产生HardFault_Handler?

  1. 由调试事件触发;
  2. 由总线错误,存储器管理错误或使用错误而产生 这个错误的产生是由于HardFault寄存器状态发生改变所导致
    这两个原因看上去说的很玄乎,实际上这个问题大家应该都不陌生,最常出现这个问题的原因一般是数据越界,堆栈溢出,等等。

出现HardFault_Handler怎么办

遇到之后没有必要手忙脚乱,这个问题其实并不难解决,因为HardFault_Handler的存在意义并不是为了让你的程序卡死,而是为了帮助你解决程序的问题,可以按照以下步骤进行:

  1. 找到Registers界面;
  2. 然后查看LR寄存器的值,该寄存器只有六种值是正常情况,具体可参考M4权威指南193页。这里给出一个表格:

通过 LR 精准定位 HardFault 的步骤
读取 HardFault 时的 LR 值

在调试器(如 Keil、GDB)中停在HardFault_Handler的断点处,查看 LR 寄存器的EXC_RETURN值。
判断错误发生的场景

若 LR 是0xFFFFFFF9/E9:说明错误出现在普通程序代码(非中断)中;

若 LR 是0xFFFFFFF1/E1:说明错误是在中断 / 异常的处理过程中被打断(即发生了中断嵌套)。
确定对应的栈指针(MSP/PSP)

根据EXC_RETURN的低 4 位(Bit2)判断:

Bit2=0 → 使用 MSP(主栈);

Bit2=1 → 使用 PSP(进程栈,通常 RTOS 任务用)。
读取栈帧,获取出错指令地址

异常发生时,硬件会自动将错误发生前的上下文(R0-R3、R12、LR、PC、xPSR)压入对应栈中。找到栈指针(MSP/PSP)指向的地址,从栈中读取PC值 ------这个 PC 就是触发 HardFault 的指令地址,结合反汇编即可定位到具体代码行。

关于MSP和PSP核心区别
  1. 栈类型:MSP(主栈) vs PSP(进程栈)------ 定位时「查哪个栈」的关键
    0xFFFFFFF9/E9(MSP 主栈):
    错误发生在「使用主栈的线程模式」,典型场景:
    ✅ 裸机程序(无 RTOS):整个程序默认用 MSP,HardFault 大概率是主函数、中断外的普通代码(如数组越界、空指针)触发;
    ✅ RTOS 系统:仅「主线程 / 空闲任务」或「未切换到任务栈的初始化阶段」用 MSP,HardFault 指向系统初始化、主线程代码。
    定位实操:读取 MSP 寄存器 指向的栈帧,解析其中的 PC 值(触发错误的指令地址)。
    0xFFFFFFFD/ED(PSP 进程栈):
    错误发生在「使用进程栈的线程模式」,典型场景:
    ✅ 仅存在于 RTOS 系统(裸机几乎不会出现):每个任务有独立的 PSP 栈,HardFault 必然是某一个任务的代码触发(如任务内访问非法地址、任务栈溢出);
    ✅ 0xFFFFFFED 额外特征:错误发生在「非特权级任务」(如用户态任务访问特权级外设 / 寄存器)。
    定位实操:读取 PSP 寄存器 指向的栈帧,结合 RTOS 的任务栈信息,定位到具体崩溃的任务和代码行。
  2. 特权级:仅 0xFFFFFFED 是非特权级 ------ 缩小错误原因范围
    0xFFFFFFF9/E9/FD 都是「特权级」:错误原因多是代码逻辑(如空指针、数组越界)、硬件访问(如非法外设地址);
    0xFFFFFFED 是「非特权级」:大概率是「非特权代码试图访问特权资源」(如任务直接操作内核寄存器、写受保护的内存),而非单纯的代码逻辑问题。
查看寄存器->找到

根据LR的寄存器的值判断是主栈还是线程栈导致的问题,如果是主栈就继续查看MSP寄存器,如果是进程栈,那么就查看PSP寄存器。

根据MSP寄存器或者PSP寄存器的记录,将其值在Memory中查看其具体地址,一般是0x80开头的。

打开Disassembly,在里面可以找到具体是哪一行代码导致的该问题。

就可以成功找到我们跑飞前代码地方了。

所遇到的问题

这里的问题很简单,同时也是我故意设置的一个堆栈溢出的问题,这里我带大家分析一下:首先,我们是走到一条叫menuPointselectItem+scrollBar.func1()来去执行我们想执行的函数,我们通过Watch窗口查看一下:

函数的地址应该在flash里对面,这里很明显跑到ram里面了,就跑飞了

从 Watch 窗口能看到,func1的地址是0x20000200------ 这明显是 RAM 区域的地址,而不是 Flash 里的函数地址(正常函数地址应该是0x08xxxxxx开头)。

当程序执行menuPointselectItem+scrollBar.func1()时,会跳转到0x20000200这个地址去执行,但 RAM 里的内容是数据 / 随机值,不是合法的 CPU 指令。此时 CPU 执行 "非法指令",就会触发 HardFault_Handler,也就是说的 "跑飞"。

里面的func1是应该指向一个叫amplitudeSub的函数指针的,这里指向不正确,于是我就非常怀疑这个结构体赋值给这个成员有一个环节出了问题,于是我就一步步的单步指向,知道我执行到了一个地方:

在这之前,结构体里面的值都是我理想的值,当我执行完下一条指令的时候,错误出现了。

这里的func1的值居然给我离奇的改变了,因为他的改变,导致我后续程序的跑飞。我们仔细看看执行的那条代码,发现是数组溢出了。

相关推荐
蓝斯49710 小时前
从.到实现一个Windows上的虚拟hid键盘设备
windows·单片机·计算机外设
熙芯XiChip10 小时前
硬实时与软实时RTOS核心区别
嵌入式硬件
伟大的前程11 小时前
unsigned int 定义的变量 在 电脑的 64 位系统中 和 32 位 cortex-m0 和 8 位的 51 单片机
单片机·嵌入式硬件
小渔村的拉线工11 小时前
1.HPM6E80 解析芯片整体工作原理和存储架构
单片机·嵌入式硬件·mcu·架构·hpm6e80
熙芯XiChip12 小时前
CAN与CANopen的本质区别
单片机
lilihewo12 小时前
Altium Designer 封装库的创建(分享pcb 3D封装库)
嵌入式硬件·51单片机
神罚天下ET13 小时前
典型arm位单片机启动流程(从上电到main.c)
c语言·arm开发·单片机
碧海银沙音频科技研究院14 小时前
蓝牙耳机使用WSDF5361锂保芯片进入船运模式实现方法
嵌入式硬件·算法
Zevalin爱灰灰15 小时前
模拟信号链电路——基于TL431的电压基准电路
单片机·嵌入式硬件
csdn杰哥15 小时前
瑞萨单片机开发教程[二十七] DTC 外部中断触发传输实验
单片机·嵌入式硬件·mongodb·瑞萨·ra6m5