声明:非原创,只是搬运到优快云平台作为自己的学习笔记,方便以后复习回顾
原创 xinggaoyong Smart Agriculture 2022年05月11日 22:04 北京
在使用Keil进行STM32开发过程中,经常遇到HardFault硬件异常,导致程序卡死。HardFault异常比较隐匿,如何快速定位产生HardFault异常前的问题代码段是解决异常的关键。该文总结了发生HardFault异常时,定位问题代码段的三种方法。前两种是在反汇编文件中,根据LR寄存器的值来确定问题代码段的位置。最后一种方法是通过KEIL仿真调试环境中函数调用栈直接定位问题代码段的具体位置。
❞
1、产生HardFault硬件异常的几个操作
-
数组越界操作:进行数组操作时,发生了越界访问。
-
堆栈溢出,程序跑飞:当程序函数调用较深,且每个函数中有较多的局部变量时,如果栈空间设置的比较小,很容易产生栈溢出,致使程序跑飞。
-
非法地址访问:在使用指针时,引用
NULL
指针或其他寄存器非法地址时有可能会导致错误,产生HardFault
硬件异常。 -
中断处理错误。
因此,当产生HardFault
硬件异常时,定位到发生HardFault
前的问题程序片段后,可以查看程序片段中是否有发生数组越界,指针操作是否合法,是否定义比较大的局部变量数组(栈溢出),一般就是这几种错误。
2、快速定位发生HardFault硬件异常三种方法
2.1、Coretex-M3架构芯片异常处理
以下内容参考书籍The definitive guide to ARM Cortex-M3 and cortex-M4 Processors(Third Edition)
中第8章---->深入了解异常处理。
使用STM32CubeMX
创建工程时,默认使用的是HAL
库,HAL
库中使用C函数处理异常。
在使用C函数处理处理异常时,异常机制需要在异常入口处自动保存R0~R3、R12、LR、PSR,并在异常退出时将它们恢复,这些都要由处理器硬件控制。这样,当返回到被中断的程序时,所有寄存器的数值都会和进入中断时相同。另外,与普通的C函数调用不同,返回地址(PC)的数值并没有存储在LR中。异常机制在进入异常时将EXC_RETURN代码放入了LR中,当利用BX、POP或存储器加载指令(LDR或LDM)被加载到程序寄存器中时,该数值用于触发异常返回机制。因此,异常流程也需要将返回地址保存。
EXC_RETURN位域表
从EXC_RETURN位域表可以知道,EXC_RETURN值的不同,代表的含义不同:
-
EXC_RETURN = 0XFFFFFFF9,使用MSP,中断返回用户程序。
-
EXC_RETURN = 0XFFFFFFFD,使用PSP,中断返回用户程序。
-
EXC_RETURN = 0XFFFFFFF1,使用MSP,从中断返回另一个中断。
因此,进行KEIL仿真时,通过查看EXC_RETURN(也即LR寄存器)的值,我们就可以判断出,错误是发生在中断中,还是发生在其他函数中,同时判断使用的是MSP(主栈)还是PSP(进程栈)。MSP、PSP中保存的是栈帧的起始地址,根据栈帧结构和起始地址就可以确定发生异常前LR的值。
栈帧:在异常入口处被压入栈空间的数据块为栈帧。发生异常时,R0~R3、R12、LR、PSR会被压入栈。下图是栈帧结构:
图1 在不需要或禁止双字栈对齐时,Cortex-M3或Cortex-M4(无浮点单元)处理器的异常栈帧
2.2、如何确定LR链接寄存器的值
为什么要确定LR寄存器的值?
LR(Link Register)链接寄存器,在Coretex-M3架构中有两种用途:一是用来保存子程序的返回地址;二是当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2)。因此在异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行,可以确定发生异常前,处理器正在运行的程序片段。因此,要想确定发生Hardfault异常前,处理器正在运行的程序片段,就必须确定LR寄存器的值。
确定LR寄存器值的步骤:
-
调试开始前先在
HardFault_Handler
函数while(1)处
打断点。 -
运行到断点处时,查看右侧LR寄存器的值(特别注意,此时LR是保存的EXC_RETURN的值),根据寄存器值确定使用的是主栈指针MSP还是进程栈指针PSP(图中EXC_RETURN = 0xFFFFFFF9,表明使用的是主栈)。
-
查看Banked中对应栈指针的值,这里MSP = 0x20000678。
-
调出Memory1窗口,查看以0x20000678为首地址的栈帧内容。
-
从首地址开始,第六个08开头的数据就是实际LR寄存器的值。
图2 如何确定LR的值
2.3、通过反汇编文件根据LR的值确定发生异常前运行的程序片段
2.3.1 配置KEIL生成反汇编文件
根据图3,在Run#1
一栏中填写如下指令,编译工程生成扩展名为dis的汇编文件。
图3 配置KEIL自动生成反汇编文件
使用Notepad++打开生成的反汇编文件,查找LR寄存器的值0x08000DA1,「有可能查不到」。因为ARM-UAL( Unified Assembly Language)中规定,操作数的bit0决定该条指令使用Thumb指令集还是ARM指令集。
-
bit0 = 0时,使用ARM指令集
-
bit0 = 1时,使用Thumb指令集
因此,可以忽略bit0,也即查找0x08000DA0,发现代码片段在main函数中,然后去main函数中去查看代码有没有用发生数组越界,非法地址访问等错误。
图4 在反汇编文件中查找LR,定位问题代码段
main函数异常代码如下,导致错误的原因是数组访问越界。主函数中定义了testbuff
数组,长度为10。但是在主循环中,累加赋值时,并没有限制变量i
的范围,导致数组访问越界,产生了HardFault
异常。
2.3.2 直接在调试时调出反汇编代码窗口进行定位
在调试界面工具栏View-->Disassembly Window,调出反汇编窗口,然后在该窗口中右击选择下图中的④Show Disassembly at Address...,输入要查找的LR地址0x08000DA0,点击Go To,就会跳转到对应的代码区域(反汇编代码和代码区同步联动)
图5 利用反汇编调试窗口定位问题代码段
2.4、根据程序调用栈直接定位问题代码段
-
在调试界面右下方选择Call Stack+Locals。
-
选择HardFault_Handler,鼠标右击。
-
在右键菜单选择Show Caller Code,程序会直接跳转到进入HardFault_Handler函数之前的代码片段。
图6 使用程序调用栈,直接定位问题代码段
3、HardFault调试实例
本例是在实际开发中遇到的一个问题,场景是在开发GPS数据解析程序时,串口3中断中利用函数指针调用GPS数据解析处理回调函数,但是由于GPS芯片一上电就会不间断的向串口发送数据。此时如果已经打开串口中断,但是没有设置回调函数,使用函数指针调用回调函数时就会发生HardFault
异常,因为此时函数指针的值是0。
3.1、制造HardFault异常
1、在函数MX_USART3_UART_Init
中,添加25~27行代码,让串口3初始化时就打开接收中断。
2、主函数中故意在函数SetGPSInputDataProcessCallBack
之前延时1000ms,制造异常发生的条件。
3、因为在串口初始化时,串口中断已经打开,在延时1000ms过程中,GPS已经向串口发送数据,产生了串口中断,但此时还没有设置回调函数,函数指针__GPSInputProcessCallback
的值是0,当程序进入串口3中断执行到17行时,就会发生HardFault异常。
4、调试之前,先在main.h
中,将宏TEST_INVALID_ADDRESS_ERR
设置为1,宏TEST_STACK_ERR
设置为0。
3.2、异常调试
1、根据2.4中的方法,直接利用函数调用栈定位问题代码段,可以发现,系统自动定位在第55行函数指针__GPSInputProcessCallback
使用的地方,然后在定位处打断点。
❝PS:此时可以看到LR寄存器的值,也就是EXC_RETURN的值为0xFFFFFFF1,说明使用的是MSP,从中断返回另一个中断,也就是说,异常之前代码是在中断中产生的。
❞
图7 根据程序调用栈直接确定问题代码段
2、重新调试,程序停止在上一步骤设置的断点处,此时光标移动到函数指针__GPSInputProcessCallback处,会发现,系统提示函数指针__GPSInputProcessCallback
的值是0x00000000。
图8 程序代码段错误原因分析
3、分析为什么函数指针的值为0x00000000,由于是示例程序,前面已经给出了答案。但是在实际的调试过程中,是需要根据自己的程序进行分析的。本例程序的具体解决方案可以分为两种:
-
第一种解决方法:不在串口初始化时打开中断,在函数
MX_USART3_UART_Init
中去掉25~27行代码。在设置回调函数指针后再打开串口中断。 -
第二种解决办法:在调用函数指针时进行判断,添加第17行代码即可。