跳过正文

MCU hardfault调试指南

·177 字·1 分钟
jiladahe1997
作者
jiladahe1997
To see the world as it is, and to love it

简介
#

hardfault是单片机开发中经常遇到的“错误(exception)”,这类错误非常的难以定位和解决。 本文的目的是描述hardfault产生的原因、调试解决办法。


产生原因
#

什么是错误
#

任何编程语言都可能发生“错误(exception)”。最简单的例子,任何编程语言都不能执行除零操作,即 int a=1/0 一定会报错。

不同的编程语言在处理错误时有不同的处理方法。在C语言中,任何错误会直接导致程序崩溃;在Java、JavaScript、Nodejs等高级语言中,可以使用try-catch 对错误进行捕获,只有未捕获的错误才会导致程序崩溃。
例如以下代码可以正常运行:

public class Main
{
	public static void main(String[] args) {
	    try {
	        int a = 1/0;
	    } catch(Exception e) {
	        System.out.println("Catch error!");
	    }
		System.out.println("Hello World");
	}
}
//Output:
//Catch error!                                                                                                                              
//Hello World

图片.png

C++崩溃



图片.png

c语言程序崩溃



图片.png

java中崩溃



什么是Hardfault
#

在ARM的Cortex架构中,设计了一种中断叫做hardfault interrupt。 当发生了任何错误(exception)”的时候,会触发此中断,并调用 Hardfault_Handle

例如:非法地址访问、除0、等。






调试及解决办法
#

从原理上分析
#

从原理上看hardfault只是进入了一个特殊的中断,并且在进入中断时,将一些SR寄存器(Status Register)标志位置位,以提供非常少的必要的信息:例如错误类型。
https://www.keil.com/appnotes/files/apnt209.pdf

图片.png


解决办法
#

理论
#

通常要解决hardfault,要知道哪一行代码导致了hardfault。
因此解决办法是去“栈(Stack)”中寻找“帧(Frame)”:


帧(Frame): 当在函数A中执行函数B的时候,会将A的局部变量、地址等保存在栈中;然后跳转到B执行,执行完毕后,再从栈中取出A的地址并恢复A的局部变量,然后继续执行A函数。 上述过程中保存A的局部变量、地址等就叫做保存一帧。
http://kirste.userpage.fu-berlin.de/chemnet/use/info/gdb/gdb_7.html

当发生了hardfault的时候,去栈中寻找上一帧,从而寻找到最近执行的函数位置。



SP、LR、PC、PSR、MSP、PSP寄存器:
#

MSP和PSP寄存器区别: https://www.sciencedirect.com/topics/engineering/main-stack-pointer

  • SP Stack Pointer: 栈指针,用于表示当前执行函数所用的栈

  • LR Link Register:     链接指针,用于表示当前执行函数返回地址

  • PC Program Counter: 编程计数器,用于表示当前执行的指令地址

  • PSR program status register: TODO

  • MSP Main Stack Pointer: TODO

  • PSP Process Stack Pointer: TODO

MSP和PSP都是栈指针。 当跑裸机的时候,只使用MSP。 当跑操作系统(RTOS)的时候,会使用MSP+PSP。

SP指针一定等于MSP或者PSP指针两者之一。

裸机:
当跑裸机的时候,指使用一个栈,SP == MSP 等于程序所用的指针。

image.png


操作系统:
当跑操作系统的时候,由于涉及到多线程切换,会用到多个栈,所以除了SP指针,还需要PSP指针来协助。
PSP指针指向当前线程所用的栈,MSP指针指向主栈。




STM32上的实际操作
#

如下图所示,发生了一次hardfault,并且在hardfault handle中打了一个断点:

图片.png


因为这里使用了操作系统,所以应该看PSP指针所指向的栈:0x2401CC40

image.png


然后寻找帧Frame:
对栈上的数据进行观察,0x081xxxx的数据可能是地址。 比如上图中的 08161058。 反查这个地址:

image.png

无关,再观察到0817A559,再反查:
image.png

有关了,可以看到是这里有问题。



最佳实践:
#

帧中的地址
#

上述有提到,帧是由局部变量+函数返回地址等组成的,而我们要寻找其中的函数返回地址。 帧是没有明显规律的,只能通过经验观察。

值得一提的是,函数返回地址附近一定有跳转指令,即B BL BLX等汇编指令。
比如上图所示0x0817A559附近有BL.W指令。而0x08161058附近没有相关指令。所以前者是一个帧地址,后者不是。



TODO如果在hardfault中又产生了hardfault?

TODO: 从栈中寻找信息:
https://github.com/armink/CmBacktrace/blob/master/README_ZH.md




参考资料:
#

cortex-M3 M4 M7 hardfault: https://www.keil.com/appnotes/files/apnt209.pdf
gdb指令,通过帧寻找调用栈: http://kirste.userpage.fu-berlin.de/chemnet/use/info/gdb/gdb_7.html
MSP、SP、PSP寄存器的区别: https://www.sciencedirect.com/topics/engineering/main-stack-pointer
栈溢出导致的错误: https://blog.csdn.net/lanximu/article/details/18259829


cortex 寄存器汇总: https://developer.arm.com/documentation/dui0552/a/the-cortex-m3-processor/programmers-model/core-registers?lang=en
PSR寄存器: https://www.sciencedirect.com/topics/computer-science/current-program-status-register




附录:离线调试hardfault
#

上述的操作都是建立在使用jlink调试器的场景下。 在实际产品中,由于各种各样的奇怪因素,往往我们不能使用jlink调试器。

例如:硬件设计时没有拉出jlink引脚、结构设计时没有预留jlink口、设备在遥远的国外。等等因素。

所以这里介绍一下如何离线调试hardfault。 整体思路和 linux的 coredump 类似: 发生hardfault时,将内存、寄存器等信息存到文件里面,然后离线分析。

建议参考已有现成的方案:

zephyr的coredump: https://docs.zephyrproject.org/latest/services/debugging/coredump.html

具体代码:https://github.com/zephyrproject-rtos/zephyr/blob/main/kernel/fatal.c#L116