前言

关于定位hardfault产生之前的程序运行情况,有如下两种方法:

  • 在keil里单步调试时在hardfault入口处打个断点,然后通过看寄存器的值,或直接用鼠标操作就能回退到上一层出错的位置;具体方法有很多人都分享过;
  • 使用armink的工具cmbacktrace,这是朱天龙老师在github上的一个开源项目,特意为cotex-m系列MCU做的异常追踪库;

hardfault,有些时候找到触发路径,也不知道原因。其实触发hardfault的原因在《权威指南》里都说的很清楚了,并且cmbacktrace工具也把产生hardfault的条件按照《权威指南》描述的规则找出来了,但奈何本人经验有限,仍然是对一些问题无法做到全面的解释。所以希望借此文总结下引起hardfault的原因,望量变引起质变。

1 错误类型基本描述

参考《M3&M4权威指南》12.1

1.1 引起错误的原因

电子系统总是会时不时的出错,有时可能为软件中的错误,不过许多情况下可由外部因素引起。例如:

  • 供电不稳
  • 电器噪声(电力线噪声)
  • 电磁干扰(EMI)
  • 静电放电
  • 极端的运行环境(如温度,机械振荡)
  • 反复编程或高低温交替导致的部件损耗(如flash/EEPROM设备、晶体振荡器及电容)
  • 辐射(宇宙射线)
  • 使用问题(未按照用户手册使用)或非法的外部输入
    所有这些问题可能都会引起处理器上运行的程序执行失败。(后面省略部分原文)

1.2 错误类型简介

所有的错误默认都会触发Hardfault异常(异常类型3),该错误异常在报考M0,M0+在内的所有cotex-M系列处理器上都是可用的。M3和M4还包括另外三个可配置的错误异常:

  • MemManage(存储器管理)错误(异常类型4)
  • 总线错误(异常类型5)
  • 使用错误(异常类型6)

我们知道这3个可配置的异常如果没有使能,就会直接触发hardfault了。

1.3 结论

由以上两点,可以初步推测出,1.1中提到的那些因素,可能触发hardfault。最初看到这段文字的时候我是持怀疑态度的,因为当时使用stm32的时间还不长,这些问题基本都没有遇到过。但是后面有几次,遇到过反复开关机导致供电不稳时,触发了hardfault;还有外接的RAM设备由于供电不足,单片机在操作它的过程中进入了hardfault等等。所以可以将1.1中提到的这些原因当作触发hardfault的潜在条件。
当然除了使用问题外,如果是其它几个原因导致的hardfault,大部分是无法解决的了,只能想办法避免触发到它。

上面的这些大多都是比较特殊,需要结合硬件知识一起解决的。下面再针对大多数情况下是纯软件触发的问题(即总线错误,存储器管理错误,使用错误),作详细的解读。这三个可配置异常也是日常遇到的hardfault中最大可能的原因。

2 存储器管理错误

2.1 存储器管理错误的原因

以下参考《M3&M4权威指南》

  • 非特权任务试图访问只支持特权访问的存储区域;
  • 访问未在任何MPU区域中定义的存储器位置,私有外设总线(PPB)除外,其总可被特权代码访问;
  • 写入被MPU定义为只读的存储器位置;
  • 若MemManage错误发生在异常入口压栈过程中,则会被称作压栈错误;
  • 若MemManage错误发生在异常退出流程出战过程中,则会被称作出栈错误;
  • 当在永不执行区域执行程序代码时也会触发MemManage错误,即便不具有可选MPU的M3和M4处理器,也会发生这种情况;

2.2 MemManage错误状态寄存器SCB->CFSR最低字节(0XE00ED28)的信息

以下大部分参考《COTEX-M3权威指南》- 宋岩,附录E

  • MMARVALID置1:表示产生了MemManage错误
  • MLSPERR置位:浮点惰性压栈错误(只在具有浮点单元的cotex-m4中存在);
  • 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值被破坏了。

2.3 结论

上面的描述已经比较清楚,笔者暂时也无法举出实例说明。暂时先这样,待日后遇到再作补充。

3. 总线错误

3.1 总线错误的原因

以下参考《M3&M4权威指南》

  • 总线错误可由存储器访问期间从处理器总线接口上收到的错误响应触发;
  • 总线错误也可在异常处理流程的压栈和出栈过程中产生;
  • 处理器试图访问一个非法的存储器位置;
  • 设备未准备好接收某传输。例如,在未初始化DRAM控制器的情况下访问DRAM会触发总线错误,这是由设备决定的;
  • 收到传输请求的总线从设备返回一个错误响应。例如,若总线从设备不支持传输的类型/大小或者外设不允许正在执行的操作,就会出现这种情况;
  • 对私有外设总线的非特权访问和默认的存储器访问权限相冲突
  • 需要注意,总线错误由精确和不精确之分;之所以不精确,是因为处理器总线接口上存在写缓冲。即当一个指令需要多个周期执行时,总线不用等这个指令执行完,就可以执行其它指令。写缓存可以提高性能,当不利于定位异常;当然写缓存是可以被禁止的,使用辅助控制寄存器中的DISDEFWBUF位。

3.2 总线错误状态寄存器SCB->CFSR第二个字节(0XE00ED29)表示的信息

以下大部分参考《COTEX-M3权威指南》- 宋岩,附录E

  • BARVALID置1:表示产生了总线错误
  • LSPERR 浮点惰性压栈错误.
  • STKERR 压栈错误.1)堆栈指针的值被破坏;2)堆栈容易太大,到达了未定义存储器区域;3.PSP未初始化就使用;
  • UNSTKERR 出栈错误.如果没有发生过STKERR,则最可能的就是在异常处理期间把SP的值破坏了。
  • IMPRECISERR 不精确的数据访问冲突。与设备传送数据的过程中发生总线错误。可能因为设备未经初始化而引起;在用户级访问了特权级的设备,或者传送的数据单位尺寸不能为设备所接受。此时,有可能是 LDM/STM 指令造成了非精确总线 fault。
  • PRECISERR 精确的数据访问冲突。在数据访问期间的总线错误。通过 BFAR 可以获取具体的地址。发生 fault 的原因同上。
  • IBUSERR 指令访问冲突。同 MemManage fault 中的 IACCVIOL

3.3 结论

上面的描述已经比较清楚,笔者暂时也无法举出实例说明。暂时先这样,待日后遇到再作补充。

4. 使用错误

4.1 使用错误的原因

以下参考《M3&M4权威指南》

  • 未定义指令的执行(包括在浮点单元禁止时执行浮点指令)
  • 协处理器指令的执行。(M3和M4不支持协处理器访问指令)
  • 试图切换值ARM状态。某些经典处理器支持ARM和Thumb指令,而cotex-m系列只支持Thumb指令。所以用在经典处理器的代码如果移植到M系列的处理器上,可能存在切换到ARM状态的指令。
  • 异常返回流程使用了非法的EXC_RETURN代码,如在异常仍然活跃时试图返回线程级;
  • 具有多加载或多存储指令的非对齐存储器访问(包括双子加载和双子存储);
  • 在SVC优先级不大于当前等级时执行SVC;
  • 异常返回时已出栈的xPSR具有中断可继续指令(ICI)位,不过异常返回后正在执行的指令并非多加载/存储指令;
    可配置的使用错误(通过配置寄存器CCR控制)
  • 被零除(默认是等于0,不产生使用错误)
  • 所有非对齐访问

4.2 使用错误状态寄存器SCB->CFSR后两个字节(0XE00ED2A)表示的信息

以下大部分参考《COTEX-M3权威指南》- 宋岩,附录E

  • DIVBYZERO:发生被0除,(只有DIV_0_TRP置位时,才会置0);
  • UNALIGNED:产生了非对齐访问错误;
  • NOCP:试图执行协处理器指令;
  • INVPC:试图执行EXC_RETURN错误的异常;
  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:试图切换到错误的状态(如ARM);
    1) 加载到 PC 中的跳转地址值是偶数(LSB=0)。通过检查入栈 PC 的值,一下子就可以查出该问题。
    2)向量地址的 LSB=0,诊断方法同上。
    3) 入栈的 PSR 在异常处理过程中被破坏,使得在返回时内核尝试进入 ARM状态。
  • UNDEFINSTR:试图执行未定义的指令;
    1) 使用了 CM3 不支持的指令
    2) 代码段中的数据被破坏
    3) 连接时加载了 ARM 目标码。请检查编译阶段的设置
    4) 指令对齐的问题。例如,在使用 GNU 工具链时,忘记了在.ascii 后使用.align,就有可能导致下一条指令没有对齐

4.3 结论

上面的描述已经比较清楚,笔者暂时也无法举出实例说明。暂时先这样,待日后遇到再作补充。

5. hardfault

5.1 hardfault产生的原因

  • 上面三个可配置异常如果没使能,就直接触发hardfault。这也是笔者目前最经常遇到的状态;
  • 取向量期间收到总线异常;(这个书中在总线异常那小节有描述)
  • 连接了调试器(暂停调试未使能)且调试监控异常未使能时执行了断点(BKPT)指令;
  • 如果在BKPT指令执行时却发现C_DEBUGEN和MON_EN都为0,则会因为无法进入调试而
    上访成硬fault,并且把硬fault状态寄存器(HFSR)的DEBUGEVT位给置1,同时在调试fault状态寄存器(DFSR)中的BKPT位也置1(笔者最近就遇到了该问题,没有连接调试器,但还没有找到是哪里执行了BKPT指令);

5.2 hardfault错误状态寄存器SCB->HFSR(0xE000ED2C)表示的信息

以下大部分参考《COTEX-M3权威指南》- 宋岩,附录E

  • DEBUGEVT:调试事件触发了hardfault;
    1)断点/观察点事件
    2)如果在硬 fault 服务例程的执行过程中,没有使能监视器异常(MON_EN=0)也没有使能停机调试(C_DEBUGEN=0),却执行了 BKPT 指令。缺省时,有些 C 编译器可能会在半主机代码中使用 BKPT 指令。
  • FORCED:总线错误、存储器管理错误或使用错误导致hardfault;
    试图在 SVC/监视器服务例程中执行 SVC/BKPT,或者在其它拥有相同或更高优先级的服务例程中执行 SVC/BKPT。
  • VECTBL:取向量失败导致hardfault;
    1)在取向量过程中发生总线 fault
    2)向量表偏移量设置有误

5.3 结论

上面的描述已经是比较清楚了,笔者暂时也无法举出实例了说明。暂时先这样,待日后遇到再作补充。
注意:在没有连接调试器的时候,若执行了BKPT指令,也会触发hardfault。但是为什么没连接调试器,也会执行BKPT指令。笔者暂时还没搞清楚。

写在最后

其实野指针,数组越界,堆栈溢出等等,都是由于触发了总线异常、存储器管理异常、使用异常中的一个或多个,才触发了hardfault。本来应该将这些实际的原因归纳到每小节的结论里,但由于一时想不起来这些操作导致异常的具体步骤,所以还是待日后遇到再补充了。

文中内容基本上摘抄自《COTEX-M3权威指南》宋岩和《ARM cotexm3与cotexm4权威指南》两本书。文中内容难免会有个人色彩在里面,所以大家最好去翻阅原文。
如有错误之处,欢迎指出!
谢谢!

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐