一、Hard Fault产生原因
硬件方面常见原因:
电源设计有错误,造成期间供电不稳定;
电源质量不好,纹波、噪声过大;
器件接地不良;
对于带有Vcap引脚的器件,管脚处理不当;
电路中有强干扰源,对器件造成干扰。
软件方面常见原因:
使用了空指针;
对地址偏移量的计算有误;
数组越界导致程序出错;
动态内存使用不当,导致访问了已释放的内存地址;
通过地址访问了已失效的局部变量。
一般因为硬件造成Hardfault错误的可能性较低,大多数都是软件原因造成的。
二、用C把进入Hardfault时候的寄存器保存下来
在启动文件startup.s文件中,把HardFault_Handler函数重构一下:
HardFault_Handler\
PROC
EXPORT Hardfault_Handler [WEAK]
IMPORT hardfault_handler_c
TST LR, #4
ITE EQ
MRSEQ R0, MSP
MRSNE R0, PSP
B hardfault_handler_c
ENDP
上面这段是用汇编写的函数,具体分析如下:
第一行定义函数,使用第二行的PROC和第十行的ENDP伪指令,表示中间的代码是该函数的子程序部分;
第三行使用EXPORT声明,表示本函数可以被外部模块调用,而且用WEAK声明,表示可以被外部函数重构(如果其他模块有定义同样名字的函数,会将这个函数覆盖);
第四行使用IMPORT调用外部文件的函数hardfault_handler_c;
第五行测试EXC_RETURN.2;
第六行判定上一行中测试结果是否为零;
第七行表示在结果为零的情况下,把主堆栈MSP值加载到R0去;
第八行表示在结果不为零的情况下,把进程堆栈PSP值加载到RO去;
第九行通过B跳转到函数hardfault_handler_c。
hardfault_handler_c函数
typedef struct _pushed_stack stPUSHED_STACK;
typedef struct _special_regs stSPECIAL_REGS;
struct _pushed_stack
{
volatile unsigned int r0;
volatile unsigned int r1;
volatile unsigned int r2;
volatile unsigned int r3;
volatile unsigned int r12;
volatile unsigned int lr;
volatile unsigned int pc;
volatile unsigned int xpsr;
};
struct _special_regs
{
volatile unsigned int shcrs; /* 系统Handler控制及状态寄存器 */
volatile unsigned char mfsr; /* 存储管理Fault状态寄存器 */
volatile unsigned int mmar; /* 存储器管理地址寄存器 */
volatile unsigned char bfsr; /* 总线Fault状态寄存器 */
volatile unsigned int bfar; /* 总线Fault地址寄存器 */
volatile unsigned short ufsr; /* 用法Fault状态寄存器 */
volatile unsigned int hfsr; /* 硬Fault状态寄存器 */
volatile unsigned int dfsr; /* 调试Fault状态寄存器 */
};
stPUSHED_STACK g_pushed_stack;
stSPECIAL_REGS g_special_regs;
void hardfault_handler_c(unsigned int *stack)
{
g_pushed_stack.r0 = stack[0];
g_pushed_stack.r1 = stack[1];
g_pushed_stack.r2 = stack[2];
g_pushed_stack.r3 = stack[3];
g_pushed_stack.r12 = stack[4];
g_pushed_stack.lr = stack[5];
g_pushed_stack.pc = stack[6];
g_pushed_stack.xpsr = stack[7];
g_special_regs.shcrs = (*((volatile unsigned int *)(0xE000ED24)));
g_special_regs.mfsr = (*((volatile unsigned char *)(0xE000ED28)));
g_special_regs.mmar = (*((volatile unsigned int *)(0xE000ED34)));
g_special_regs.bfsr = (*((volatile unsigned char *)(0xE000ED29)));
g_special_regs.bfar = (*((volatile unsigned int *)(0xE000ED38)));
g_special_regs.ufsr = (*((volatile unsigned short *)(0xE000ED2A)));
g_special_regs.hfsr = (*((volatile unsigned int *)(0xE000ED2C)));
g_special_regs.dfsr = (*((volatile unsigned int *)(0xE000ED30)));
/* TODO
在此处可以自行添加有利于调试的代码;
可以把变量g_pushed_stack和g_special_regs的值写入flash,
这样在发生故障的设备会保留现场数据,利于分析,不过要考虑过度擦写的问题
*/
while (1);
}
执行程序后,若发生内核错误,则程序会执行到最后的while (1)处,此时观察对应的堆栈和故障寄存器值,利于分析问题。
三、函数解释
看到这里,可能会产生疑惑,hardfault_handler_c函数的参数为什么可以像数组一样被获取,而且还刚好是R0、R1、R2、R3、R12、LR、PC、XPSR?
这个还要从汇编函数HardFault_Handler中的第六行说起。
当堆栈里的值被加载到R0后,PC跳转执行hardfault_handler_c,由于我们在声明hardfault_handler_c的时候用了一个形参,所以R0就作为函数hardfault_handler_c的参数传入;此时我们知道R0的值就是指向堆栈的地址,那么函数hardfault_handler_c的形参stack就可以像数组一样来用了。
那为什么刚好是R0、R1、R2、R3、R12、LR、PC、XPSR?
原来Cortex CM3对自动入栈后堆栈中的内容有规定,如下:
地址 | 寄存器 | 被保存的顺序 |
旧SP (N-0) | 原先已压 入的内容 | - |
(N-4) | xPSR | 2 |
(N-8) | PC | 1 |
(N-12) | LR | 8 |
(N-16) | R12 | 7 |
(N-20) | R3 | 6 |
(N-24) | R2 | 5 |
(N-28) | R1 | 4 |
新SP (N-32) | R0 | 3 |
四、好用的寄存器解释
在获取了“现场”数据后,就需要知道怎么去分析它们,可以查看下面的表格:
系统Handler控制及状态寄存器SHCSR 0xE000_ED24
位段 | 名称 | 类型 | 复位值 | 描述 |
18 | USGFAULTENA | R/W | 0 | 用法 fault 服务例程使能位 |
17 | BUSFAULTENA | R/W | 0 | 总线 fault 服务例程使能位 |
16 | MEMFAULTENA | R/W | 0 | 存储器管理 fault 服务例程使能位 |
15 | SVCALLPENDED | R/W | 0 | SVC 悬起中。本来已经要 SVC 服务例程,但 是却被更高优先级异常取代 |
14 | BUSFAULTPENDED | R/W | 0 | 总线 fault 悬起中,细节同上。 |
13 | MEMFAULTPENDED | R/W | 0 | 存储器管理 fault 悬起中,细节同上 |
12 | USGFAULTPENDED | R/W | 0 | 用法 fault 悬起中,细节同上 |
11 | SYSTICKACT | R/W | 0 | SysTick 异常活动中 |
10 | PENDSVACT | R/W | 0 | PendSV 异常活动中 |
9 | - | - | - | - |
8 | MONITORACT | R/W | 0 | Monitor 异常活动中 |
7 | SVCALLACT | R/W | 0 | SVC 异常活动中 |
6:4 | - | - | - | - |
3 | USGFAULTACT | R/W | 0 | 用法 fault 异常活动中 |
2 | - | - | - | - |
1 | BUSFAULTACT | R/W | 0 | 总线 fault 异常活动中 |
0 | MEMFAULTACT | R/W | 0 | 存储器管理 fault 异常活动中 |
存储器管理 fault 状态寄存器(MFSR) 0xE000_ED28
位 段 | 名称 | 类型 | 复 位 值 | 描述 |
7 | MMARVALID | - | 0 | =1 时表示 MMAR 有效 |
6:5 | - | - | - | - |
4 | MSTKERR | R/Wc | 0 | 入栈时发生错误 |
3 | MUNSTKERR | R/Wc | 0 | 出栈时发生错误 |
2 | - | - | - | - |
1 | DACCVIOL | R/Wc | 0 | 数据访问违例 |
0 | IACCVIOL | R/Wc | 0 | 取指访问违例 |
存储管理地址寄存器(MMAR) 0xE000_ED34
位段 | 名称 | 类 型 | 复 位 值 | 描述 |
31:0 | MMAR | R | - | 触发存储管理 fault 的地址 |
总线 fault 状态寄存器(BFSR) 0xE000_ED29
位段 | 名称 | 类型 | 复位值 | 描述 |
7 | BFARVALID | - | 0 | =1 时表示 BFAR 有效 |
6:5 | - | - | - | - |
4 | STKERR | R/Wc | 0 | 入栈时发生错误 |
3 | UNSTKERR | R/Wc | 0 | 出栈时发生错误 |
2 | IMPRECISERR | R/Wc | 0 | 不精确的数据访问违例(violation) |
1 | PRECISERR | R/Wc | 0 | 精确的数据访问违例 |
0 | IBUSERR | R/Wc | 0 | 取指时的访问违例 |
总线 fault 地址寄存器(BFAR) 0xE000_ED38
位段 | 名称 | 类型 | 复位值 | 描述 |
31:0 | BFAR | R | - | 触发总线 fault 的 |
用法 fault 状态寄存器(UFSR),地址: 0xE000_ED2A
位段 | 名称 | 类型 | 复位值 | 描述 |
9 | DIVBYZERO | R/Wc | 0 | 表示除法运算时除数为零(只有在 DIV_0_TRP 置位时才会发生) |
8 | UNALIGNED | R/Wc | 0 | 未对齐访问导致的 fault |
7:4 | - | - | - | - |
3 | NOCP | R/Wc | 0 | 试图执行协处理器相关指令 |
2 | INVPC | R/Wc | 0 | 在异常返回时试图非法地加载 EXC_RETURN 到 PC。包括非法的指令,非法的上下文以及 非法的值。 The return PC 指向的指令试图 设置 PC 的值(要理解此位的含义,还需学习 后面的讨论中断级异常的章节) |
1 | INVSTATE | R/Wc | 0 | 试图切入 ARM 状态 |
0 | UNDEFINSTR | R/Wc | 0 | 执行的指令其编码是未定义的——解码不能 |
硬 fault 状态寄存器(HFSR) 0xE000_ED2C
位段 | 名称 | 类型 | 复位值 | 描述 |
31 | DEBUGEVT | R/Wc | 0 | 硬 fault 因调试事件而产生 |
30 | FORCED | R/Wc | 0 | 硬 fault 是总线 fault,存储器管理 fault 或是用法 fault 上访的结果 |
29:2 | - | - | - | - |
1 | VECTBL | R/Wc | 0 | 硬 fault 是在取向量时发生的 |
0 | - | - | - | - |
调试 fault 状态寄存器(DFSR) 0xE000_ED30
位段 | 名称 | 类型 | 复位值 | 描述 |
4 | EXTERNAL | R/Wc | 0 | EDBGREQ 信号有效 |
3 | VCATCH | R/Wc | 0 | 发生向量加载 |
2 | DWTTRAP | R/Wc | 0 | 发生 DWT 匹配 |
1 | BKPT | R/Wc | 0 | 执行到 BKPT 指令 |
0 | HALTED | R/Wc | 0 | 在 NVIC 中请求 |
MemManage fault 状态寄存器提供的讯息
位 | 可能的原因 |
MSTKERR | 入栈时发生错误(异常响应序列开始时) 1) 堆栈指针的值被破坏 2) 堆栈容易过大,已经超出 MPU 允许的 region 范围 |
MUNSTKERR | 出栈时发生错误(异常响应序列终止时)。入栈时没有发生错误,出栈时却出 错,总令人有些匪夷所思,可能的原因是 1. 异常服务例程破坏了堆栈指针 2. 异常服务例程更改了 MPU 配置 |
DACCVIOL | 内存访问保护违例。这是 MPU 发挥作用的体现。常常是用户应用程序企图访 问特权级 region 所致 |
IACCVIOL | 1. 内存访问保护违例。常常是用户应用程序企图访问特权级 region。在这种情 况下,入栈的 PC 给出的地址,就是产生问题的代码之所在 2. 跳转到不可执行指令的 regions 3. 异常返回时,使用了无效的 EXC_RETURN 值 4. 向量表中有无效的向量。例如,异常在向量建立之前就发生了,或者加载的 是用于传统 ARM 内核的可执行映像 5. 在异常处理期间,入栈的 PC 值被破坏了 |
总线 fault 状态寄存器提供的讯息
位 | 可能的原因 |
STKERR | (自动)入栈期间出错 1. 堆栈指针的值被破坏 2. 堆栈用量太大,到达了未定义存储器的区域 3. PSP 未经初始化就使用 |
UNSTKERR | (自动)出栈期间出错。如果没有发生过 STKERR,则最可能的就是在异常 处理期间把 SP 的值破坏了 |
IMPRECISERR | 与设备之间传送数据的过程中发生总线错误。可能是因为设备未经初始化而 引起;或者在用户级访问了特权级的设备,或者传送的数据单位尺寸不能为 设备所接受。此时,有可能是 LDM/STM 指令造成了非精确总线 fault。 |
PRECISERR | 在数据访问期间的总线错误。通过 BFAR 可以获取具体的地址。发生 fault 的原因同上。 |
IBUSERR | 同 MemManage fault 中的 IACCVIOL |
用法 fault 状态寄存器提供的讯息
位 | 可能的原因 |
DIVBYZERO | 当 DIV_0_TRP 置位时则发生了除数为零的情况。引发此 fault 的指令可以从入栈 的 PC 读取 |
UNALIGNED | 当 UNALIGN_TRP 置位时发生未对齐访问。引发此 fault 的指令可以从入栈的 PC |
NOCP | 企图执行一个协处理器指令。引发此 fault 的指令可以从入栈的 PC 读取 |
INVPC | 1. 异常返回时使用了无效的 EXC_RETURN,例如 1) 当 EXC_RETURN=0xFFFF_FFF1 时却要返回线程模式 2) 当 EXC_RETURN=0xFFFF_FFF9 时却要返回 handler 模式 2. 无效的异常活动状态,例如 1) 当前异常的活动状态已经清除了,却在此时执行异常返回。往往是因为滥 用 VECTCLRACTIVE 或清除了 SHCSR 中活动状态所致 2) 在尚有异常的活动位置位时,却要返回线程模式 3. 由于堆栈指针错误导致了 IPSR 的值不正确。对于 INVPC fault,入栈的 PC 指出了该 fault 服务例程在何处抢占了其它代码。这个问题往往是由比较隐晦 的程序错误造成的,欲详细调查该问题的原因,最好使用 ITM 的跟踪功能。 4. ICI/IT 位对当前指令无效。当 LDM/STM 指令被异常打断后,在异常服务例程 中又更改了入栈的 PC。结果在中断返回时,非零的 ICI 位段作用到了不使用 ICI 位段的指令上。如果是其它原因破坏了 PSR 的值,也可能导致此 fault。 |
INVSTATE | 1. 加载到 PC 中的跳转地址值是偶数(LSB=0)。通过检查入栈 PC 的值,一下子 就可以查出该问题。 2. 向量地址的 LSB=0,诊断方法同上。 3. 入栈的 PSR 在异常处理过程中被破坏,使得在返回时内核尝试进入 ARM 状态。 |
UNDEFINSTR | 1. 使用了 CM3 不支持的指令 2. 代码段中的数据被破坏 3. 连接时加载了 ARM 目标码。请检查编译阶段的设置 4. 指令对齐的问题。例如,在使用 GNU 工具链时,忘记了在.ascii 后使用.align, 就有可能导致下一条指令没有对齐 |
硬 fault 状态寄存器提供的讯息
位 | 可能的原因 |
DEBUGEVF | 因调试事件导致的 fault 1. 断点/观察点事件 2. 在硬 fault 服务例程的执行过程中,没有使能监视器异常(MON_EN=0)也 没有使能停机调试(C_DEBUGEN=0),却执行了 BKPT 指令。缺省时,有些 C 编译器可能会在半主机代码中使用 BKPT 指令。 |
FORCED | 这是 fault“上访”的情况 1. 试图在 SVC/监视器服务例程中执行 SVC/BKPT,或者在其它拥有相同或更 高优先级的服务例程中执行 SVC/BKPT。 2. 发生了 fault,但是它的服务例程被除能 3. 发生了 fault,但是当前处理器在响应同级或更高优先级的异常 4. 发生了 fault,但是它被掩蔽了 |
VECTBL | 取向量失败, 1. 在取向量过程中发生总线 fault 2. 向量表偏移量设置有误 |
调试 fault 状态寄存器提供的讯息
位 | 可能的原因 |
EXTERNAL | EDBGREQ 信号置为有效 |
VCATCH | 发生了向量抓捕事件 |
DWTTRAP | 发生了 DWT 观察点事件 |
BKPT | 1. 执行了 BKPT 指令 2. FPB 单元产生了断点事件 |