STM32F103項目中使用了uCOS-II,出現(xiàn)一個致命問題:當(dāng)只跑uCOS-II時,程序運行正常,一旦開啟
USB功能(或任何其它帶高優(yōu)先級中斷的程序),程序運行一段時間后就會死掉,時間是隨機的。
通過keil啟動程序,死機時停下來,看到死在HardFault_Handler中:
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
提示出現(xiàn)了硬件錯誤。
看下這時的寄存器:
注意其中的LR,它是一個奇怪的值0xFFFFFFF5,后面再介紹它。
既然出現(xiàn)了硬件錯誤,可以看下異常寄存器(菜單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官網(wǎng)直接下載),SCB寄存器(System control block (SCB))的地址是0xE000E000開始的,HardFault Status Register的地址是0xE000ED2C(HFSR,
百度上也有),查看相應(yīng)地址的值:
可以看到HFSR(0xE000ED2C)的值是0x40000000,而UFSR(0xE000ED2A)的值是0x0004,查看《Cortex-M3 Devices Generic User Guide》手冊,相應(yīng)比特位的意思是:
HFSR的FORCED位為1,表示硬件錯誤的原因是上訪造成的,此位置1表示產(chǎn)生了其他類型的異常,但由于優(yōu)先級問題或者使能問題導(dǎo)致無法處理異常,于是這些異常就升級成硬件錯誤異常。
UFSR的INVPC位為1,表示在異常中斷返回時嘗試向PC載入非法的EXC_RETURN值,從而引起用法錯誤。
這里用法錯誤升級為硬件錯誤的原因是沒有使能用法錯誤位,SHCRS(0xE000ED24, System Handler Control and State Register)的USGFAULTENA位為0。
使用keil重新加載程序,在運行程序之前先將0xE000ED24的值改為0x00070000(臨時使能USGFAULTENA位),然后再次運行,程序死機時可以看到死機的位置變成UsageFault_Handler了(這一步?jīng)]必要,只是為了驗證對異常機制的理解)。
UsageFault_Handler\
PROC
EXPORT UsageFault_Handler [WEAK]
B .
ENDP
這個時候異常寄存器的值也變成了:
可以看到HFSR的值沒有了,只剩UFSR的值指示發(fā)生了INVPC錯誤。
現(xiàn)在要檢查為什么會發(fā)生INVPC錯誤。
對于INVPC錯誤,《Cortex-M3 Devices Generic User Guide》的描述是:
這上面說如果由于錯誤的上下文,或者錯誤的EXC_RETURN值,導(dǎo)致向PC中非法載入EXC_RETURN值,就會引起此錯誤。
當(dāng)INVPC位是1的時候,在相應(yīng)的堆棧中保存了引起這個錯誤的異常中斷返回點的PC值。
說一下EXC_RETURN是什么意思:
EXC_RETURN是用于程序從異常中斷中返回的。
根據(jù)Cortex-M3的異常處理流程,當(dāng)發(fā)生異常時,CPU先將核心寄存器壓入當(dāng)前堆棧(如果當(dāng)前是線程模式,則壓入PSP堆棧,如果當(dāng)前是Handler模式,則壓入MSP堆棧),然后CPU會將LR設(shè)置為一個特殊的值,比如0xFFFFFFFD,然后切換到Handler模式,切換成MSP堆棧,最后進(jìn)入異常處理例程(異常處理例程總是使用MSP堆棧)。在異常處理例程完成后需要從中斷返回時,就將LR的值載入到PC中(通常是BX LR指令,也可以是MOV PC,LR指令,或者POP {..., PC}等指令,只要能將LR賦給PC即可),由于LR的值是0xFFFFFFFD,CPU檢測到向PC中載入的是這個特殊值時,就知道是中斷返回,于是做中斷返回的動作(與壓入動作相反:從堆棧中彈出核心寄存器的值,恢復(fù)到線程模式或Handler模式等)。
這里這個特殊的值(0xFFFFFFFD)就是EXC_RETURN,它的特點是高28位全部是1,只有低4位可變化,不同的低4位表示不同的中斷返回動作。
這個值是CPU在進(jìn)入異常處理前自動設(shè)置的,只有3個值是合法的:
0xFFFFFFF1 表示中斷返回時從MSP堆?;謴?fù)寄存器值,中斷返回后進(jìn)入Handler模式,使用MSP堆棧,(相當(dāng)于從中斷返回到另一個中斷)。
0xFFFFFFF9 表示中斷返回時從MSP堆棧恢復(fù)寄存器值,中斷返回后進(jìn)入線程模式,使用MSP堆棧(這種用于不使用PSP只使用MSP堆棧的情況)。
0xFFFFFFFD 表示中斷返回時從PSP堆?;謴?fù)寄存器值,中斷返回后進(jìn)入線程模式,使用PSP堆棧(這是常見的,OS處理完中斷后返回用戶程序)。
可以看到,中斷返回依賴于LR中的值,在此項目中,LR的值變成了0xFFFFFFF5,顯然也是一個EXC_RETURN值,但這個值與上面3個都不同,是非法的,所以引起了INVPC錯誤。
進(jìn)入中斷時LR的值是CPU自動設(shè)置的,不會有錯,為什么退出中斷時LR值變成非法的了呢?只有一個原因:中斷例程修改了LR的值,改錯了。
為了找到修改LR的中斷例程,需要找到引起UsageFault的中斷返回指令。
下面根據(jù)UsageFault錯誤信息查找引起錯誤的指令。
如INVPC描述中所述,堆棧中保存了引起UsageFault錯誤的位置。
首先查看LR的值(0xFFFFFFF5),第4比特位是1,所以使用的是PSP堆棧(不要去管R13(SP)的值,R13是MSP堆棧的值)。
PSP的值是0x20000760(見前面寄存器
截圖),查看相應(yīng)內(nèi)存的值:
根據(jù)Cortex-M3的異常處理流程,進(jìn)入中斷時,CPU按如下
位置保存寄存器的值:
xPSR (高地址)
PC
LR
R12
R3
R2
R1
R0 (低地址)
根據(jù)上面的順序,在PSP堆棧的截圖中標(biāo)記了對應(yīng)的寄存器位置,它們就是進(jìn)入異常中斷(這里是UsageFault異常)前CPU所處的狀態(tài)。
從PSP堆棧的值可以看到,進(jìn)入UsageFault異常中斷前,PC值是0x08002200,LR值是0x08000F59。
在keil的反匯編窗口,右鍵菜單選擇“Show Disassembley at Address...”,輸入0x08002200,對應(yīng)的源碼區(qū)也一起變化,可以看到引起UsageFault異常的代碼是:
OS_CPU_SR_Restore MSR PRIMASK, R0 BX LR
這是uCOS-II里的代碼,就是這條BX LR引起的UsageFault異常,應(yīng)該是運行到這里的時候,LR值已經(jīng)被錯誤修改了。
到這里還看不出什么,繼續(xù)往上走一層,查看LR對應(yīng)的代碼,在反匯編窗口中查看0x08000F58的代碼(注意LR中的值是奇數(shù),表示返回地址是THUMB指令,實際代碼地址是其對應(yīng)的偶數(shù)地址0x08000F58)。
可以看到對應(yīng)的匯編代碼是:
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}
對應(yīng)的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();
}
所以理清思路,調(diào)用過程就是:
... --> OSCtxSw --> OS_CPU_SR_Restore --> OS_CPU_SR_Restore里引起UsageFault異常。
OS_CPU_SR_Restore的代碼很簡單(見前文),它并沒有修改LR的值。所以繼續(xù)看前面一個函數(shù)OSCtxSw。
這個函數(shù)在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函數(shù)自己用了LR,好像也不會亂改LR(否則自己就無法工作),線索好像中斷了。
但仔細(xì)看OSCtxSw的代碼,它實際上是激活了PendSV標(biāo)志后返回。閱讀OS_Sched代碼可知:在OSCtxSw里設(shè)置PendSV標(biāo)志時并不會立即觸發(fā)中斷,因為此時CPU的全局中斷是關(guān)斷的,只有當(dāng)全局中斷被打開時,這個PendSV中斷才會真正觸發(fā)。
什么時候會打開全局中斷呢?看OS_CPU_SR_Restore的第一句:
OS_CPU_SR_Restore MSR PRIMASK, R0 BX LR
MSR PRIMASK, R0,就是在這句打開的。
所以在這句代碼運行后就會觸發(fā)PendSV中斷,PendSV中斷返回后就會運行BX LR指令。而PendSV中斷是uCOS-II真正做任務(wù)上下文切換的地方,它會大量修改CPU寄存器,是很有可能修改LR的。
所以繼續(xù)查看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,如果這里改錯了,就會引起問題(疑問:這里改錯了后,應(yīng)該直接在最后那句BX LR時就會引起異常,為何PSP堆棧中記錄的是OS_CPU_SR_Restore中出錯的呢?實際上,真正定位問題的那次,就是抓到在OS_CPU_PendSVHandler里的BX LR出錯的,但在寫此文時又抓到是在OS_CPU_SR_Restore里出錯的)。
出錯時LR值是0xFFFFFFF5,那么在運行這句ORR語句之前,LR的值應(yīng)該是0xFFFFFFF1。
0xFFFFFFF1是合法的,它表示異常中斷返回后回到另一個異常中斷。uCOS-II是很成熟的代碼,這里怎么會改錯LR呢?看uCOS-II的說明,OS_CPU_PendSVHandler在返回前將LR異或0x04是為了保證返回后使用的是PSP堆棧,也就是它需要保證回到用戶代碼。那么這說明OS_CPU_PendSVHandler一定是在用戶模式下才進(jìn)入此中斷的,必須不能是在已有其他中斷的情況下進(jìn)入OS_CPU_PendSVHandler(嵌套中斷)。
怎么保證OS_CPU_PendSVHandler不會在已有其他中斷的情況下進(jìn)入呢?看下uCOS-II的說明,發(fā)現(xiàn)PendSV中斷的優(yōu)先級必須是最低,這樣就保證OS_CPU_PendSVHandler不會在有其他中斷時進(jìn)入。
這里L(fēng)R的本來值是0xFFFFFFF1(后被修改成0xFFFFFFF5),說明OS_CPU_PendSVHandler是在其他中斷中進(jìn)入的。那么一定是PendSV的優(yōu)先級出了問題。
查看PendSV的中斷優(yōu)先級(菜單Peripherals-->Core Peripherals-->Nested Vectored Interrupt Controller):
果然PendSV的優(yōu)先級不是最低的,它是0,與其它中斷處于相同優(yōu)先級。
顯然,只要Pend System Service的優(yōu)先級不是最低,就會引起上述問題。
檢查設(shè)置PendSV優(yōu)先級的代碼,也在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來設(shè)置PendSV的優(yōu)先級,查看《Cortex-M3 Devices Generic User Guide》,發(fā)現(xiàn)0xE000ED20的[23:16]才是PendSV的優(yōu)先級位,這里中斷優(yōu)先級的值錯誤了。
所以NVIC_PENDSV_PRI的值錯誤才是
罪魁禍?zhǔn)?/a>!考慮到優(yōu)先級的寫入指令是STRB指令,那么NVIC_SYSPRI2的值也需要修改。將這兩個值改為如下數(shù)值后,問題就解決了: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的優(yōu)先級應(yīng)該是15,如下圖所示:至于這兩個值為什么會錯呢?可能是uCOS-II代碼是從其他工程拷過來的,那個不是Cotex-M3架構(gòu)(M1架構(gòu)?),所以那邊的值到這邊不能使用,具體來源已不可考。基本上這就是個亂拷uCOS代碼引起的悲劇