1 概述
[color=rgba(0, 0, 0, 0.75)]Fault exceptions are triggered when the processor detects an error such as the execution of an undefined instruction, or when the bus system returns an error response to a memory access.[^1]
[color=rgba(0, 0, 0, 0.75)]Hardfault是ARM Cortex-M系列经常遇到且较难诊断的一种异常,其成因非常多,经常给广大电子工程师造成不小困扰。本文尝试列举了各种导致Hardfault的原因以及潜在的解决办法。
[color=rgba(0, 0, 0, 0.75)]在ARM处理器中,当程序运行出错,并且处理器检测到这一错误时,就会触发故障(fault)异常。在Cortex-M0中,只有一种故障异常处理机制:Hardfault handler。
[color=rgba(0, 0, 0, 0.75)]Hardfault handler基本是最高优先级的异常,优先级别为-1,只有不可屏蔽中断(NMI)可以抢占它。Hardfault handler有助于我们在调试阶段发现软件问题。在Hardfault handler中打断点,则当程序运行到断点处时,我们就会知道出现了故障异常。此时通过查看堆栈信息,可以追溯故障位置及产生原因。[^2]
2 故障成因
[color=rgba(0, 0, 0, 0.75)]导致Hardfault故障的原因基本可以分为两大类:电气类和软件类。
通常电气类的故障异常,异常发生时软件运行的位置不固定。且异常不稳定复现。当然也存在某些情况,比如软件运行至某些位置处,系统IO动作或者进入到高负载状态造成电源波动,从而引发异常,所以也会表现为软件运行到某些位置的函数时引发异常,但根本原因仍然是电气设计造成的。
软件类的故障异常,一般为软件写法不规范,通常可以准确定位异常发生位置。
2.1 不良的电气设计
[color=rgba(0, 0, 0, 0.75)]这是导致Hardfault故障最常见的原因。
电源供电是一切电子系统生存的基础,但其实电源不仅仅指5V或12V供电,芯片的供电实际是电源和地的差值。理想的电源是无波动的5V或12V,理想的地对大地的电阻应该是0。但实际的电子系统中,电源会随负载电流大小或系统中其他信号产生的干扰而波动,地线对地电阻也无所做到无穷小,因此电源和地线都会存在波动。一般我们在设计PCB时会尽量把接地做大面积以减小电阻。电源也应尽量远离强干扰源。并在靠近芯片引脚处放置去耦电容,滤除芯片供电的电源地波动。
芯片在设计阶段会考虑电源、温度和工艺的偏差,在设计阶段会留有一定的设计裕度,允许电源波动,允许芯片在一定温度范围内正常工作,允许因为工艺制造偏差导致某些电路速度较设计更快,有些电路速度较设计更慢。通常芯片设计在全温度范围内,理想电压±10%的范围内进行签核交付。因此也就意味着,如果电源波动太大,超出芯片的设计裕度,就可能造成芯片内部信号的不正确动作,从而引发故障异常。
2.2 非法指令
[color=rgba(0, 0, 0, 0.75)]比如处理器遇到一条未定义指令(非法指令)。这种情况,可能是因为flash本体故障,导致写入flash的数据发生变化,但是这个概率极低,仅仅是理论上存在这种可能。而且flash烧写时会进行读回校正,如果写入数据和读回数据校验不一致,烧写时就会报错。更多的可能是运行时电源地波动,或者芯片温度过高,导致处理器从flash读取数据时出现错误,从而导致处理器遇到指令异常。
另外,如果应用程序有擦写flash的动作也需要小心擦写区域。如果擦写了code区,也可能导致程序被改写为了非法指令。
亦或者是处理器因为堆栈结构被破坏异常跳转到了非指令区域,从而读到非法指令。
2.3 非法访问
[color=rgba(0, 0, 0, 0.75)]非法访问的情况也很多
[color=rgba(0, 0, 0, 0.75)]图 2-1 触发Hardfault异常的故障列举
2.3.1 访问地址未对齐
[color=rgba(0, 0, 0, 0.75)]比如word类型变量,需要地址按4byte对其,如果声明一个word变量位于0x20000402,则会造成非对齐访问。或者声明一个halfword变量位于0x20000401,而声明一个halfword变量位于0x20000402则是合法的。通常编译器可以自动处理变量的地址分配,但如果在C语言中使用类似
[color=rgba(0, 0, 0, 0.75)]__attribute__((at(0x20000402)))
[color=rgba(0, 0, 0, 0.75)]的绝对定位属性时,或者在汇编程序中直接使用如下ldr const的语法而未使用align将变量对齐
[color=rgba(0, 0, 0, 0.75)]ALIGNldr r1, =0x5aa56789
[color=rgba(0, 0, 0, 0.75)]则可能导致变量地址未对齐而导致访问异常。
以下代码通过整数变量指针访问绝对地址时,也可以产生非对齐异常,这是最可能产生非对齐访问异常的方式。
[color=rgba(0, 0, 0, 0.75)]int *ip;int tmp;ip = (int *)(0x20000003);tmp = *ip;
2.3.2 访问地址非法
[color=rgba(0, 0, 0, 0.75)]访问地址非法。比如有些MCU设计中,访问没有外设的地址空间,或者在外设未初始化完毕就进行访问,会导致外设向总线返回error response,这个跟芯片设计有关。某些外设可能在自身运行异常情况下也会向总线返回error response。
LKS芯片的bus不会返回error response,读取地址空洞通常返回0值,或者wrap back返回其他外设寄存器值,向地址空洞写入数据,可能写入无效,或者造成某个外设寄存器被无改写,需注意。
2.3.3 在无执行权限的地址空间执行程序
[color=rgba(0, 0, 0, 0.75)]Cortex-M0的地址空间分配如下图所示:
[color=rgba(0, 0, 0, 0.75)]图 2 2 Cortex-M0地址空间
[color=rgba(0, 0, 0, 0.75)]通常,某个地址有可读、可写、可执行三种属性。0x0000_0000~0x1FFF_FFFF地址通常为ROM/Flash,是可读可执行,0x2000_0000~0x3FFF_FFFF地址为RAM是可读可写可执行的。0x4000_0000~0x5FFF_FFFF是外设地址,为可读可写不可执行的,0x6000_0000~0x7FFF_FFFF为片外存储是可执行的。
如果跳转至0x4000_0000~0x5FFF_FFFF的外设地址执行则会触发异常。
2.4 非法使用断点指令
[color=rgba(0, 0, 0, 0.75)]比如在无debug模块,或者debug未使能情况下使用软件断点指令。
通常MCU都会包含debug模块。
2.5 编译异常
[color=rgba(0, 0, 0, 0.75)]也极其少见,但是有遇到过。如下图中的例子:红框中的程序在无条件分支跳转(B指令)之后是一条PUSH R4, 把R4入栈了。但是因为先分支跳转了,所以其实没有执行PUSH R4入栈。但是后面函数返回前却做了一个POP R4,导致栈指针出错。栈指针出错不会直接导致Hardfault异常,但通常会导致后续的异常跳转,当PC跳转至异常地址后会遇到非法指令从而触发Hardfault异常。
这种属于Keil编译器的问题。
本例中优化等级选1就没问题。0有问题。可以通过更改编译选项或者软件写法来试图绕过异常。
[color=rgba(0, 0, 0, 0.75)]图 2 3 Keil编译器产生的异常汇编程序
[color=rgba(0, 0, 0, 0.75)]图 2 4 Keil编译器产生的异常汇编程序(续)
[color=rgba(0, 0, 0, 0.75)]以上是处理器产生Hardfault异常的底层原因。而产生Hardfault异常的表层原因更多的是不良的软件编程习惯。从软件角度,产生hardfault的可能原因有:
[color=rgba(0, 0, 0, 0.75)](1)数组越界
(2)野指针
(3)未初始化硬件却开始操作,或无中断服务函数等
(4)任务堆栈溢出
其中(1)/(2)/(4)均会导致软件执行过程中破坏其他数据结构。(4)通常在程序链接阶段应该避免,如果ram空间某些区域用作软件加速用途装载了flash code,则应该小心剩余的ram空间是否足够满足堆栈需求。
3 处理器异常模型
[color=rgba(0, 0, 0, 0.75)]中断也属于一种异常。与中断服务的机制相同,故障异常导致处理器中断正常的程序执行,跳转进入异常处理函数。对于Cortex-M内核,架构采用错误异常的机制来检测问题,当核心检测到一个错误时,异常中断会被触发,并且核心会跳转到相应的异常终端处理函数执行,Cortex-M3/M4中错误异常的终端分为以下四种:
[color=rgba(0, 0, 0, 0.75)]HardFault
MemManage
BusFault
UsageFault
[color=rgba(0, 0, 0, 0.75)]其中HardFault为最常见的错误类型,并且,在没有开启其他异常处理的情况下,默认进入HardFault异常中断处理函数。[3]
Cortex-M0只有HardFault一种异常中断处理函数。
4 异常定位4.1 Cortex-M0
[color=rgba(0, 0, 0, 0.75)]Cortex-M0处理器为追求性价比,内核没有配备与故障相关的CSR寄存器,因此唯一可用于诊断的信息来着内核核心寄存器值。
[color=rgba(0, 0, 0, 0.75)]图 4 1命中Hardfault handler后处理器核心寄存器状态 首先需要确定当前使用堆栈是MSP还是PSP。 简单解释MSP和PSP区别,不关心可略过。 MSP是系统复位后(即其处于Handler Mode)的指定sp(vector table的前4Byte自动载入),用于处理异常中断。当结束Reset_Handler后,cpu进入正常运行状态(即其处于Thread Mode),仅在此状态下PSP才能被使用,当然MSP也可以使用。其后如有硬中断来临,则进入Handler Mode,如果硬件中断结束,则返回Thread Mode。
[color=rgba(0, 0, 0, 0.75)]图 4 2 Thread mode与Handler mode切换
[color=rgba(0, 0, 0, 0.75)]关于MSP和PSP的选用,其是通过CONTORL寄存器来配置,仅在Thread Mode下才可设置CONTORL寄存器。一般情况下,没有必要使用PSP,除非是有os存在时,MSP用于os内核的sp,而PSP用于thread级app的sp,这两个sp需严格分开[4]。
在编译器中,可以通过r13(R13)或sp(SP)来访问堆栈(具体是MSP和PSP由当时环境决定);也可以通过指定的MRS、MSR指令来访问MSP和PSP。
进入异常后链接寄存器 LR 中存放异常返回值 EXC_RETURN, 如果其 bit 2=0 那么用的就是 Main Stack,如果 bit 2=1,那么用的就是 Process Stack[3]。
[color=rgba(0, 0, 0, 0.75)]表 4-1 EXC_RETURN值
0xFFFF_FFF1 | | | 返回Thread Mode,并使用主堆栈(SP=MSP) | | 返回Thread Mode,并使用线程堆栈(SP=PSP) |
[color=rgba(0, 0, 0, 0.75)]
如果主程序在Thread Mode下运行,并且在使用MSP时被中断,则在服务例程中LR=0xFFFF_FFF9。主程序被打断前LR已被自动入栈。
如果主程序在Thread Mode下运行,并且在使用PSP时被中断,则服务例程中LR=0xFFFF_FFFD。主程序被打断前LR已被自动入栈。
本例中LR=0xFFFF_FFF9,所以使用的是MSP,见图 4 1。
[color=rgba(0, 0, 0, 0.75)]图 4 3 栈结构
[color=rgba(0, 0, 0, 0.75)]异常发生后会把进入异常前的 R0-R3,R12, LR, PC,PSR 寄存器值栈入 Main Stack 或Process Stack(取决于异常发生时使用的哪个栈,本例为MSP)。如图 4 3所示
R0(@0x2000_0648)=0x2000_0003
R1(@0x2000_064C)=0x2000_0268
R2(@0x2000_0650)=0x4001_20C0
R3(@0x2000_0654)=0x2000_0268
R12(@0x2000_0658)=0xFFFF_FFFF
R14(LR)(@0x2000_065C)=0x0000_03A5
R15(PC)(@0x2000_0660)=0x0000_039A
查看反汇编可以看到0x0000_039A,即发生Hardfault时PC位置,对应的指令是
[color=rgba(0, 0, 0, 0.75)]LDR r1, [r0, #0x00
[color=rgba(0, 0, 0, 0.75)]此时,R0=0x2000_0003,所以是非对齐加载word引发的异常,对应C代码在main.c 第65行。
[color=rgba(0, 0, 0, 0.75)]图 4-4 引发Hardfault异常的C代码及反汇编
[color=rgba(0, 0, 0, 0.75)]而此时堆栈中LR的值0x3A5(通常情况下LR的bit0是0,因为thumb是2字节对齐,不过一些指令需要将bit0置1,指示当前是在thumb状态下),是处理器从hardfault_trigger函数返回后继续执行的代码,对应地址0x3A4。
[color=rgba(0, 0, 0, 0.75)]BL.W SystemInit
[color=rgba(0, 0, 0, 0.75)]
图 4-5 堆栈LR值
[color=rgba(0, 0, 0, 0.75)]在main.c中我们可以看到hardfault_trigger();之后调用的是SystemInit()函数。见图 4 4。
[color=rgba(0, 0, 0, 0.75)]图 4-6 函数调用
[color=rgba(0, 0, 0, 0.75)]从Call Stack + Locals界面也可以看到我们是在main函数中调用的hardfault_trigger然后进入的异常。
4.2 Cortex-M3/4
Cortex-M3/M4中,内核提供了更多关于异常的信息。但异常位置定位方式与Cortex-M0类似。
[color=rgba(0, 0, 0, 0.75)]图 4 7 Cortex-M3/M4中的FAULT REPORT
5 非调试模式下核心寄存器的查看[color=rgba(0, 0, 0, 0.75)]如果故障出现时并未进行调试,则无法通过Keil界面查看核心寄存器的当前值。因此,我们在lksflash_v1.3.6版本开始提供了核心寄存器查看功能,如下图工具栏中的“核心寄存器”按钮。在连接JLink/ULink及目标板后,可以通过该按钮随时查看。 [color=rgba(0, 0, 0, 0.75)]
|
|