UCosIII在Cortex-M3核单片机上IAP跳转APP时引起HardFault错误原因分析

        最近变得好懒散,什么也不想做,回家也是对着电脑发呆,我好像失去了什么,但是我好像本来什么也没有啊。没有拥有,又何谈失去呢,看来是庸人自扰,也许发呆只是潜意识里逃避进取的借口罢了。非要说失去了什么,可能就是光阴了,不管你是在发呆还是在拼命,时间从来不会停下前进的步伐。以此说来,在过去的岁月里,不管你做了什么,失去了什么,获得了什么,都应无怨无悔,这本来就是你自己的选择。发呆久了,人也变得迟钝,敲几行字清醒一下,不能秀逗了嘛。毕竟当你什么也没有的时候,还要在程序里讨得一碗残羹冷炙,饥寒交迫中求得一线希望,黑暗中等待黎明。

        在项目中遇到MCU出现HardFault错误,查了好久,结合网上找到的零散的解决问题的方法,不断验证后解决了问题。在这里写成博客,当作学习笔记,也给遇到类似问题的程序猿提供解决问题的思路。

        一、问题描述

       项目中使用LPC1857单片机,这是一款NXP出的Cortex-M3内核MCU,我们做了一个bootload代码,用于IAP升级。在bootloader代码中我们使用了官方移植好的ucosIII系统,当检测到APP应用程序格式正确时,执行跳转功能,直接从IAP代码跳到APP代码中运行,但是跳转后在运行APP代码时MCU出现HardFault错误。

        二、解决思路

       1、HardFault错误原因

        既然MCU出现了HardFault错误,我们首先要搞清楚什么原因引起的。在《CORTEX-M3权威指南》里,对引起HardFault错误的可能原因进行了详细的描述,这里我们也转述相关基础知识点,便于大家理解。

        Cortex-M3内核提供四种异常:总线fault、存储器管理fault、用法异常和硬fault。当前三种异常没有使能时,它们的异常状态会上报到硬fault,所以引起HardFault错误的原因有以上四种情况。每种异常中又有多个标志位用于指示引起异常的具体原因,下面对每个标志位做简单的介绍。

       (1)总线Fault:在取址、数据读/写、取中断向量、进入/退出中断时寄存器堆栈操作(入栈/出栈)时检测到内存访问错误。      

可能的原因

STKERR

(自动)入栈期间出错

1. 堆栈指针的值被破坏

2. 堆栈用量太大,到达了未定义存储器的区域

3. PSP未经初始化就使用

UNSTKERR

(自动)出栈期间出错。如果没有发生过 STKERR,则最可能的就是在异常

处理期间把 SP的值破坏了

IMPRECISERR

与设备之间传送数据的过程中发生总线错误。可能是因为设备未经初始化而

引起;或者在用户级访问了特权级的设备,或者传送的数据单位尺寸不能为 设备所接受。此时,有可能是 LDM/STM指令造成了非精确总线 fault。

PRECISERR

在数据访问期间的总线错误。通过 BFAR 可以获取具体的地址。发生 fault

的原因同上。

IBUSERR

同 MemManage fault中的 IACCVIOL

        (2)存储器管理Fault:检测到内存访问违反了MPU定义的区域。

可能的原因

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值被破坏了

        (3)用法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, 就有可能导致下一条指令没有对齐

        (4)硬Fault:如果上面的总线fault、存储器管理Fault、用法Fault的处理程序不能被执行(例如禁能了总线Fault、存储器管理Fault、用法Fault异常或者在这些异常处理程序执行过程中又出现了Fault)则触发硬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. 向量表偏移量设置有误

        2、HardFault错误定位方法

        (1)在MDK中使用JTAG在线调试,出现HardFault异常后,可以查看引起错误的原因,定位是哪种错误引起的单片机异常,具体方法如下图所示:


        在该项目中,我们查看的结果是总线fault中的STKERR引起的,也就是栈指针溢出引起的,至于程序中哪段代码引起的,要通过下面的方法定位。

        (2)定位出错的代码行

        在线调试模式下,查看左侧寄存器栏中Banked确定现在使用的是哪个堆栈,MSP或者是PSP,确定以后,在内存查看窗口,输入堆栈的地址,以这个地址开始的8个32位数值,依次是R0,R1,R2,R3,R12,R14,R15,XPSR的数值,据此判定你的堆栈地址是不是对的(有时需要考虑堆栈的增长方向)。R14,R15的地址就是我们出错的代码所在的地址,需要在这个地址基础上,首先偶数对齐,然后向上减去8个字节。需要考虑的是,在使用MSP的时候,有出错的地方并不一定在R14,R15处,而是在XPSR往后的第二个地址处,在这个附近查找,排除故障。

        该项目中,最后定位出错的代码行是(MOVS R0,#0;MSR PSP,R0;),这两句汇编语句是ucosiii启动任务时调用的,该代码在os_cpu_a.s文件中,意思使MCU的从栈指针PSP指向地址0。

        3、ucosIII对栈指针MSP和PSP的使用

        既然是ucosIII中使用PSP栈指针引起的HardFault,我们就要搞清楚ucosIII中对PSP的使用方法。这个要查看ucosIII的启动代码了,由于启动代码是用汇编写的,逐行逐行的分析太累也太难理解,有兴趣的可以自己网上找找资料。如果你想在底层开发有所突破,还是要认真看看这段汇编代码的,大家都不会的你会了,你就是专家。这里我们只看重点的地方。

        启动文件是os_cpu_a.s,ucosIII启动任务时,使用了(MOVS R0,#0;MSR PSP,R0;)使PSP指向地址0,目的是把PSP作为首次进行任务切换的标志位,如果PSP等于0表示第一次进行任务切换,这时不需要把R4-R11压栈,因为在这之前还没有任务运行。如果不为0,就要先执行R4-R11压栈操作。除了这里,还有一段代码很重要(MSR     PSP, R0;ORR     LR, LR, #0x04),这两句汇编,前一句使PSP指向当前任务的栈,后一句的意思是在PENDSV中断中执行完任务切换后,恢复使用PSP作为普通任务的栈指针,因为CORTEX-M3在中断中使用的是MSP栈指针。对于MSP和PSP的用法,请自己查找资料学习,这里不再介绍。

        4、产生HardFault的原因

        从上面的分析,我们可以知道ucosIII启动时先把PSP栈指针指向地址0,在PendSV中断中使用MSP栈指针执行完任务切换后,恢复使用PSP栈指针作为普通任务的栈指针。在bootloader代码中ucosIII启动前,MCU默认使用MSP栈指针作为任务栈指针,这时使PSP指向地址0没有任何问题,因为MCU这时根本没有使用PSP。ucosIII启动后,任务的栈指针就被ucosIII切换成了PSP,这时我们在普通任务里跳转到APP代码运行,那么APP代码运行时使用的栈指针还是PSP。APP代码中也要运行ucosIII的启动代码,这样PSP正在被MCU调用的时候突然被ucosIII的启动代码把值修改成了0,由于地址0超出了MCU的内存地址空间,所以MCU内核就会报STKERR异常,即栈指针溢出,进而产生HardFault异常。

        5、解决方案

        bootloader代码中,在执行跳转到APP之前,先把栈指针修改成MSP,这样跳转到APP之后,MCU使用的是MSP栈指针,那么ucosIII就可以随意修改PSP的值了。把MCU使用的栈指针PSP修改成MSP的代码是__set_CONTROL(0)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章