最近变得好懒散,什么也不想做,回家也是对着电脑发呆,我好像失去了什么,但是我好像本来什么也没有啊。没有拥有,又何谈失去呢,看来是庸人自扰,也许发呆只是潜意识里逃避进取的借口罢了。非要说失去了什么,可能就是光阴了,不管你是在发呆还是在拼命,时间从来不会停下前进的步伐。以此说来,在过去的岁月里,不管你做了什么,失去了什么,获得了什么,都应无怨无悔,这本来就是你自己的选择。发呆久了,人也变得迟钝,敲几行字清醒一下,不能秀逗了嘛。毕竟当你什么也没有的时候,还要在程序里讨得一碗残羹冷炙,饥寒交迫中求得一线希望,黑暗中等待黎明。
在项目中遇到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 |
位 | 可能的原因 |
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值被破坏了 |
位 | 可能的原因 |
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, 就有可能导致下一条指令没有对齐 |
位 | 可能的原因 |
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. 向量表偏移量设置有误 |
(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)。