Cortex CM3之Hardfault使用

一、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)xPSR2
(N-8)PC1
(N-12)LR8
(N-16)R127
(N-20)R36
(N-24)R25
(N-28)R14
新SP (N-32)R03

四、好用的寄存器解释

在获取了“现场”数据后,就需要知道怎么去分析它们,可以查看下面的表格:

系统Handler控制及状态寄存器SHCSR 0xE000_ED24

位段名称类型复位值描述
18USGFAULTENAR/W0用法 fault 服务例程使能位
17BUSFAULTENAR/W0总线 fault 服务例程使能位
16MEMFAULTENAR/W0存储器管理 fault 服务例程使能位
15SVCALLPENDEDR/W0SVC 悬起中。本来已经要 SVC 服务例程,但
是却被更高优先级异常取代
14BUSFAULTPENDEDR/W0总线 fault 悬起中,细节同上。
13MEMFAULTPENDEDR/W0存储器管理 fault 悬起中,细节同上
12USGFAULTPENDEDR/W0用法 fault 悬起中,细节同上
11SYSTICKACTR/W0SysTick 异常活动中
10PENDSVACTR/W0PendSV 异常活动中
9----
8MONITORACTR/W0Monitor 异常活动中
7SVCALLACTR/W0SVC 异常活动中
6:4----
3USGFAULTACTR/W0用法 fault 异常活动中
2----
1BUSFAULTACTR/W0总线 fault 异常活动中
0MEMFAULTACTR/W0存储器管理 fault 异常活动中

存储器管理 fault 状态寄存器(MFSR) 0xE000_ED28

位 段名称类型复 位
描述
7MMARVALID-0=1 时表示 MMAR 有效
6:5----
4MSTKERRR/Wc0入栈时发生错误
3MUNSTKERRR/Wc0出栈时发生错误
2----
1DACCVIOLR/Wc0数据访问违例
0IACCVIOLR/Wc0取指访问违例

存储管理地址寄存器(MMAR) 0xE000_ED34

位段名称
复 位
描述
31:0MMARR-触发存储管理 fault 的地址

总线 fault 状态寄存器(BFSR) 0xE000_ED29

位段名称类型复位值描述
7BFARVALID-0=1 时表示 BFAR 有效
6:5----
4STKERRR/Wc0入栈时发生错误
3UNSTKERRR/Wc0出栈时发生错误
2IMPRECISERRR/Wc0不精确的数据访问违例(violation)
1PRECISERRR/Wc0精确的数据访问违例
0IBUSERRR/Wc0取指时的访问违例

总线 fault 地址寄存器(BFAR) 0xE000_ED38

位段名称类型复位值描述
31:0BFARR-触发总线 fault 的

用法 fault 状态寄存器(UFSR),地址: 0xE000_ED2A

位段名称类型复位值描述
9DIVBYZEROR/Wc0表示除法运算时除数为零(只有在 DIV_0_TRP
置位时才会发生)
8UNALIGNEDR/Wc0未对齐访问导致的 fault
7:4----
3NOCPR/Wc0试图执行协处理器相关指令
2INVPCR/Wc0在异常返回时试图非法地加载 EXC_RETURN
到 PC。包括非法的指令,非法的上下文以及
非法的值。 The return PC 指向的指令试图
设置 PC 的值(要理解此位的含义,还需学习
后面的讨论中断级异常的章节)
1INVSTATER/Wc0试图切入 ARM 状态
0UNDEFINSTRR/Wc0执行的指令其编码是未定义的——解码不能

硬 fault 状态寄存器(HFSR) 0xE000_ED2C

位段名称类型复位值描述
31DEBUGEVTR/Wc0硬 fault 因调试事件而产生
30FORCEDR/Wc0硬 fault 是总线 fault,存储器管理 fault
或是用法 fault 上访的结果
29:2----
1VECTBLR/Wc0硬 fault 是在取向量时发生的
0----


 

调试 fault 状态寄存器(DFSR) 0xE000_ED30

位段名称类型复位值描述
4EXTERNALR/Wc0EDBGREQ 信号有效
3VCATCHR/Wc0发生向量加载
2DWTTRAPR/Wc0发生 DWT 匹配
1BKPTR/Wc0执行到 BKPT 指令
0HALTEDR/Wc0在 NVIC 中请求

MemManage fault 状态寄存器提供的讯息

可能的原因
MSTKERR入栈时发生错误(异常响应序列开始时)
1) 堆栈指针的值被破坏
2) 堆栈容易过大,已经超出 MPU 允许的 region 范围
MUNSTKERR出栈时发生错误(异常响应序列终止时)。入栈时没有发生错误,出栈时却出
错,总令人有些匪夷所思,可能的原因是
1. 异常服务例程破坏了堆栈指针
2. 异常服务例程更改了 MPU 配置
DACCVIOL内存访问保护违例。这是 MPU 发挥作用的体现。常常是用户应用程序企图访
问特权级 region 所致
IACCVIOL1. 内存访问保护违例。常常是用户应用程序企图访问特权级 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 读取
INVPC1. 异常返回时使用了无效的 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。
INVSTATE1. 加载到 PC 中的跳转地址值是偶数(LSB=0)。通过检查入栈 PC 的值,一下子
就可以查出该问题。
2. 向量地址的 LSB=0,诊断方法同上。
3. 入栈的 PSR 在异常处理过程中被破坏,使得在返回时内核尝试进入 ARM 状态。
UNDEFINSTR1. 使用了 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 状态寄存器提供的讯息

可能的原因
EXTERNALEDBGREQ 信号置为有效
VCATCH发生了向量抓捕事件
DWTTRAP发生了 DWT 观察点事件
BKPT1. 执行了 BKPT 指令
2. FPB 单元产生了断点事件

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值