STM32F103项目中使用了uCOS-II,出现一个致命问题:当只跑uCOS-II时,程序运行正常,一旦开启USB功能(或任何其它带高优先级中断的程序),程序运行一段时间后就会死掉,时间是随机的。 通过keil启动程序,死机时停下来,看到死在HardFault_Handler中:
提示出现了硬件错误。 看下这时的寄存器:
既然出现了硬件错误,可以看下异常寄存器(菜单Peripherals->Core Peripherals->Fault Reports): (这样查看比较快,后文有通过寄存器查看的方法) 可以看到硬件错误(Hard Faults)是上访造成的(FORCED位),而真实的错误原因是由用法错误(Usage Faults)引起的,具体引起用法错误的原因是INVPC错误。 如果用的不是Keil,也可以通过直接查看异常寄存器值来得到错误原因。 查看《ARM Cortex-M3 Processor Technical Reference Manual》和《Cortex-M3 Devices Generic User Guide》这两个手册(可以从ARM官网直接下载),SCB寄存器(System control block (SCB))的地址是0xE000E000开始的,HardFault Status Register的地址是0xE000ED2C(HFSR,百度上也有),查看相应地址的值: 可以看到HFSR(0xE000ED2C)的值是0x40000000,而UFSR(0xE000ED2A)的值是0x0004,查看《Cortex-M3 Devices Generic User Guide》手册,相应比特位的意思是: HFSR的FORCED位为1,表示硬件错误的原因是上访造成的,此位置1表示产生了其他类型的异常,但由于优先级问题或者使能问题导致无法处理异常,于是这些异常就升级成硬件错误异常。 UFSR的INVPC位为1,表示在异常中断返回时尝试向PC载入非法的EXC_RETURN值,从而引起用法错误。 这里用法错误升级为硬件错误的原因是没有使能用法错误位,SHCRS(0xE000ED24, System Handler Control and State Register)的USGFAULTENA位为0。 使用keil重新加载程序,在运行程序之前先将0xE000ED24的值改为0x00070000(临时使能USGFAULTENA位),然后再次运行,程序死机时可以看到死机的位置变成UsageFault_Handler了(这一步没必要,只是为了验证对异常机制的理解)。
这个时候异常寄存器的值也变成了:
现在要检查为什么会发生INVPC错误。 对于INVPC错误,《Cortex-M3 Devices Generic User Guide》的描述是: 这上面说如果由于错误的上下文,或者错误的EXC_RETURN值,导致向PC中非法载入EXC_RETURN值,就会引起此错误。 当INVPC位是1的时候,在相应的堆栈中保存了引起这个错误的异常中断返回点的PC值。 说一下EXC_RETURN是什么意思: EXC_RETURN是用于程序从异常中断中返回的。 根据Cortex-M3的异常处理流程,当发生异常时,CPU先将核心寄存器压入当前堆栈(如果当前是线程模式,则压入PSP堆栈,如果当前是Handler模式,则压入MSP堆栈),然后CPU会将LR设置为一个特殊的值,比如0xFFFFFFFD,然后切换到Handler模式,切换成MSP堆栈,最后进入异常处理例程(异常处理例程总是使用MSP堆栈)。在异常处理例程完成后需要从中断返回时,就将LR的值载入到PC中(通常是BX LR指令,也可以是MOV PC,LR指令,或者POP {..., PC}等指令,只要能将LR赋给PC即可),由于LR的值是0xFFFFFFFD,CPU检测到向PC中载入的是这个特殊值时,就知道是中断返回,于是做中断返回的动作(与压入动作相反:从堆栈中弹出核心寄存器的值,恢复到线程模式或Handler模式等)。 这里这个特殊的值(0xFFFFFFFD)就是EXC_RETURN,它的特点是高28位全部是1,只有低4位可变化,不同的低4位表示不同的中断返回动作。 这个值是CPU在进入异常处理前自动设置的,只有3个值是合法的: 0xFFFFFFF1 表示中断返回时从MSP堆栈恢复寄存器值,中断返回后进入Handler模式,使用MSP堆栈,(相当于从中断返回到另一个中断)。 0xFFFFFFF9 表示中断返回时从MSP堆栈恢复寄存器值,中断返回后进入线程模式,使用MSP堆栈(这种用于不使用PSP只使用MSP堆栈的情况)。 0xFFFFFFFD 表示中断返回时从PSP堆栈恢复寄存器值,中断返回后进入线程模式,使用PSP堆栈(这是常见的,OS处理完中断后返回用户程序)。 可以看到,中断返回依赖于LR中的值,在此项目中,LR的值变成了0xFFFFFFF5,显然也是一个EXC_RETURN值,但这个值与上面3个都不同,是非法的,所以引起了INVPC错误。 进入中断时LR的值是CPU自动设置的,不会有错,为什么退出中断时LR值变成非法的了呢?只有一个原因:中断例程修改了LR的值,改错了。 为了找到修改LR的中断例程,需要找到引起UsageFault的中断返回指令。 下面根据UsageFault错误信息查找引起错误的指令。 如INVPC描述中所述,堆栈中保存了引起UsageFault错误的位置。 首先查看LR的值(0xFFFFFFF5),第4比特位是1,所以使用的是PSP堆栈(不要去管R13(SP)的值,R13是MSP堆栈的值)。 PSP的值是0x20000760(见前面寄存器截图),查看相应内存的值: 根据Cortex-M3的异常处理流程,进入中断时,CPU按如下位置保存寄存器的值:
根据上面的顺序,在PSP堆栈的截图中标记了对应的寄存器位置,它们就是进入异常中断(这里是UsageFault异常)前CPU所处的状态。 从PSP堆栈的值可以看到,进入UsageFault异常中断前,PC值是0x08002200,LR值是0x08000F59。 在keil的反汇编窗口,右键菜单选择“Show Disassembley at Address...”,输入0x08002200,对应的源码区也一起变化,可以看到引起UsageFault异常的代码是: OS_CPU_SR_Restore MSR PRIMASK, R0 BX LR 这是uCOS-II里的代码,就是这条BX LR引起的UsageFault异常,应该是运行到这里的时候,LR值已经被错误修改了。 到这里还看不出什么,继续往上走一层,查看LR对应的代码,在反汇编窗口中查看0x08000F58的代码(注意LR中的值是奇数,表示返回地址是THUMB指令,实际代码地址是其对应的偶数地址0x08000F58)。 可以看到对应的汇编代码是:
对应的C源代码是:
所以理清思路,调用过程就是: ... --> OSCtxSw --> OS_CPU_SR_Restore --> OS_CPU_SR_Restore里引起UsageFault异常。 OS_CPU_SR_Restore的代码很简单(见前文),它并没有修改LR的值。所以继续看前面一个函数OSCtxSw。 这个函数在os_cpu_a.asm文件中:
OSCtxSw函数自己用了LR,好像也不会乱改LR(否则自己就无法工作),线索好像中断了。 但仔细看OSCtxSw的代码,它实际上是激活了PendSV标志后返回。阅读OS_Sched代码可知:在OSCtxSw里设置PendSV标志时并不会立即触发中断,因为此时CPU的全局中断是关断的,只有当全局中断被打开时,这个PendSV中断才会真正触发。 什么时候会打开全局中断呢?看OS_CPU_SR_Restore的第一句: OS_CPU_SR_Restore MSR PRIMASK, R0 BX LR MSR PRIMASK, R0,就是在这句打开的。 所以在这句代码运行后就会触发PendSV中断,PendSV中断返回后就会运行BX LR指令。而PendSV中断是uCOS-II真正做任务上下文切换的地方,它会大量修改CPU寄存器,是很有可能修改LR的。 所以继续查看PensSV中断处理代码,也在os_cpu_a.asm文件中, OS_CPU_PendSVHandler的最后代码是:
在OS_CPU_PendSVHandler返回前强制将LR异或0x04,这里强制修改LR,如果这里改错了,就会引起问题(疑问:这里改错了后,应该直接在最后那句BX LR时就会引起异常,为何PSP堆栈中记录的是OS_CPU_SR_Restore中出错的呢?实际上,真正定位问题的那次,就是抓到在OS_CPU_PendSVHandler里的BX LR出错的,但在写此文时又抓到是在OS_CPU_SR_Restore里出错的)。 出错时LR值是0xFFFFFFF5,那么在运行这句ORR语句之前,LR的值应该是0xFFFFFFF1。 0xFFFFFFF1是合法的,它表示异常中断返回后回到另一个异常中断。uCOS-II是很成熟的代码,这里怎么会改错LR呢?看uCOS-II的说明,OS_CPU_PendSVHandler在返回前将LR异或0x04是为了保证返回后使用的是PSP堆栈,也就是它需要保证回到用户代码。那么这说明OS_CPU_PendSVHandler一定是在用户模式下才进入此中断的,必须不能是在已有其他中断的情况下进入OS_CPU_PendSVHandler(嵌套中断)。 怎么保证OS_CPU_PendSVHandler不会在已有其他中断的情况下进入呢?看下uCOS-II的说明,发现PendSV中断的优先级必须是最低,这样就保证OS_CPU_PendSVHandler不会在有其他中断时进入。 这里LR的本来值是0xFFFFFFF1(后被修改成0xFFFFFFF5),说明OS_CPU_PendSVHandler是在其他中断中进入的。那么一定是PendSV的优先级出了问题。 查看PendSV的中断优先级(菜单Peripherals-->Core Peripherals-->Nested Vectored Interrupt Controller): 果然PendSV的优先级不是最低的,它是0,与其它中断处于相同优先级。 显然,只要Pend System Service的优先级不是最低,就会引起上述问题。 检查设置PendSV优先级的代码,也在os_cpu_a.asm文件里:
OSStartHighRdy将0xE000ED20置为0xFFFF来设置PendSV的优先级,查看《Cortex-M3 Devices Generic User Guide》,发现0xE000ED20的[23:16]才是PendSV的优先级位,这里中断优先级的值错误了。 所以NVIC_PENDSV_PRI的值错误才是罪魁祸首! 考虑到优先级的写入指令是STRB指令,那么NVIC_SYSPRI2的值也需要修改。 将这两个值改为如下数值后,问题就解决了:
至于这两个值为什么会错呢?可能是uCOS-II代码是从其他工程拷过来的,那个不是Cotex-M3架构(M1架构?),所以那边的值到这边不能使用,具体来源已不可考。 基本上这就是个乱拷uCOS代码引起的悲剧 |
|
来自: 刀首木 > 《HardFault》