ARM Cortex-M 异常-HardFault(UsageFault) INVPC置1解决过程

By: Ailson Jack
Date: 2020.12.19
个人博客:http://www.only2fire.com/
本文在我博客的地址是:http://www.only2fire.com/archives/127.html,排版更好,便于学习,也可以去我博客逛逛,兴许有你想要的内容呢。

CPU:STM32F429IGT6

对于其他的stm32芯片或者其他ARM Cortex-M芯片,其实解决方法都相通。建议先完整阅读了本文之后,再对照着你所遇到问题的现象进行调试。

1.基础知识

在ARM Cortex-M系列处理器中,有若干个系统异常专用于 fault 处理。 CM3 中的 Faults 可分为以下几类:

(1).总线 faults;

(2).存储器管理 faults;

(3).用法 faults;

(4).硬 fault;

1.1.总线 faults

当 AHB 接口上正在传送数据时,如果回复了一个错误信号(error response),则会产生总线faults,产生的场合可以是: (1).取指,通常被称作“预取流产”(prefetch abort); (2).数据读/写,通常被称作“数据流产”(data abort); 在 CM3 中,执行如下动作时,如果地址有误,亦会触发总线异常: (1).中断处理起始阶段的堆栈 PUSH 动作。此时若发生总线 fault,则称为“入栈错误”; (2).中断处理收尾阶段的堆栈 POP 动作。此时若发生总线 fault,则称为“出栈错误”; (3).在处理器启动中断服务序列(sequence)后读取向量时。这是一种极度罕见的特殊情况,被归类为硬 fault。

总线 fault 状态寄存器(BFSR),地址:0xE000_ED29,BFSR的各个位的定义如下:

1.2.存储器管理 faults

存储器管理 faults 多与 MPU 有关,其诱因常常是某次访问触犯了 MPU 设置的保护规范。另外,某些非法访问,例如,在不可执行的存储器区域试图取指,也会触发一个 MemManage fault,而且在这种场合下,即使没有 MPU 也会触发 MemMange fault。 MemManage faults 的常见诱因如下所示: (1).访问了所有 MPU regions 覆盖范围之外的地址; (2).访问了没有存储器与之对应的空地址; (3).往只读 region 写数据; (4).用户级下访问了只允许在特权级下访问的地址;

存储器管理 fault 状态寄存器(MFSR),地址:0xE000_ED28,MFSR的各个位的定义如下:

1.3.用法 faults

用法 faults 发生的场合可以是: (1).执行了协处理器指令。 Cortex-M3 本身并不支持协处理器,但是通过 fault 异常机制,可以建立一套“软件模拟”的机制,来执行一段程序模拟协处理器的功能,从而可以方便地在其它 Cortex 处理器间移植。 (2).执行了未定义的指令。同上一点的道理,亦可以软件模拟未定义指令的功能。 (3).尝试进入 ARM 状态。因为 CM3 不支持 ARM 状态,所以用法 fault 会在切换时产生。软件可以利用此机制来测试某处理器是否支持 ARM 状态。 (4).无效的中断返回(LR 中包含了无效/错误的值); (5).使用多重加载/存储指令时,地址没有对齐。 另外,如果需要严格要求程序的质量,还可以让 CM3 在遇到除数为零的时候,以及遇到未对齐访问的时候也产生用法 fault。在 NVIC 中有两个控制位分别与它们对应。通过设置这两个控制位,就可以激活它们。

用法 fault 状态寄存器(UFSR),地址:0xE000_ED2A,UFSR的各个位的定义如下:

1.4.硬 fault

硬 fault 是上文讨论的总线 fault、存储器管理 fault 以及用法 fault 上访的结果。如果这些 fault 的服务例程无法执行,它们就会成为“硬伤” ——上访( escalation)成硬 fault。另外,在取向量(异常处理时对异常向量表的读取)时产生的总线 fault 也按硬 fault 处理。在 NVIC中有一个硬 fault 状态寄存器(HFSR),它指出产生硬 fault 的原因。如果不是由于取向量造成的,则硬 fault 服务例程必须检查其它的 fault 状态寄存器,以最终决定是谁上访的。

硬 fault 状态寄存器(HFSR),地址:0xE000_ED2C,HFSR的各个位的定义如下:

2.UsageFault INVPC置1解决过程

最近在使用RTOS增加DMA驱动时,在对内存到设备和设备到内存的DMA传输测试时,出现了UsageFault,并且UFSR中的INVPC置1了。最开始,单独测试DMA发送是没有问题的,但是DMA发送和接收一起测试时,就会出现UsageFault(INVPC置1)。这个异常不太好定位出现问题的具体位置,因此就检查DMA驱动,并且逐步调试吧。最终,DMA驱动检查和修改好了,仍然出现UsageFault,实在没法了,还是从为什么会出现UsageFault(INVPC置1)开始分析吧。

2.1.出现UsageFault(INVPC置1)的原因

如果LR中的EXC_RETURN不是合法的值(合法值见下图,包括企图返回ARM状态),则引起用法fault。如果用法fault被除能,也上访成硬fault。此时,用法Fault状态寄存器(UFSR,地址: 0xE000_ED2A)中的INVPC位(位偏移: 2),或者是INVSTATE位(位偏移: 1)置位。

上面就是出现该异常的文字分析了。

2.2.UsageFault(INVPC置1)的解决过程

因为该异常是异常响应期间才可能出现的异常(<<Cortex-M3权威指南>> 9.8节介绍了下,在9.8.4节进行文字说明),因此,只要在异常或中断的返回处打断点,执行下一步就有可能进入UsageFault异常。(当然了这个方法,是比较笨的,不过在缩小了异常出现的范围之后,可以在每次异常或中断的返回处打断点,然后执行下一步,就有可能进入UsageFault异常)

我这里是每次开始DMA测试之后,就进入UsageFault异常,并且我的系统中目前就打开了SysTick,PendSV,DMA1_STREAM5,DMA1_STREAM6,NMI,HardFault,MemFault,BusFault,UsageFault这些异常和中断。因此我在开始DMA测试的时候打一个断点,在程序运行到DMA测试开始的断点处时,再在DMA1_STREAM5,DMA1_STREAM6,NMI,HardFault,MemFault,BusFault,UsageFault这些函数的入口打断点,在PendSV的返回处打断点,SysTick就暂时先不管。然后全力运行,发现每次都会在PendSV的异常返回断点处停留(因为任务切换嘛)总共在PendSV的断点处停留了大概7到8次,就进入到了UsageFault。

有了上面步骤的铺垫,先去除中断和异常中的断点,还是先在DMA测试开始处打断点,等运行到DMA测试开始处,再在上述的中断和异常相关位置打断点。接下来我就慢慢的调试,在开始DMA测试之后,全速运行,在退出PendSV异常时,执行单步运行到下一步,重复7到8次,从PendSV就进入了UsageFault,在这7到8次中,我看在退出PendSV时,LR寄存器中的值都是0xFFFFFFFD,是合法的啊,当时仔细一想,有可能是退出异常时硬件再将堆栈中的PC赋值给PC时出问题,导致进入了UsageFault。果不其然,在PenSV退出之前,我查看每个PSP(0xFFFFFFFD:返回线程模式,并使用线程堆栈)对应内存数值,能够正常退出PendSV的寄存器和PSP堆栈内容如下图所示:

进入UsageFault异常之前的寄存器和PSP堆栈内容如下图所示:

此处堆栈中的内容,明显的0x20003D94堆栈中的PC值是有问题的,我的程序是烧写到flash中的PC地址应该是08xxxxxx,然而现在堆栈中的PC地址是0x20003DA8,这个地址是SRAM中的地址,SRAM存储的数据而不是代码,出现这个问题的原因,猜想一下,应该就是任务堆栈溢出导致,当我增加任务堆栈的大小之后,哈哈,程序正常运行,世界是如此美好。

3.调试小结

3.1.解决过程小结

其实上面的步骤2,是我自己的一个调试解决问题的过程,这里给大家提供一个比较直接的解决方式。在开始运行程序之前,直接在UsageFault异常入口函数中打一个断点,然后全速运行程序,等程序停止在UsageFault异常函数的断点处时,需要注意以下几点:

(1).如果LR是合法值,那么根据LR判断退出异常时使用的堆栈,然后在Memory查看窗口中,查看堆栈中R0,R1,R2,R3,R12,LR,PC,xPSR这些寄存器的值,根据这些寄存器的值,判断是否是堆栈溢出导致该异常发生;如果不是堆栈溢出导致该异常发生,那么就要根据PC值,在汇编窗口中跳转到PC值对应的代码处,分析导致异常发生的原因;

(2).如果LR不是合法值,就要分析下你的代码中,有哪些地方修改过LR的值,确保修改的值要是合法的。

3.2.关于UsageFault 如何才能让INVPC置1

(1).在退出异常或中断时,执行BX LR时,LR的值是非法的,此时就会触发UsageFault异常,并且INVPC置1。

(2).在退出异常或中断时,执行BX LR时,LR的值是合法的,但是退出异常之后要使用的堆栈中,堆栈里面的PC值是有问题的,此时就有可能触发UsageFault异常,并且INVPC置1。

对于上面的两点的模拟其实也比较好做,在PendSV或者其他异常的退出的地方打一个断点,然后手动修改LR或者堆栈中PC的值,就能触发UsageFault异常,并且INVPC置1。这里注意一下,修改堆栈中PC的值,我这里测试时候,设置PC值为其他值可能引起其他的异常,貌似修改PC的值为RAM中数据区的地址才会出现该异常,不太清楚为什么会这样,可能是数据区是没有执行代码的权限,因此出异常吧,不太确定,有知道的朋友,欢迎留言讲解。

当然了,博主的知识水平有限,对于异常的一些说明可能会有误,如果有误,欢迎指正,谢谢!

如果这篇文章对你有帮助,记得点赞和关注博主就行了^_^。

排版更好的内容见我博客的地址:http://www.only2fire.com/archives/127.html
注:转载请注明出处,谢谢!^_^

猜你喜欢

转载自blog.csdn.net/jackailson/article/details/111405263