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,在里面可以找到具体是哪一行代码导致的该问题。

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

所遇到的问题

这里的问题很简单,同时也是我故意设置的一个堆栈溢出的问题,这里我带大家分析一下:首先,我们是走到一条叫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的值居然给我离奇的改变了,因为他的改变,导致我后续程序的跑飞。我们仔细看看执行的那条代码,发现是数组溢出了。

相关推荐
三佛科技-134163842122 小时前
BP85928D贴片SOP8 5V500MA智能家居开关电源芯片 (典型应用电路、替代方案FT8451B/FT8451H)
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
BT-BOX2 小时前
51单片机家居空气质量监测系统设计温湿度PM2.5
单片机·嵌入式硬件·51单片机
XINVRY-FPGA3 小时前
EP1C6T144I7N Altera Cyclone FPGA
嵌入式硬件·fpga开发·硬件工程·dsp开发·fpga
影阴3 小时前
stm32 HAL库实现 ADC 多通道采集
stm32·单片机·嵌入式硬件
我想我不够好。3 小时前
第二次考试 12.8
单片机·嵌入式硬件
小李做物联网4 小时前
【物联网毕业设计】113.1基于单片机物联网图书馆监测系统嵌入式
stm32·单片机·嵌入式硬件·物联网
✎ ﹏梦醒͜ღ҉繁华落℘4 小时前
单片机基础知识(八)SRAM芯片---IS62WV51216 芯片
单片机·嵌入式硬件
猫猫的小茶馆4 小时前
【ARM】内核移植(编译)
linux·arm开发·stm32·单片机·嵌入式硬件·mcu·pcb工艺
某林2125 小时前
STM32 底层固件架构与驱动设计
stm32·单片机·嵌入式硬件