STM32F103项目中使用了uCOS-II,出现一个致命问题:当只跑uCOS-II时,程序运行正常,一旦开启USB功能(或任何其它带高优先级中断的程序),程序运行一段时间后就会死掉,时间是随机的。

通过keil启动程序,死机时停下来,看到死在HardFault_Handler中:

HardFault_Handler\
                PROC
                EXPORT  HardFault_Handler          [WEAK]
                B       .
                ENDP

提示出现了硬件错误。

看下这时的寄存器:


注意其中的LR,它是一个奇怪的值0xFFFFFFF5,后面再介绍它。


既然出现了硬件错误,可以看下异常寄存器(菜单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了(这一步没必要,只是为了验证对异常机制的理解)。

UsageFault_Handler\
                PROC
                EXPORT  UsageFault_Handler         [WEAK]
                B       .
                ENDP

这个时候异常寄存器的值也变成了:


可以看到HFSR的值没有了,只剩UFSR的值指示发生了INVPC错误。


现在要检查为什么会发生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按如下位置保存寄存器的值:

xPSR  (高地址)
PC
LR
R12
R3
R2
R1
R0 (低地址)

根据上面的顺序,在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)。

可以看到对应的汇编代码是:

  1621:                 OS_TASK_SW();                          /* Perform a context switch                     */ 
  1622:             } 
  1623:         } 
  1624:     } 
0x08000F4E F001F968  BL.W     OSCtxSw (0x08002222)
  1625:     OS_EXIT_CRITICAL(); 
0x08000F52 4620      MOV      r0,r4
0x08000F54 F001F952  BL.W     OS_CPU_SR_Restore (0x080021FC)
  1626: } 
0x08000F58 BD10      POP      {r4,pc}

对应的C源代码是:

void  OS_Sched (void)
{
    ...
    if (OSIntNesting == 0) {                           /* Schedule only if all ISRs done and ...       */
        if (OSLockNesting == 0) {                      /* ... scheduler is not locked                  */
                ...
                OSCtxSwCtr++;                          /* Increment context switch counter             */
                OS_TASK_SW();                          /* Perform a context switch                     */
            }
        }
    }
    OS_EXIT_CRITICAL();
}

所以理清思路,调用过程就是:

... --> OSCtxSw --> OS_CPU_SR_Restore --> OS_CPU_SR_Restore里引起UsageFault异常。


OS_CPU_SR_Restore的代码很简单(见前文),它并没有修改LR的值。所以继续看前面一个函数OSCtxSw。

这个函数在os_cpu_a.asm文件中:

OSCtxSw
    LDR     R0, =NVIC_INT_CTRL                                  ; Trigger the PendSV exception (causes context switch)
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR

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
    ...
                                                                ; At this point, entire context of process has been saved
OS_CPU_PendSVHandler_nosave
    ...
    LDR     R0, [R2]                                            ; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr;
    LDM     R0, {R4-R11}                                        ; Restore r4-11 from new process stack
    ADDS    R0, R0, #0x20
    MSR     PSP, R0                                             ; Load PSP with new process SP
    ORR     LR, LR, #0x04                                       ; Ensure exception return uses process stack
    CPSIE   I
    BX      LR      

在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文件里:

NVIC_INT_CTRL   EQU     0xE000ED04                              ; Interrupt control state register.
NVIC_SYSPRI2    EQU     0xE000ED20                              ; System priority register (priority 2).
NVIC_PENDSV_PRI EQU     0xFFFF                              ; PendSV priority value (lowest).
NVIC_PENDSVSET  EQU     0x10000000                              ; Value to trigger PendSV exception.


OSStartHighRdy
    LDR     R0, =NVIC_SYSPRI2                                   ; Set the PendSV exception priority
    LDR     R1, =NVIC_PENDSV_PRI
    STRB    R1, [R0]


OSStartHighRdy将0xE000ED20置为0xFFFF来设置PendSV的优先级,查看《Cortex-M3 Devices Generic User Guide》,发现0xE000ED20的[23:16]才是PendSV的优先级位,这里中断优先级的值错误了。

所以NVIC_PENDSV_PRI的值错误才是罪魁祸首!


考虑到优先级的写入指令是STRB指令,那么NVIC_SYSPRI2的值也需要修改。

将这两个值改为如下数值后,问题就解决了:

NVIC_INT_CTRL   EQU     0xE000ED04                              ; Interrupt control state register.
NVIC_SYSPRI2    EQU     0xE000ED22                              ; System priority register (priority 2).
NVIC_PENDSV_PRI EQU     0x000000FF                              ; PendSV priority value (lowest).
NVIC_PENDSVSET  EQU     0x10000000                              ; Value to trigger PendSV exception.


正确运行情况下PendSV的优先级应该是15,如下图所示:



至于这两个值为什么会错呢?可能是uCOS-II代码是从其他工程拷过来的,那个不是Cotex-M3架构(M1架构?),所以那边的值到这边不能使用,具体来源已不可考。

基本上这就是个乱拷uCOS代码引起的悲剧




Logo

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

更多推荐