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、如何定位出错的点?
在查阅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、结语
上述内容,是我日常学习与工作遇到的问题,查阅了相关资料以及视频讲解,加上自己理解所写,用作学习记录,在这和大家分享,如有不对,烦请指出,大家一起学习交流,进步!