赞
踩
HardFault(硬件错误)是一类在嵌入式系统开发中较为常见的系统异常,优先级仅低于复位和NMI(不可屏蔽中断)。当系统运行过程中遇到了某些错误时程序就会跳转至HardFault_Handler函数中,引发程序故障进而影响程序的正常运行。
HardFault的产生原因可依照来源被分为外部因素和内部因素,主要有以下几种:
外部因素
内部因素
一般因外部因素造成HardFault的可能性较低,大多数都是软件层面的问题。所以遇到HardFault问题时,基本先从内部因素着手去排查。
在嵌入式开发过程中,经常会遇到HardFault异常引发的系统Core dump问题,在企业级项目开发中这种情况更为常见。此类错误是较难处理的,原因有三点:
想要快速准确地定位解决HardFault问题需要对Cortex-M系统架构有着一定的理解,下文将以Cortex-M3为例介绍Cortex-M的架构内容。
从编程模型的角度上看,Cortex-M3处理器有两种操作状态和两个操作模式:
操作状态
操作模式
下图展示了程序在运行时,Cortex-M3处理器的操作状态和模式的转换关系。
![![[Pasted image 20230701105510.png|850]]](https://img-blog.csdnimg.cn/ebb75017627241b6a95499471633d7a8.png)
但对于许多简单的应用,非特权线程模式不会被使用,此时转换关系即可简化为下图所示。
![![[Pasted image 20230701105728.png|850]]](https://img-blog.csdnimg.cn/f5d5a95c17024642bd3ca8c40986ef5a.png)
寄存器是一种时序逻辑电路,在CPU内部暂时存放参与运算的数据和运算结果。与其它几乎所有寄存器类似,Cortex-M3处理器在处理器内核中有多个执行数据处理和控制的寄存器,如下图所示。
![![[Pasted image 20230701110825.png]]](https://img-blog.csdnimg.cn/f84618ec67d1427fa0e94f3d30a2ab82.png)
从图3可以看出,Cortex-M3寄存器组中共有16个寄存器,其中13个为通用目的寄存器,其它三个则有特殊用途。
R0~R12
寄存器R0~R12为通用目的寄存器,前8个(R0~R7)也被称作低寄存器。由于指令中可用的空间有限,许多16位指令只能访问低寄存器,R8~R12也被称作高寄存器,32位指令可以对其进行访问。
Cortex-M3遵循ATPCS(ARM Thumb Procedure Call Standard)原则,该原则规定了寄存器组的使用规则,R0~R3用于向子程序传递参数,这时寄存器可以记作:A1~A4,被调用的子程序在返回前无需恢复寄存器R0~R3的内容。R4~R11用于保存子程序的局部变量,此时寄存器记为V1~V8,子程序在进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值,对于子程序中没有用到的寄存器则不必执行这些操作。因此,R4~R11也被称作“被调用者保存寄存器”。R12为内部调用暂存寄存器,记作IP,在过程调用期间,可以将它用于任何用途,被调用函数在返回之前不必恢复R12。
R13
R13为栈指针(SP),可以通过PUSH和POP操作实现栈存储的访问。从物理存储的角度上看,存在两个栈指针,分别为MSP(主栈指针)和PSP(进程栈指针),其中MSP适用于系统复位(Thumb状态)或处理器处于处理模式时,而PSP只能用于线程模式。栈指针的选择由特殊寄存器CONTROL决定。
在同一时刻,这两个栈指针只会有一个可见。在简单的不带有OS的嵌入式系统中,线程模式和处理模式都可以只使用MSP,如下图所示。在中断发生后,处理器在进入中断服务程序(ISR)前会首先将多个寄存器压入栈中,在中断退出后,通过出栈操作恢复进入中断前的寄存器内容。整个过程仅使用到主栈指针MSP。
![![[Pasted image 20230701111201.png|800]]](https://img-blog.csdnimg.cn/faebfb58cc394d6e90deffedd8e25cd6.png)
若嵌入式系统带有OS,它们通常会将应用任务和内核所用的栈空间分离开来,PSP此时将被用于线程模式。当中断发生时,会用PSP完成压栈操作并实现栈指针的切换(PSP->MSP),当中断退出时,将完成栈指针的切换(MSP->PSP)并用PSP完成出栈操作,如下图所示。这种设计最大程度确保了不同模式的栈空间不会被相互干扰。
![![[Pasted image 20230701111304.png|800]]](https://img-blog.csdnimg.cn/d91c8060535b4e028ee3e94e85b8298b.png)
R14
R14被称作链接寄存器(LR),当执行了函数或子程序调用后,LR的数值会自动更新。若某函数需要调用另一个函数或子程序,为了防止上一级函数返回地址丢失,系统会将当前R14的值保存在栈中。
在异常处理期间,LR也会被自动更新为特殊的EXC_RETURN(异常返回)数值,EXC_RETURN的位段含义见下表所示,之后该数值会在异常处理结束时触发异常返回。
| 位段 | 含义 |
|---|---|
| 31:4 | EXC_RETURN标识符,默认全为1 |
| 4 | 栈帧类型,1(8字)或0(26字) |
| 3 | 0(返回进处理模式)1(返回进线程模式) |
| 2 | 0(返回后使用MSP)1(返回后使用PSP) |
| 1 | 保留,默认为0 |
| 0 | 0(返回ARM状态)1(返回Thumb状态) |
R15
R15为程序计数器(PC),读R15将返回当前指令地址加4,即PC总是指向“正在取指”的指令。
除了寄存器组中的寄存器,Cortex-M3还有若干特殊寄存器,这些寄存器只能通过MSR和MRS指令访问。简要介绍如下:
XPSR
xPSR为程序状态寄存器,由APSR、EPSR和IPSR组成,作用是为ALU提供算数标志位(负标志、零标志、进位标志、溢出标志和饱和标志)以及异常编号等。
PRIMASK
用于中断和异常屏蔽,位宽为1,默认值为0。当被置位为1时,表示允许NMI和HardFault异常,其它所有中断都会屏蔽。
FAULTMASK
用于中断和异常屏蔽,位宽为1,默认值为0。当被置位为1时,表示仅允许NMI异常,其它所有中断都会被屏蔽。
BASEPRI
用于中断和异常屏蔽,位宽最多8位(取决于芯片的优先级位数),默认值为0。当它被设置为非0时,它会屏蔽具有相同或更低优先级的异常(包括中断)。
CONTROL
控制寄存器定义了线程模式的访问等级(位0,nPRIV)和栈指针的选择(位1,SPSEL)。在Cortex-M4中,还有一位表示当前上下文是否使用浮点单元。
Cortex-M3地址总线为32位,存储器空间大小为2^32Bit = 4GB。Cortex-M3的4GB地址空间根据实际用法又被分为了多个存储器区域,如下图所示。这种架构安排具有很大的灵活性,存储器区域可用于其它目的。例如,程序可在CODE区,SRAM区和外部RAM区中执行。
![![[Pasted image 20230701113830.png|800]]](https://img-blog.csdnimg.cn/aba443c8bf38449f8f6ca695314e1ff6.png)
外设存储器区域主要用于管理片上外设,外部设备区则用于片外外设等其它存储器。系统存储器区域可分为以下几个部分:
所有的Cortex-M设备均按照此种架构安排设计芯片,这样可以提高不同Cortex-M设备间的软件可移植性和代码可重用性。
当一个函数调用另一个函数时,系统首先需要保存该函数的特定信息,如局部变量,返回地址,这些被保存的函数信息又被称作函数现场。当被调用函数退出时,根据保存的函数信息,恢复函数现场并继续向下执行程序。通常情况下,我们将保存函数信息的内存区域称为函数栈,栈就是函数的现场。
栈是存储器使用的一种线性数据结构,它由一块连续的内存和一个栈顶指针组成。Cortex-M3采用的栈模型为向下生长的满栈,满栈指的就是栈指针SP始终指向栈顶(最后一个被压入栈的32位数值)。
ARM处理器将系统主存储器用于栈空间操作,使用PUSH指令往栈中存储数据和POP指令从栈中读取数据,两个指令分别对应着函数现场的保存和函数现场的恢复,现将其过程简要介绍如下:
PUSH操作:(1)SP自减4(SP = SP - 4)(2)在SP指针处存入数据
![![[Pasted image 20230701114240.png]]](https://img-blog.csdnimg.cn/5fa91e119619477ca87a87c477bad765.png)
POP操作:(1)读出SP指针处数据(2)SP自增4(SP = SP + 4)
![![[Pasted image 20230701114416.png]]](https://img-blog.csdnimg.cn/326a48d24f6e49d0b7bcd9576cf0652d.png)
栈回溯法的核心思路是在程序进入异常后手动获取进入异常前的函数现场,进而得知系统执行哪一行代码出现了错误和函数的调用关系。
栈是函数的现场,获取函数现场即分析栈的存储内容。有两种情况下处理器会向栈中压入数据,一种情况是函数调用,另一种情况是程序进入中断或异常。两种情况下系统往栈中压入的内容不一致。
函数调用
从汇编的角度去看,当一个函数(称为A)调用另一个函数(称为B)时,首先会把当前函数的函数现场保存,将存储局部变量的寄存器(R4~R11)和函数返回地址(R14,LR)压入栈中,压栈顺序为:
L R − > R 11 − > . . . . . . − > R 4 LR->R11->......->R4 LR−>R11−>......−>R4
需要注意的是,只有存储了局部变量的寄存器才会被压入到栈中,未存储的不会被压入,且压栈顺序不会发生改变,假设A函数仅使用R4、R5、R6存储局部变量,则压栈顺序为:
L R − > R 6 − > R 5 − > R 4 LR->R6->R5->R4 LR−>R6−>R5−>R4
函数调用时并不需要将R0~R3,R12,xPSR寄存器压入栈中,根据AAPCS(ARM架构调用过程标准),这些寄存器被称作“调用者保存寄存器”,函数调用时可直接覆盖,无需保存。当函数执行完毕退出前,会执行出栈操作,将被调用者寄存器恢复为调用该函数之前的值。然后根据LR跳转到指定的指令继续向下执行。
程序进入中断/异常
不同于其它的处理器结构,Cortex-M的定位一开始就是为实时性、小体积容量的设计考虑的,所以在中断处理这一块,也做了一个十分有意思的设计——自动压栈处理。以往这个阶段都是通过人工操作写程序完成的,在Cortex-M上,当程序进入中断或异常时,芯片硬件会自动将8个通用寄存器压入当前栈空间,压栈的内容和顺序为:
x P S R − > P C − > L R − > R 12 − > R 3 − > R 2 − > R 1 − > R 0 xPSR->PC->LR->R12->R3->R2->R1->R0 xPSR−>PC−>LR−>R12−>R3−>R2−>R1−>R0
根据上述内容,以下述代码块为例,尝试分析当系统进入HardFault异常时函数栈结构。
static void A(void) { printf("Enter function A!\r\n"); ... B(); ... printf("Exit function A!\r\n"); } static void B(void) { printf("Enter function B!\r\n"); ... fault_func(); ... printf("Exit function B!\r\n"); } //该函数内无任何其它调用 static void Fault_Func(void) { ... //此处因某个编程错误进入HardFault异常 ... }
代码构建的函数调用关系为:
A ( ) − > B ( ) − > F a u l t _ F u n c ( ) A()->B()->Fault\_Func() A()−>B()−>Fault_Func()
当函数A调用函数B时,系统将函数A的函数现场压入栈中,当函数B调用函数Fault_Func时,系统再将函数B的函数现场压入栈中,当执行到Fault_Func函数中的异常代码时,芯片硬件自动将8个通用寄存器压入到栈空间,进入HardFault异常。最终得到的栈内容如下图所示。
![![[Pasted image 20230701162111.png|700]]](https://img-blog.csdnimg.cn/771a2d42cd004a9abbbbe2ba7ee8ac49.png)
需要注意的是,函数栈中并不会保存Fault_Func的函数现场,因为其不再有更深层次的调用。
明确了Cortex-M3的压栈机制,我们便可以使用栈回溯法分析MCU HardFault问题,这里选择IDE为Keil uVision5(简称为Keil5,下同),在调试状态下的具体步骤:
![![[Pasted image 20230701162500.png|250]]](https://img-blog.csdnimg.cn/9e963d4288524ffa961bb8730aedaabb.png)
![![[Pasted image 20230701162845.png|620]]](https://img-blog.csdnimg.cn/60c5dd789fa04786ac7b35a19e3d1b3d.png)
![![[Pasted image 20230701162858.png|620]]](https://img-blog.csdnimg.cn/7a133b10b8454f17a37f17b2e13c892c.png)
进一步分析函数调用关系。只知道执行哪行代码时进入HardFault还不足以解决问题,还需要进一步分析在哪个调用链上出错。在分散加载文件中获取代码的起始地址,并在编译输出窗口中获取代码占用大小,计算代码的存储地址区间。将栈中数据与地址区间匹配,找到每一级的函数返回地址,整理出函数调用关系。
打开Fault Report窗口,观察各寄存器标志位。结合代码异常位置和函数调用关系,分析HardFault的产生原因,修改代码错误。
下图展示了栈回溯方法查找和分析HardFault问题的流程。
![![[Pasted image 20230701163132.png|620]]](https://img-blog.csdnimg.cn/cf1ea648a98d4a31a365dd4a6836f28a.png)
CmBackTrace(Cortex Microcontroller Backtrace)是一款针对ARM Cortex-M系列MCU的错误代码自动追踪、定位、错误原因分析的开源库。该方法支持在非Debug模式下,自动分析故障类型和产生原因并定位发生故障的代码位置。结合addr2line工具,还能够输出错误现场的函数调用栈,支持多种主流编译器和操作系统平台。
在使用CmBackTrace分析HardFault问题之前,首先需完成开源库的移植,具体步骤如下:
![![[Pasted image 20230701164052.png]]](https://img-blog.csdnimg.cn/55b217a766554eb79373d30619027b8b.png)
| 配置名称 | 功能 | 备注 |
|---|---|---|
| cmb_println(…) | 错误及诊断信息输出 | 必须使能 |
| CMB_USING_BARE_METAL_PLATFORM | 是否使用裸机平台 | |
| CMB_USING_OS_PLATFORM | 是否使用操作系统 | |
| CMB_OS_PLATFORM_TYPE | 操作系统种类 | RTT/UCOSII/FREERTOS |
| CMB_CPU_PLATFORM_TYPE | CPU平台 | M0/M3/M4/M7 |
| CMB_USING_DUMP_STACK_INFO | 是否Dump堆栈信息 | |
| CMB_PRINT_LANGUAGE | 输出信息时的语言 | CHINESE/ENGLISH |
/* cmbacktrace调用接口 */ #ifdef USE_CMB char *vTaskName(void) { configASSERT(pxCurrentTCB != NULL); return pxCurrentTCB->pcTaskName; } /** * @brief 获取任务栈起始地址 * @return pxStack 任务栈起始地址 */ volatile uint32_t *vTaskStackAddr(void) { configASSERT(pxCurrentTCB != NULL); return pxCurrentTCB->pxStack; } uint32_t vTaskStackSize(void) { configASSERT(pxCurrentTCB != NULL); /* 根据栈向上/下生长划分出两种计算任务栈深度的方式 */ #if (portSTACK_GROWTH < 0) return pxCurrentTCB->uxStackDepth; #else return pxNewTCB->pxEndOfStack - pxNewTCB->pxStack + 1; #endif } #endif /* 在结构体tskTCB声明中添加 */ typedef struct tskTaskControlBlock { ... #ifdef USE_CMB StackType_t uxStackDepth; #endif ... } tskTCB; /* 在任务创建函数prvInitialiseNewTask中添加 */ static void prvInitialiseNewTask(...) { ... #if( portSTACK_GROWTH < 0 ) { pxNewTCB->uxStackDepth = ulStackDepth; ... } }
相较于繁琐的移植过程,CmBackTrace的使用方法极为简洁,步骤如下:
![![[Pasted image 20230701170528.png|800]]](https://img-blog.csdnimg.cn/e45515e2d4574d60941b7724fd2505d5.png)
CmBackTrace的目录结构如下图所示。
![![[Pasted image 20230701170659.png|800]]](https://img-blog.csdnimg.cn/ea3c8d0c6fb245ffa16efa4737f6874c.png)
CmBackTrace的核心功能主要在cm_backtrace.c中实现,该文件实现的主要功能有:CmBackTrace的初始化、转储当前堆栈信息,打印中断压栈寄存器,打印addr2line指令,错误原因分析等。当程序在某处出现异常时,CmBackTrace的执行步骤如下:
下图展示了CmBackTrace的执行流程。从上文解析和流程图可以看出,CmBackTrace的功能实现主要依靠于以下几点:(1)充分利用Cortex-M3的系统架构特性,代码的功能实现多依赖于系统寄存器和硬件,少有复杂的算法,降低了代码的复杂度和占用空间。(2)模拟栈回溯法分析过程,无冗余代码和输出信息,最大限度地提升代码执行效率。
![![[Pasted image 20230701171306.png|500]]](https://img-blog.csdnimg.cn/0225c0c95b6d4419989c86069271797b.png)
本文介绍了HardFault问题的背景和解决HardFault问题所需的理论知识,并在此基础上介绍了两种HardFault问题的查找和分析法,后续将会介绍两个HardFault实例现场,分别采用栈回溯法和CmBackTrace查找法对现场进行查找和分析,最后比较两种方法间的差异。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。