UCOS-III學習筆記——OS獲取CPU控制權深入理解
μC/ OS III Version: v3.03.01
μC/ CPU Version: v1.29.01
μC/ LIB Version: v1.37.00
FOR: NXP LPC1768 CPU
在UCOS-III學習筆記——main函式中的配置與初始化一文中我們瞭解了在main()函式中ucos系統的初始化過程,在main()函式的末尾通過OSStart()函式中的OSStartHighRdy()函式,將CPU的控制權交給OS,啟動多工處理。
這一過程主要通過控制PendSV中斷,儲存上文恢復下文,以實現任務的轉換,大多使用的是底層的組合語言,所以跟核心關係密切,這裡我們使用的是cotex-M3核心進行實驗,適用於STM32,LPC1768等微控制器。下面我們將分析程式碼深入理解這一過程的具體步驟和原理,以下函式均在os_cpu_a.s檔案內。
1. OSStartHighRdy()
OSStartHighRdy LDR R0, =NVIC_SYSPRI14 ; 設定PendSV中斷的優先順序 LDR R1, =NVIC_PENDSV_PRI STRB R1, [R0] MOVS R0, #0 ; 將PSP指標設定為0 MSR PSP, R0 LDR R0, =OS_CPU_ExceptStkBase ; 初始化MSP指標 LDR R1, [R0] MSR MSP, R1 LDR R0, =NVIC_INT_CTRL ; 觸發PendSV中斷 LDR R1, =NVIC_PENDSVSET STR R1, [R0] CPSIE I ; 開中斷 OSStartHang B OSStartHang ; 程式永遠不會執行到這裡
- 首先,配置PendSV優先順序為0xff最低。PendSV是可懸起異常,如果我們把它配置最低優先順序,那麼如果同時有多個異常被觸發,它會在其他異常執行完畢後再執行,而且任何異常都可以中斷它。更詳細的內容在《Cortex-M3 權威指南》裡有介紹。
- 然後,將PSP指標設定為0(為後面的內容做準備),並將MSP賦值。其中,PSP為任務堆疊指標,MSP為主堆疊指標。在任務切換過程中主要使用的是PSP。
- 然後觸發PendSV中斷,但由於在main函式的初始化過程中關閉了所有中斷,所以此時還需要手動開啟所有中斷。
- 之後,程式將立即跳轉到PendSV中斷函式中,即OS_CPU_PendSVHandler。
2. OS_CPU_PendSVHandler()
OS_CPU_PendSVHandler
CPSID I ; 關中斷,防止切換過程中發生中斷
MRS R0, PSP ; 賦予r0為PSP的值
CBZ R0, OS_CPU_PendSVHandler_nosave ; 如果為0則跳轉(第一次必跳轉)
SUBS R0, R0, #0x20 ; 儲存 r4-11 暫存器
STM R0, {R4-R11}
LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] ; r0是當前任務的堆疊指標
- 首先,為了避免切換任務被打斷,需要關閉總中斷。
- 然後,讀取PSP的值,第一次進入中斷時,由於之前已經給PSP賦值為0,所以一定會直接跳轉到下面的OS_CPU_PendSVHandler_nosave()函式開始執行。
- 若不是第一次進入中斷,則需要做儲存上文的工作。則在進入中斷時,CPU將自動把當前任務的暫存器xPSR, PC, LR, R12, R0-R3存到當前任務堆疊中,PSP指向R0;剩下的r4-11暫存器則需要手動儲存到任務堆疊中,STM指令即為入棧的意思,PSP指標不變,改變的是R0暫存器,PSP依舊指向的是自動儲存後的地址。
- 暫存器進棧完畢後,讀取OSTCBCurPtr(當前任務TCB指標,即堆疊指標OSTCBStkPtr)的值,將其賦予R0,因為OS_TCB結構體第一個成員為任務的堆疊指標OSTCBStkPtr,OSTCBStkPtr的地址與任務TCB地址相同。
- 每個OS_TCB的OSTCBStkPtr的值在建立任務時被賦值,指向的是每個任務堆疊的R4的位置。所以當前OSTCBCurPtr的值為圖中StkPtr所示。
3. OS_CPU_PendSVHandler_nosave()
OS_CPU_PendSVHandler_nosave
PUSH {R14} ; 儲存LR
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
BLX R0
POP {R14}
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R1, =OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ; R0 新任務的堆疊指標
LDM R0, {R4-R11} ; POP出 r4-11
ADDS R0, R0, #0x20
MSR PSP, R0 ; 給PSP賦值
ORR LR, LR, #0x04 ; 確保使用的是PSP不是MSP
CPSIE I
BX LR ; 跳轉LR的地址
END
- 首先,函式首先利用儲存上文與恢復下文的間隙,呼叫執行OSTaskSwHook()使用者定義的任務切換對外介面函式,之後返回。
- 然後,將OSPrioCur(當前的優先順序)和OSTCBCurPtr(當前的任務TCB指標)更新(在進入中斷前,已經將OSPrioHighRdy和OSTCBHighRdyPtr通過相應的函式賦好了新的值,指向了新的高優先順序的任務,例如第一次賦值是在OSStart()函式中進行的)。
- 通過LDR指令,獲取任務的堆疊指標OSTCBStkPtr,將R4-R11手動POP出堆疊,存入CPU的暫存器中,因為退出中斷時剩餘暫存器將自動POP出堆疊,只需將R0當前的值賦予PSP即可,所以剩餘的暫存器我們無需操作。當剩餘暫存器自動POP出時,PSP將指向堆底。整個過程正好與儲存上文的過程相反。
- ORR LR, LR, #0x04 這一步確保在跳轉後使用的是PSP,《Cortex-M3 權威指南》裡有介紹。
- 之後開啟中斷,跳轉到LR的地址繼續執行新的任務。
總結來說:
OSStartHighRdy()函式完成的是OS開始前的準備工作;
OS_CPU_PendSVHandler()函式完成了上文儲存工作;
OS_CPU_PendSVHandler_nosave()函式完成的是切換任務恢復下文並跳轉執行的工作;
涉及到任務切換時,無論是任務級別的切換還是中斷級別的切換,實際的切換操作都是在PendSV異常中斷函式中實現的,實現過程如下:
1.如果系統是剛開機,準備執行第一個任務,此時PSP還為0,則呼叫切換函式之前已經得到OSTCBPrioRdy和OSPrioHighRdy的值,此時該任務的cpu各暫存器內容已經儲存在任務堆疊中,此時我們所做的就是將壓入堆疊的內容彈回cpu的各個暫存器,恢復執行現場(確切點說第一次不能用恢復),恢復現場之後將此時的OSTCBCur->OSTCBStkPtr給予cpu的sp,此時cpu的sp暫存器就儲存了現在執行任務的堆疊棧頂地址。
2.如果此次任務切換不是第一次,則此時PSP儲存的是切換之前任務的堆疊棧頂地址,此時我們可以用PUSH指令將R4-R11的內容根據PSP所提供的堆疊棧頂地址壓入堆疊。然後根據步驟1我們可以知道,在呼叫切換函式之前我們已經知道了OSTCBPrioRdy和OSPrioHighRdy,此時我們可以我們可以知道即將執行的任務的堆疊地址,根據堆疊地址我們用POP命令彈出堆疊儲存的值,恢復cpu的執行現場,彈出之後將OSTCBCur->OSTCBStkPtr的值給予cpu的sp暫存器,此時cpu的sp暫存器就儲存了現在執行任務的堆疊棧頂地址。