1. 汇编代码
HardFault_Handler PROC
;EXPORT HardFault_Handler [WEAK]
;B .
IMPORT Hard_Fault_Handler
TST LR, #4
ITE EQ
MRSEQ R0, MSP
MRSNE R0, PSP
B Hard_Fault_Handler
ENDP
使用该HARDFAULT检查,在汇编文件的HARDFAULT函数中插入此段(替换原本汇编文件中的HardFault跳转),适用于M3/M33/M4
在理解该段汇编段之前,应该明确产生硬件错误时,ARM内核在做什么
2. ARM内核的相关的寄存器
首先了解一下相关的寄存器,其中最为重要的寄存器来自ARM内核的重要外设模块,系统控制块(SCB)
一般的用户手册不会将该模块作为重点说明,因为这是"内核"的外设,而不是"CPU"的外设
比如调试窗口经常可以看到的R0~R15寄存器,作为ARM内核的核心寄存器,没有任何芯片的手册会强调这些内容
SCB(System Control Block)寄存器组包含了
- CPUID:CPUID基本寄存器。
- ICSR:中断控制状态寄存器。
- VTOR:向量表偏移寄存器,用于指定中断向量表的地址。
- AIRCR:应用中断/复位控制寄存器,用于软件复位等功能。
- SCR:系统控制寄存器。
- SHPR:系统处理优先级寄存器。
- SHCSR:系统处理器控制和状态寄存器。
- CFSR:可配置故障状态寄存器。
- HFSR:硬故障状态寄存器。
- DFSR:调试故障状态寄存器。
- MMFAR:内存管理地址寄存器。
- BFAR:总线故障地址寄存器。
- AFSR:辅助故障状态寄存器。
实现HARDFAULT检查仅需两个寄存器,分别是HFSR和CFSR
************************************************************
HFSR寄存器(Hard Fault Status Register)是一个1字(32bit)的寄存器,地址0xE000ED2C,有效位仅3位(其余均为保留位)
- VECTTBL(Bit 1): 向量表错误。如果设置为1,表示在访问中断向量表时发生了错误。
- FORCED(Bit 30): 硬故障。如果设置为1,表示硬件故障引发了硬故障异常。
- DEBUGEVT(Bit 31): 调试事件。如果设置为1,表示硬故障是由调试事件引发的。
************************************************************
CFSR寄存器(Configurable Fault Status Registers)是一个1字(32bit)的寄存器,地址0xE000ED28,实际上该寄存器由三个子寄存器组成,且每个寄存器都可以通过寻址独立访问
MMFSR(MemManage Fault Status Register): 该寄存器一字节(8bit)大小,包含存储器管理错误的状态标志位。存储器管理错误可能被触发的原因有: 尝试在权限不足的情况下访问特定权限区域,尝试向不可写的地址写入值,访问地址不存在实际存储器等
- MMARVALID(Bit 7): 表示MemManage故障地址寄存器(MMFAR)可用状态,32位寄存器MMFAR包含触发MemManage故障的内存地址。
- MSTKERR(Bit 4): 入栈执行导致一个或多个内存访问异常标志位
- MUNSTKERR(Bit 3): 出栈执行导致一个或多个内存访问异常标志位
- DACCVIOL(Bit 1): 尝试数据访问触发MPU或非法地址访问错误,如试图修改不可写的寄存器
- IACCVIOL(Bit 0): 尝试执行指令触发MPU或非法地址访问错误,如试图访问不存在存储器的内存地址
************************************************************
BFSR(Bus Fault Status Register): 该寄存器一字节(8bit)大小,包含总线错误的状态标志位
- BFARVALID(Bit 15): 表示Bus故障地址寄存器(BFAR)可用状态,32位寄存器BFAR包含触发Bus故障的内存地址。
- 总线错误通常由AHB汇报,可能被触发的原因有: 传送数据宽度不能被设备支持,总线传送未就绪,访问无效的存储器等
- STKERR(Bit 12): 入栈执行导致一个或多个总线异常标志位
- UNSTKERR(Bit 11): 出栈执行导致一个或多个总线异常标志位
- IMPRECISERR(Bit 10): 非精确总线错误标志位,堆栈返回的地址与总线错误无关
- PRECISERR(Bit 9): 精确总线错误标志位,压入堆栈的PC寄存器指向引起错误的指令地址
- IBUSERR(Bit 8): 总线错误发生标志位
************************************************************
UFSR(Usage Fault Status Register): 该寄存器半字(16bit)大小,包含使用错误的状态标志位
使用错误可能被触发的原因有:执行了协同处理器指令(M3内核可通过检测该位来软件模拟协同处理器),多重加载指令时未对齐,尝试将错误的EXC_RETURN写入PC寄存器等
- DIVBYZERO(Bit 24): 发生除0错误标志位
- UNALIGNED(Bit 23): 发生非对齐访问错误标志位,如ARM指令通常以字对齐(32Bits,4Bytes),PC寄存器存储的程序指针的Bit[1:0]通常都会保持为0
- STKOP(Bit 19): 发生堆栈溢出错误标志位
- NOCP(Bit 18): 无协同处理器标志,指示协同处理器禁用或不存在
- INVPC(Bit 17): EPSR(异常程序状态,异常处理过程中会存放在LR寄存器中,寄存器值被称为EXC_RETURN)寄存器异常行为置位标志位
- UNDEFINSTR(Bit 16): 执行未定义指令的错误标志位
************************************************************
3. 异常和异常处理过程
1. 异常的跳转
在进入异常服务程序后,硬件自动更新链路寄存器LR(R14,用于保存子程序返回地址)的值为特殊的EXC_RETURN。
当程序从异常服务程序返回,把这个EXC_RETURN值送往PC寄存器时,就会启动处理器的异常中断返回序列。
2.异常的定义
异常(Exception)是CPU运行状态的最重要概念,表示CPU在执行主程序流过程中发生的一些特殊事件或错误条件,这些事件可能导致正常的程序流程被中断,需要进行特定的处理
ARM内核定义了7个异常源,分别是:
- 复位异常: CPU上电或执行复位动作后执行该异常
- 数据终止访问异常: 数据访问异常时执行该异常,如试图访问数据的地址不存在
- 硬件中断异常: 标准ARM内核将中断区分为快速中断FIQ和通用中断IRQ,ARM Cortex-M中取消了FIQ的概念,使用紧凑度更高的中断优先级方式模拟接近FIQ功能
- 预取指令异常: 预读取指令异常时执行该异常,如试图读取指令的地址不存在
- 未定义指令异常: 检测到试图执行未定义指令时执行该异常
- 软件中断异常: 由用户自定义的软件中断
3.异常的处理过程
异常执行执行时,CPU通常会执行一系列行为用于保存当前运行状态和跳转至异常回调程序入口
1.保存执行状态和执行现场
当前程序的执行状态是保存在CPSR里面的,异常发生时,要保存当前的CPSR里的执行状态到异常模式里的SPSR里,将来异常返回时,恢复回CPSR,恢复执行状态。
程序的执行现场保存在通用寄存器中,异常发生时,短栈帧(R0,R1,R2,R3,R12,LR,PC,PSR)或长栈帧(R0,R1,R2,R3,R12,LR,PC,PSR,浮点运算寄存器S0-S15)将寄存器值依照上述顺序依次压栈,将来异常返回时,恢复回对应的寄存器值,恢复执行现场。
2.模式切换
硬件自动根据当前的异常类型,将异常码写入CPSR里的M[4:0]模式位,这样CPU就进入了对应异常模式下。
不论是在ARM状态下还是在THUMB(一种16位指令集)状态下发生异常,都会自动切换到ARM状态下进行异常的处理,这是由硬件自动完成的,将CPSR[5] 设置为 0。
同时,CPU会关闭中断IRQ(设置CPSR 寄存器I位),防止中断进入,如果当前是快速中断FIQ异常,关闭快速中断(设置CPSR寄存器F位)。
3.保存返回地址
当前程序被异常打断,切换到异常处理程序里,异常处理完之后,返回当前被打断模式继续执行。
因此必须要保存当前执行指令的下一条指令的地址到LR,由于异常模式不同以及ARM内核采用流水线技术,异常处理程序里要根据异常模式计算返回地址。
4.跳入异常向量表
该操作是CPU硬件自动完成的,当异常发生时,CPU强制将PC的值修改为一个固定内存地址,这个固定地址叫做异常向量,通过该方法可以建立用户自定义程序和硬件异常处理间的连接。
5.异常处理返回
异常处理结束后,将预先保存的执行现场出栈,恢复被打断程序运行时寄存器数据,恢复程序运行时状态CPSR(上文提到的可配置错误状态寄存器)并通过进入异常时保存的返回地址,返回到被打断程序继续执行
4.ARM汇编功能说明
下面说明该段汇编指令是如何工作的
- LR的值EXC_RETURN通常表现为0XFFFFFFFX,即Bit[3:0]
- 如果主程序在线程模式下运行,并且在使用MSP时被中断,则在服务程序中LR=0xFFFFFFF9(主程序被打断前LR已被自动入栈)
- 如果主程序在线程模式下运行,并且在使用PSP时被中断,则在服务程序中LR=0xFFFFFFFD(主程序被打断前LR已被自动入栈)
检查链路寄存器LR的[Bit2](0x04),可以确定进入HARDFAULT时使用的是主堆栈指针(MSP)或进程堆栈指针(PSP)
TST指令用于将LR的值和立即数4进行与运算,测试该位是否为0,运算结果会影响当前程序状态寄存器CPSR(Current Program Status Register)的条件码,即Z==0
TST LR, #4
IT指令的指令原型为IT{x{y{z}}} {cond},最多可以跟随四条指令,X/Y/Z仅可以是T和E,表示对应顺序的指令在满足条件时执行和不执行
EQ表示等于条件判定,NE表示不等于条件判定
ITE EQ
MRS指令的指令原型为MRS{<cond>}<Rd>,<Rm>,用于读取特定寄存器的值到通用寄存器中
MRSEQ R0, MSP
MRSNE R0, PSP
B指令的指令原型是B<cond> <label>,直接相对跳转
B Hard_Fault_Handler
通过该汇编段的判断,可以在异常发生时跳转到用户自行定义的Hard_Fault_Handler(名字随意)函数
同时当前通用寄存器r0的值可以直接传参给Hard_Fault_Handler
获取出现异常之前系统处于主堆栈或进程堆栈的地址的目的在于,Hardfault异常发生时CPU会将R0/R1/R2/R3/R12/LR/PC/PSR寄存器(上文中已提到异常发生时CPU的现场保护机制)压入当前正在使用的栈区
明确异常前堆栈的地址指针可以直接读取错误发生时通用寄存器的状态并且分析错误发生的位置和原因,可以直接在反汇编窗口定位程序出错时的运行位置
Hard_Fault_Handler函数内部通过分析HFSR寄存器确定是否发生硬件错误
并且根据CFSR的三个子寄存器是否置位判断错误发生的类型
5. 简单的Hardfault分析和打印代码(C)
enum { r0, r1, r2, r3, r12, lr, pc, psr };
/*!
@brief 打印使用错误
@param CFSRValue: 错误状态寄存器值
@retval none
*/
static void printUsageErrorMsg(uint32_t CFSRValue)
{
printf("Usage fault: \r\n");
CFSRValue >>= 16; // 取使用错误状态寄存器值
/*
错误分析代码
*/
}
/*!
@brief 打印总线错误
@param CFSRValue: 错误状态寄存器值
@return none
*/
static void printBusFaultErrorMsg(uint32_t CFSRValue)
{
printf("Bus fault: \r\n");
CFSRValue = ((CFSRValue & 0x0000FF00) >> 8); // 取总线错误状态寄存器值
/*
错误分析代码
*/
}
/*!
@brief 打印MPU非法访问错误
@param CFSRValue: 错误状态寄存器值
@retuen none
*/
static void printMemoryManagementErrorMsg(uint32_t CFSRValue)
{
printf("Memory Management fault: \r\n");
CFSRValue &= 0x000000FF; // 取内存错误状态寄存器值
/*
错误分析代码
*/
}
/*!
@brief 打印堆栈寄存器值
@param stack[]: 被压栈的寄存器首地址
@return none
*/
static void stackDump(uint32_t stack[])
{
static char msg[80];
sprintf(msg, "R0 = 0x%08x\r\n", stack[r0]);
printf("%s\n", msg);
sprintf(msg, "R1 = 0x%08x\r\n", stack[r1]);
printf("%s\n", msg);
sprintf(msg, "R2 = 0x%08x\r\n", stack[r2]);
printf("%s\n", msg);
sprintf(msg, "R3 = 0x%08x\r\n", stack[r3]);
printf("%s\n", msg);
sprintf(msg, "R12 = 0x%08x\r\n", stack[r12]);
printf("%s\n", msg);
sprintf(msg, "LR = 0x%08x\r\n", stack[lr]);
printf("%s\n", msg);
sprintf(msg, "PC = 0x%08x\r\n", stack[pc]);
printf("%s\n", msg);
sprintf(msg, "PSR = 0x%08x\r\n", stack[psr]);
printf("%s\n", msg);
}
/*!
@brief HardFault回调函数
@param stack[]: 被压栈的寄存器首地址
@return none
*/
void Hard_Fault_Handler(uint32_t stack[])
{
/* 当系统进入HARDFAULT时,打印系统进入错误状态的运行断点状态 */
static char msg[80];
//if((CoreDebug->DHCSR & 0x01) != 0) {
printf("\r\n Hard Fault Occur !\r\n");
sprintf(msg, "SCB->HFSR = 0x%08x\r\n", SCB->HFSR);
printf("%s\n", msg);
if ((SCB->HFSR & (1 << 30)) != 0)
{
printf("Forced Hard Fault\r\n");
sprintf(msg, "SCB->CFSR = 0x%08x\r\n", SCB->CFSR);
printf("%s\n", msg);
if ((SCB->CFSR & 0xFFFF0000) != 0)
{
printUsageErrorMsg(SCB->CFSR);
}
if ((SCB->CFSR & 0xFF00) != 0)
{
printBusFaultErrorMsg(SCB->CFSR);
}
if ((SCB->CFSR & 0xFF) != 0)
{
printMemoryManagementErrorMsg(SCB->CFSR);
}
}
stackDump(stack);
__ASM volatile("BKPT #01"); /* 编译器不同内联汇编的方式可能有所差异 */
//}
while (1);
}
总结
1. 尝试了解和阅读ARM内核有关的参考手册(如Arm® v7-M Architecture Reference Manual),了解ARM内核的不同功能和不同版本,学习ARM内核的工作机制
2. 尝试了解ARM汇编指令,更高效的编写代码和分析问题
M3权威指南_cn.pdf/M3&M4权威指南_en.pdf/DDI0403E_e_armv7m_arm,pdf(百度网盘)