> 技术文档 > STM32进入HardFault_Handler时,如何找到问题所在?_stm32进入hardfaulthandler

STM32进入HardFault_Handler时,如何找到问题所在?_stm32进入hardfaulthandler

        刚开始初学STM32时,每当程序进入HardFault_Handler时,心中就会犯怵,不知怎么解决,当时网上搜到的一些大佬的帖子,问题也能得到解决,但不知道底层机理是什么,所以对这一块的掌握一直不深,之前跟着导师做项目,大冬天在外面调试设备时,又出现这样的报错,当时阴差阳错发现是FreeRTOS任务栈溢出了,现场问题解决了,后面也暗自下决心,要把这类问题深学一下。在看了Cortex-M3内核手册和STM32芯片手册以及大佬们的帖子后,整理一下,将我这个问题的解决方案做个记录。如有不对,烦请各位大佬指出!

1、前置知识

1.1、为什么会进入HardFault_Handler?

        HardFault_Handler 是 ARM Cortex-M(包括 CM3)内核的一种硬件级保护机制,当代码运行到错误地方,防止非法操作进一步导致系统完全失控。增强系统的鲁棒性。

        常见导致该状况的原因有以下几点:

  1. 内存访问违规(如数组越界)
  2. 指令执行错误
  3. 栈溢出或破坏
  4. 中断处理错误等

1.2、如何定位出错的点?

        在查阅Cortex-M3内核手册时,了解到当系统异常发生时,内核做了什么。

首先了解一下Cortex-M3的内核寄存器

其中R0~R12叫做通用寄存器,作用就是程序运行时进行数据操作,简单来说我要运行1+2=3,我就把1放进R0,2放进R1,计算后的结果放进R3。

R13叫做堆栈指针寄存器,Cortex-M3拥有两个堆栈指针:MSP和PSP,但在同一个时刻只能有一个在使用。

堆栈指针主要用来存放代码运行时产生的一些临时变量,函数栈帧,中断发生时的上下文数据等

MSP:单片机上电初始化时,执行内核代码所用到的堆栈指针,当中断发生时,也运用这个堆栈指针。如果代码跑的是裸机,那代码全程运用MSP

PSP:如果你的代码移植了RTOS,那么当运行任务代码时,将使用PSP作为堆栈指针,主要存放用户任务程序代码执行时产生的变量,函数调用栈等数据。

R14叫做链接寄存器,当运行主程序时,调用了一个子程序,R14存放的就是其返回地址。

但当内核进入异常或中断时,LR的用法将会被更新,下面放上手册中的解释:

看一下上图中提到的“EXC_RETURN”值的含义

R15叫做程序计数器,逻辑上理解的是程序执行当前代码的地址,实际中,因为内核的流水线(Pipline)工作模式,导致PC存储的实际是程序代码下一条的地址。

这里还有一个寄存器叫做xPSR,程序状态寄存器(下面会提到),存储程序运行的状态,以及当前执行的中断号等。

1.3、小端模式

        在STM32中,内核采用的是小端模式存储架构,即存储一段数据,低字节存放在低地址,高字节存放在高低址(大端模式与其相反)。

例:数据0x12345678

在内核中的存储如下所示

地址:0x00 0x01 0x02 0x03 数据:0x78 0x56 0x34 0x12

1.4、栈的生长方向

        首先知道,栈在当前主流架构中(如X86、ARM)中,其栈顶元素的地址是高地址,每当有数据存入(入栈),每入栈一个数据其元素地址是比前一个入栈的地址要低的。也就是说,栈是从高低址向低地址生长的

2、当中断/异常发生时,内核做了什么?

手册中提到:

        简单理解就是,当中断或者异常发生时,内核会将xPSR、PC、LR、R12、R3、R2、R1、R0这些寄存器按照上述顺序依次存入堆栈,这些寄存器中的值是中断/异常发生前一刻的值,叫作保护现场,当中断服务函数/异常处理函数执行完后,将这些寄存器进行出栈,恢复到进入中断前的下一条代码,继续执行。

        根据上述知识的介绍,PC寄存器存的就是被中断打断的指令的下一条指令地址,所以我们同个定位PC寄存器中地址所指向的代码,就可以精准定位或定位在问题代码附近。

        知道以上知识点,题中的问题就可以得到解决了,下面举两个例子来进行讲解

3、案例

3.1执行裸机代码时,如何定位?

        在主函数中写入以下测试代码

#include \"stm32f10x.h\"  // Device header#include \"OLED.h\"#include \"Serial.h\"#include \"String.h\"int main(){Serial_Init();uint8_t arr[5] = {0x01, 0x02 ,0x03, 0x04, 0x05};for(int i=0;i<100000;i++){arr[i] = 0x06;}printf(\"abc\");while(1){}}

【编译】,【烧录】,进入【调试模式】,点击运行再点击终止运行:

可以发现,这段代码当循环次数大于数组长度时,就会发生数组越界,这就会让内核进入HardFault_Handler:

打开【寄存器窗口】:

可以看到我们刚才提到的这些寄存器:

根据【前置知识】提到,当R14(LR)的值等于0xFFFFFFF9时,代表进入中断处理函数之前,程序执行用的是MSP堆栈指针,所以我们点开Banked目录,查看MSP的值,图中MSP的值是0x200003E8。

在【Memory】的输入框中输入MSP的值,可以看到中断发生后,被压栈的八个寄存器的值,按照小端模式和栈地址生长顺序,可以找出每个值所对应的寄存器:

【补充】这里可以在下图的空白区域,按如下操作,将显示模式选择为【Unsigned Long】

将PC寄存器的值复制,按下图操作(在Disassembly窗口空白朱右击鼠标):

 在弹出的输入框中输入【0x+PC的值】,这里注意,如果不加0x的话会被认为无效地址,不会进行程序跳转。

 然后就可以定位到出错的的点了:

3.2搭载RTOS时,如何定位?

        方法都一样,只不过在选择堆栈指针时查看PSP就好了:

我创建了一个任务,里面执行如下代码:

void myTask_Arr(void *arg){uint8_t arr[8] = {0x00,0x01,0x02,0x03,0x04};while(1){for(int i=0;i<100000;i++){arr[i] = 0x06;}vTaskDelay(50);}}

 显然,这也会造成数组越界。按照上述流程,一样编译,烧录,进调试。

直接观察寄存器的值

 此时R14(LR)的值是0xFFFFFFFD时,代表进入中断处理函数之前,程序执行用的是PSP堆栈指针,所以我们点开Banked目录,查看PSP的值,图中PSP的值是0x20001038。

在【Memory】的输入框中输入MSP的值,可以看到中断发生后,被压栈的八个寄存器的值,按照小端模式和栈地址生长顺序,可以找出每个值所对应的寄存器:

将PC寄存器的值复制,按下图操作(在Disassembly窗口空白朱右击鼠标):

 在弹出的输入框中输入【0x+PC的值】,这里注意,如果不加0x的话会被认为无效地址,不会进行程序跳转。

 然后就可以定位到出错的的点了:

4、补充

        在一开始调试,发现当循环次(如循环100次)比较小的时候,并不会发生进入HardFault_Handler, 后续看了一些大佬的讲解,数组越界进入HardFault_Handler根本是堆栈溢出,威胁到了内核代码的正确执行,所以才会触发这个保护机制。举个例子,程序在跑的时候,函数堆栈用来存放代码执行所需的临时变量,返回地址,即函数运行现场。可能当循环次数较小时,并不会威胁到内核,一旦越界访问次数变多,一次又一次的越界,内存溢出威胁到了内核,对函数运行现场进行破坏,进而触发保护机制。

        如上图所示,如果循环次数较小的话,数组越界访问所造成的内存溢出可能影响不到内核运行现场,一旦循环次数加大,内存过度溢出,损坏了内核运行现场,将会触发保护机制。

5、结语

        上述内容,是我日常学习与工作遇到的问题,查阅了相关资料以及视频讲解,加上自己理解所写,用作学习记录,在这和大家分享,如有不对,烦请指出,大家一起学习交流,进步!