type
status
date
slug
summary
tags
category
icon
password
上次编辑时间
Sep 10, 2025 07:00 AM

背景:

用 Keil 进行Debug时,有时候点运行会发现程序跑飞了,点击停止运行,发现程序停在HardFault_Handler函数里的死循环,这说明stm32出现了硬件错误。

stm32触发HardFault一般就几种原因:

  1. 内存溢出或访问越界
    1. 非法内存访问:访问未分配的内存区域或是只读存储区
    2. 访问野指针
    3. 数组越界
  1. 栈溢出

定位错误代码办法:

一、手动栈回溯

进入HardFault后,查看左侧Registers Window窗口,在寄存器查看窗口查找R14(LR)的值。
notion image
  • 如果R14(LR) = 0xFFFFFFE9,继续查看MSP(主堆栈指针)的值;
  • 如果R14(LR) = 0xFFFFFFFD,继续查看PSP(进程栈指针)的值。
以上这么做的原因如下(展开折叠)
在Cortex_M3权威指南中可以看到如下图所示:
notion image
发生异常之后可首先查看LR寄存器中的值,确定当前使用堆栈为MSP或PSP,然后找到相应堆栈的指针,并在内存中查看相应堆栈里的内容。

这里解释一下关于 LR 寄存器的工作原理:

如上所述,当 Cortex-M4 处理器接受了一个异常后,寄存器组中的一些寄存器值会被自动压入当前栈空间里,这其中就包括链接寄存器(LR )。这时的 LR 会被更新为异常返回时需要使用的特殊值(EXC_RETURN)。
关于EXC_RETURN 的定义如下,其为 32 位数值,高 28 位置 1,第 0 位到第三位则提供了异常返回机制所需的信息,如下表所示。可见其中第 2 位标示着进入异常前使用的栈是 MSP还是PSP。在异常处理过程结束时,MCU 需要根据该值来分配 SP 的值。这也是本方法中用来判断所使用堆栈的原理。
notion image
我的程序R14(LR) = 0xFFFFFFE9,注意这里R13(SP)的值实际上与MSP的值一致。
notion image
Keil菜单栏点击“View”——“Memory Windows”——“Memory1”,在“Address”地址栏中输入MSP的值:
notion image
0x200005F8,然后在对应行里找到地址。地址一般以0x08开头的32位数。注意从右往左看。
发生异常后我们可以首先查看LR寄存器的值,确认当前使用的堆栈是MSP还是PSP,然后找到相对应的堆栈指针,并在内存中查看相对应堆栈的内容,内核将R0~R3,R12,LR,PC(Return address),xPRS寄存器依次入栈,其中堆栈后第25个字节到28字节PC(Return address)即为发生异常前PC将要执行的下一条指令地址。
💡
LR存储的是函数的返回地址,即函数被调用完后的下一条指令地址。而PC存储的是下一条即将要执行的指令的地址
本例中,LR地址为0x08000E49,PC地址为0x08000E48:
notion image
设置后如下图:
notion image
【补充】
从Memory1中可以看到0x200005F8开始的第一个32位值就对应了R0的值,后面也分别对应了R1,R2,R3,R12
notion image
随后在Keil菜单栏点击“View”——“Disassembly Window”(这个反汇编窗口一般默认已经开启),在“Disassembly”窗口中右击,在下拉菜单中选择“Show Disassemblyat Address…”。在弹出框“Show Code atAdress”的地址框中输入地址0x08000E49进行搜索,然后就会找到相对应的代码。这里的代码就是进入循环中断之前的情况。仔细查看附近区域的相关代码来排查错误具体原因。
notion image
notion image
随后发现已经跳转到错误代码附近:
notion image

二、Keil Call Stack功能

在Keil菜单栏点击“View”——“Call Stack Window”弹出“Call Stack + Locals”对话框。
然后在对话框中HardFault_Handler处右键选择“Show Caller Code”,就会跳转到出错之前的函数处,仔细查看这部分函数被调用或者内存使用情况。(有时不一定有效)
notion image

原理解释:

可参考Cortex-M3权威指南第九章:中断的具体行为
解释一下当系统产生异常时 MCU 的处理过程:
  • 有一个压栈的过程,若产生异常时使用 PSP(进程栈指针),就压入到 PSP 中,若 产生异常时使用 MSP(主栈指针),就压入 MSP 中。
  • 会根据处理器的模式和使用的堆栈,设置 LR 的值(当然设置完的 LR 的值再压栈)。
  • 异常保存,硬件自动把 8 个寄存器的值压入堆栈(8 个寄存器依次为 xPSR、PC、LR、 R12 以及 R3~R0)。如果异常发生时,当前的代码正在使用 PSP,则上面 8 个寄存 器压入 PSP; 否则就压入 MSP。
当系统产生异常时,我们需要两个关键寄存器值,一个是 PC ,一个是 LR (链接寄存 器),通过 LR 找到相应的堆栈,再通过堆栈找到触发异常的 PC 值。将产生异常时压入栈 的 PC 值取出,并与反汇编的代码对比就能得到哪条指令产生了异常。
notion image
再解释一下关于 LR 寄存器的工作原理。如上所述,当 Cortex-M3 处理器接受了一个 异常后,寄存器组中的一些寄存器值会被自动压入当前栈空间里,这其中就包括链接寄存 器(LR )。这时的 LR 会被更新为异常返回时需要使用的特殊值(EXC_RETURN)
关于 EXC_RETURN 的定义如下,其为 32 位数值,高 28 位置 1,第 0 位到第三位则提供了异常 返回机制所需的信息,如下表所示。
可见其中第 2 位标示着进入异常前使用的栈是 MSP 还是 PSP。在异常处理过程结束时,MCU 需要根据该值来分配 SP 的值。
notion image
不难发现,这里FFF9中的 9 对应的就是1001,即第二位为0,表示使用主栈
FFFD同理,D 对应1101,即第二位为1,表示使用从栈
notion image
notion image

基础补充:

一、Cortex-M3寄存器介绍

Cortex-M3处理器拥有R0~R15的寄存器组。其中R13作为堆栈指针SP。SP有两个,但是在同一时刻只能看到一个,就是所谓的“banked”寄存器。
notion image
notion image
notion image
notion image
notion image

二、双堆栈指针MSP和PSP

Cortex-M3处理器拥有R0-R15的寄存器组。其中R13作为堆栈指针SP。SP有两个,但在同一时刻只能有一个可以看到,这也就是所谓的“banked” 寄存器。
拥有两个SP寄存器,意味着我们可以拥有两个函数调用栈:MSP和PSP
正是使用了双堆栈,即MSP和PSP,这极大的方便了OS的设计。
MSP的含义是Main_Stack_Pointer,即主栈 PSP的含义是 Process_Stack_Pointer,即任务栈(或进程堆栈,有的也叫从栈)
那什么时候使用MSP,什么时候使用PSP呢?
也就是说SP寄存器中的值在某一时刻到底是使用MSP的值还是PSP的值?
这是根据CONTROL寄存器的bit1来决定的:
  • 当CONTROL的bit1为0使用MSP(默认方式)
  • 当CONTROL的bit1为1使用PSP
notion image
在裸机开发中,CONTROL的bit1始终是0,也就是说裸机开发中全程使用MSP,并没有使用PSP。在执行后台程序(大循环程序)SP使用的是MSP,在执行前台程序(中断服务程序)SP使用的也是MSP。
在OS开发中,当运行中断服务程序的时候CONTROL的bit1是0,SP使用的是MSP;
当运行线程程序的时候CONTROL的bit1是1,SP使用的是PSP。
notion image
当 CONTROL[1]=1 时,线程模式将不再使用 MSP,而改用 PSP(handler 模式永远使用 MSP)。
这样做的好处在哪里?
原来,在使用 OS 的环境下,只要 OS 内核仅在 handler 模式下执行,用户应用程序仅在用户模式下执行,这种双堆栈机制派上了用场——防止用户程序的堆栈错误破坏 OS 使用的堆栈。
注:此时,进入异常时的自动压栈使用的是进程堆栈,进入异常 handler 后才自动改为 MSP,退出异常时切换回 PSP,并且从进程堆栈上弹出数据。(可以看下图)
【双堆栈指针在OS中的应用】 典型的OS环境中,MSP和PSP的用法如下:
  • MSP用于OS内核和异常处理。
  • PSP用于应用任务
notion image

拓展

当进入HardFault断点后,菜单栏Peripherals >Core Peripherals >FaultReports打开异常发生的报告,可以查看异常报告。
notion image

Keil异常报告

notion image
发生了总线 fault 后,我们将如何找出该 fault 的事故原因呢?
在这里,NVIC 提供了 若干个 fault 状态寄存器:
notion image
notion image
notion image
notion image