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 构建与工具问题)
- [1.1 软件原因](#1.1 软件原因)
- [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并进行仿真
- Registers工具
- Memory工具
- Disassembly工具
- [Call Stack工具](#Call Stack工具)
- 找到程序跑飞位置
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未对齐访问/除零
HFSR的FORCED位表示硬故障被强制触发- 对于
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 调用仅限
FromISRAPI,且优先级不高于阈值 - SysTick/PendSV 维持合理优先级以保证任务切换
用keil5调试HardFault_Handler并进行仿真
我们插好我们的仿真器,常见的STM32仿真器有很多种,有STLINK或者DAP等等,我这里使用的是STLINK_V2这款仿真器,只能说便宜又好用了。插好仿真器,点击魔术棒->Debug->Setting,如果SW Device下面显示了芯片的ID,说明我们成功用仿真器连接到芯片。
Registers工具

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

Memory Window是Keil调试环境中的一个窗口,它提供了对程序内存的直接访问。
通过这个窗口,你可以查看内存中的字节、字、双字 等数据,并可以实时修改这些数据以测试程序的行为。
这对于调试和验证程序中的内存访问、堆栈使用、变量存储等问题非常有帮助。
Disassembly工具

在Disassembly窗口中,你可以看到程序的反汇编代码。
这些代码是程序在CPU上实际执行的指令的文本表示。
你可以滚动窗口来查看不同的部分,或者使用窗口中的搜索功能来定位特定的代码或地址。
Call Stack工具

Call Stack(调用堆栈)界面是一个关键的调试工具,它允许开发者查看程序执行过程中函数调用的顺序和当前的位置。
Call Stack窗口将显示当前函数调用的堆栈。这包括每个函数的调用顺序、每个函数的名称(如果可用)以及调用该函数的地址。
你可以看到程序是如何从main函数开始,逐步调用其他函数,直到达到当前执行点的。
找到程序跑飞位置
为什么会产生HardFault_Handler?
- 由调试事件触发;
- 由总线错误,存储器管理错误或使用错误而产生 这个错误的产生是由于HardFault寄存器状态发生改变所导致
这两个原因看上去说的很玄乎,实际上这个问题大家应该都不陌生,最常出现这个问题的原因一般是数据越界,堆栈溢出,等等。
出现HardFault_Handler怎么办
遇到之后没有必要手忙脚乱,这个问题其实并不难解决,因为HardFault_Handler的存在意义并不是为了让你的程序卡死,而是为了帮助你解决程序的问题,可以按照以下步骤进行:
- 找到Registers界面;
- 然后查看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核心区别
- 栈类型:MSP(主栈) vs PSP(进程栈)------ 定位时「查哪个栈」的关键
0xFFFFFFF9/E9(MSP 主栈):
错误发生在「使用主栈的线程模式」,典型场景:
✅ 裸机程序(无 RTOS):整个程序默认用 MSP,HardFault 大概率是主函数、中断外的普通代码(如数组越界、空指针)触发;
✅ RTOS 系统:仅「主线程 / 空闲任务」或「未切换到任务栈的初始化阶段」用 MSP,HardFault 指向系统初始化、主线程代码。
定位实操:读取 MSP 寄存器 指向的栈帧,解析其中的 PC 值(触发错误的指令地址)。
0xFFFFFFFD/ED(PSP 进程栈):
错误发生在「使用进程栈的线程模式」,典型场景:
✅ 仅存在于 RTOS 系统(裸机几乎不会出现):每个任务有独立的 PSP 栈,HardFault 必然是某一个任务的代码触发(如任务内访问非法地址、任务栈溢出);
✅ 0xFFFFFFED 额外特征:错误发生在「非特权级任务」(如用户态任务访问特权级外设 / 寄存器)。
定位实操:读取 PSP 寄存器 指向的栈帧,结合 RTOS 的任务栈信息,定位到具体崩溃的任务和代码行。 - 特权级:仅 0xFFFFFFED 是非特权级 ------ 缩小错误原因范围
0xFFFFFFF9/E9/FD 都是「特权级」:错误原因多是代码逻辑(如空指针、数组越界)、硬件访问(如非法外设地址);
0xFFFFFFED 是「非特权级」:大概率是「非特权代码试图访问特权资源」(如任务直接操作内核寄存器、写受保护的内存),而非单纯的代码逻辑问题。
查看寄存器->找到
根据LR的寄存器的值判断是主栈还是线程栈导致的问题,如果是主栈就继续查看MSP寄存器,如果是进程栈,那么就查看PSP寄存器。

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

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


就可以成功找到我们跑飞前代码地方了。
所遇到的问题
这里的问题很简单,同时也是我故意设置的一个堆栈溢出的问题,这里我带大家分析一下:首先,我们是走到一条叫menuPoint[selectItem+scrollBar].func1()来去执行我们想执行的函数,我们通过Watch窗口查看一下:

函数的地址应该在flash里对面,这里很明显跑到ram里面了,就跑飞了
从 Watch 窗口能看到,func1的地址是0x20000200------ 这明显是 RAM 区域的地址,而不是 Flash 里的函数地址(正常函数地址应该是0x08xxxxxx开头)。
当程序执行menuPoint[selectItem+scrollBar].func1()时,会跳转到0x20000200这个地址去执行,但 RAM 里的内容是数据 / 随机值,不是合法的 CPU 指令。此时 CPU 执行 "非法指令",就会触发 HardFault_Handler,也就是说的 "跑飞"。
里面的func1是应该指向一个叫amplitudeSub的函数指针的,这里指向不正确,于是我就非常怀疑这个结构体赋值给这个成员有一个环节出了问题,于是我就一步步的单步指向,知道我执行到了一个地方:

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

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