1. 基础知识
Fault 类异常
有若干个系统异常专用于 fault 处理。 CM3 中的 Faults 可分为以下几类:
- 总线 faults
- 存储器管理 faults
- 用法 faults
- 硬 fault
表 7.8 总线 fault 状态寄存器(BFSR), 地址:0xE000_ED29
表 7.9 存储器管理 fault 状态寄存器(MFSR),地址:0xE000_ED28
表 7.10 用法 fault 状态寄存器(UFSR), 地址:0xE000_ED2A
表 7.11 硬 fault 状态寄存器 地址:0xE000_ED2C
1.1 总线 Faults
当 AHB 接口上正在传送数据时,如果回复了一个错误信号(error response),则会产生总线 faults,产生的场合可以是:
- z 取指,通常被称作“预取流产”(prefetch abort)
- z 数据读/写,通常被称作“数据流产”(data abort)
在 CM3 中执行如下动作可以触发总线异常:
- 中断处理起始阶段的堆栈 PUSH 动作。 称为“入栈错误”
- 中断处理收尾阶段的堆栈 POP 动作。 称为“出栈错误”
- 在处理器启动中断处理序列(sequence)后的向量读取时。这是一种罕见的特殊情况,被归类为硬 fault。
1.3存储器管理 faults
存储器管理faults多与MPU有关,其诱因常常是某次访问触犯了MPU设置的保护策略。另外,某些非法访问,例如,在不可执行的存储器区域试图取指,也会触发一个 MemManagefault,而且即使没有 MPU 也会触发。MemManage faults 的常见诱因如下所示:
- 访问了 MPU 设置区域覆盖范围之外的地址
- 往只读 region 写数据
- 用户级下访问了只允许在特权级下访问的地址
1.4 用法 faults
用法 faults 发生的场合可以是:
- 执行了未定义的指令
- 执行了协处理器指令(Cortex‐M3 不支持协处理器,但是可以通过 fault 异常机制来使用软件模拟协处理器的功能,从而可以方便地在其它 Cortex 处理器间移植)
- 尝试进入 ARM 状态(因为 CM3 不支持 ARM 状态,所以用法 fault 会在切换时产生。软件可以利用此机制来测试某处理器是否支持 ARM 状态)
- 无效的中断返回(LR 中包含了无效/错误的值)
- 使用多重加载/存储指令时,地址没有对齐。另外,通过设置 NVIC 的对应控制位,可以在下列场合下也产生用法 fault:
- 除数为零
- 任何未对齐的访问
1.5硬 fault
硬 fault 是上文讨论的总线 fault、存储器管理 fault 以及用法 fault *的结果。如果这些 fault 的服务例程无法执行,它们就会成为“硬伤”——*(escalation)成硬 fault。另外,在取向量(异常处理是对异常向量表的读取)时产生的总线 fault 也按硬 fault 处理。
在NVIC 中有一个硬 fault 状态寄存器(HFSR),它指出产生硬 fault 的原因。如果不是由于取向量造成的,则硬 fault 服务例程必须检查其它的 fault 状态寄存器,以最终决定是谁*的。
2. 经典案例
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代码引起的悲剧