學習目的:
- 熟悉uCOS-III任務間切換實現原理
在使用單片機做一些復雜的產品開發時,單純的裸機系統通常不能很完美的解決問題,為了降低編程的難度,開發中我們一般會引入RTOS進行多任務管理。在引入RTOS的后,編程思想和裸機系統程序設計有所不同,我們會根據產品所要實現的功能,將整個系統分割成一個個獨立的且無法返回的函數,這些函數也就是我們通常所講的任務。不同的任務在RTOS內核的管理下不停運行,宏觀上,不同的任務之間仿佛是在同時運行的。但實際上對於單片機而言,一般情況下它只有一個CPU,每個時間點只能運行一個任務,其實宏觀上的並行不是真正的並行,而是RTOS在不同任務間進行切換,分別讓不同任務獲取CPU資源運行。由於CPU的運行速度很快,從而讓人感覺到任務間好像是同時運行的
這里我們來分析在Cortex-M3架構下,uCOS-III內核中任務間切換是如何實現的。值得注意的是,任務間切換是和CPU相關的,不同內核的CPU在切換細節上可能有所不同,但思路上應該是一致的。本文講述的只是uCOS-III任務切換的底層實現細節,對於任務間何時進行切換沒有進行詳細描述
一、觸發任務切換
所謂任務切換的觸發就是當OS內核判斷滿足任務調度條件時,告訴CPU去執行任務切換的代碼。uCOS-III內核設計時,將任務間切換定在了在PendSV異常的服務函數中完成,因此任務切換的觸發也就是去讓CPU進入PendSV異常
對於ARM Cortex-M3架構的單片機,可通過設置SCB寄存器(異常和中斷控制寄存器)中包含的ICSR寄存器(中斷控制及狀態寄存器)的第28位來觸發PendSV異常,ICSR寄存器的地址是0xE000ED04。當向ICSR寄存器的第28位寫1,PendSV異常被掛起,當其他高優先級的中斷被執行后,被掛起的PendSV異常被執行。此時,CPU跳轉到PendSV異常入口,執行PendSV異常服務程序
由此可以知道,運行在Cortex-M3內核單片機上的uCOS-III系統,如果想進行任務切換,需要向0xE000ED04寄存器中寫入0x10000000來觸發PendSV異常
為了通用性,uCOS-III中將觸發PendSV異常的操作封裝成了一些宏,這樣uCOS-III內核上層的程序不需要關心不同硬件觸發PendSV異常的具體細節,只需要調用這個宏即可。這些宏在os_cpu.h文件中定義的,具體實現需要由移植人員根據所使用的單片機架構去完成,如在Cortex-M3內核單片機上,這些宏的實現如下:
#ifndef NVIC_INT_CTRL #define NVIC_INT_CTRL *((CPU_REG32 *)0xE000ED04) #endif #ifndef NVIC_PENDSVSET #define NVIC_PENDSVSET 0x10000000 #endif #define OS_TASK_SW() NVIC_INT_CTRL = NVIC_PENDSVSET #define OSIntCtxSw() NVIC_INT_CTRL = NVIC_PENDSVSET
不過,實際上,uCOS-III在進行任務切換時並沒有直接去調用OS_TASK_SW(),內核設計者將OS_TASK_SW()放在OSSched函數中,OSSched函數才是上層任務切換直接調用到的函數。OSSched函數中先找到當前任務就緒表中優先級最高的就緒任務,然后調用OS_TASK_SW()觸發PendSV異常,等待CPU處理PendSV異常,在異常中完成任務間的切換
void OSSched (void) { ... OSPrioHighRdy = OS_PrioGetHighest(); /* Find the highest priority ready */ OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr; ... OS_TASK_SW(); /* Perform a task level context switch */ ... }
二、任務切換實現
uCOS-III任務的切換是在PendSV異常中進行的,當內核想進行任務切換時,調用OSSched函數。該函數最終會觸發PendSV異常,緊接着CPU進入PendSV異常的入口,調用PendSV異常處理函數,任務切換的實現也就是在PendSV異常處理函數中完成的。下面我們將會具體分析下任務切換的實現機制
在介紹任務切換實現的具體內容前,我們先了解一個uCOS-III中一個核心的數據結構——TCB(任務控制塊)。任務控制塊,本質上就是一個結構體,在軟件上代表的就是對一個任務的抽象,相當於任務的身份證,里面存有任務的所有信息,比如任務的堆棧,任務名稱,任務參數等。有了這個任務控制塊之后,以后系統對任務的全部操作都可以通過這個TCB來實現。任務控制塊中有很多內容,下面只介紹了我們今天所要使用到的幾個成員
struct os_tcb { CPU_STK *StkPtr; /* Pointer to current top of stack */ ... CPU_STK *StkBasePtr; /* Pointer to base address of stack */ ... OS_TASK_PTR TaskEntryAddr; /* Pointer to task entry point address */ ... OS_PRIO Prio; /* Task priority (0 == highest) */ CPU_STK_SIZE StkSize; /* Size of task stack (in number of stack elements) */ ... };
- StkPtr:指向當前任務棧頂指針
- StkBasePtr:指向當前任務棧的基地址指針
- TaskEntryAddr:指向任務的入口函數指針
- Prio:任務優先級
- StkSize:任務分配棧大小
uCOS-III中,每創建的一個任務,需要實例化一個os_tcb對象來描述這個任務,同時,需要給每個創建的任務分配一個棧空間和優先級,os_tcb中記錄了當前任務的所有信息。每個os_tcb對象里的信息,在調用OSTaskCreate時被初始化
好了,了解了任務控制塊的一些信息后,我們就開始來分析任務切換的具體實現。任務切換代碼是用匯編來編寫的,存放在os_cpu_a.s文件中,它依賴於CPU,CPU不同實現上也有所不同
OS_CPU_PendSVHandler CPSID I ; 關中斷 MRS R0, PSP ---------------------------->① ; PSP is process stack pointer CBZ R0, OS_CPU_PendSVHandler_nosave ----->② ; Skip register save the first time SUBS R0, R0, #0x20------------------------>③ ; Save remaining regs r4-11 on process stack STM R0, {R4-R11} LDR R1, =OSTCBCurPtr--------------------->④ ; OSTCBCurPtr->OSTCBStkPtr = SP; LDR R1, [R1] STR R0, [R1] ; R0 is SP of process being switched out ; At this point, entire context of process has been saved OS_CPU_PendSVHandler_nosave PUSH {R14}-------------------------------->⑤ ; Save LR exc_return value 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 is new process SP; SP = OSTCBHighRdyPtr->StkPtr; 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 ; Exception return will restore remaining context END
① 將PSP指針的值加載到R0寄存器中,PSP指針指向當前任務的堆棧棧頂
Cortex-M3處理器內核共有兩個堆棧指針,也就是支持兩個堆棧。當引用R13(或寫作SP)時,引用到的是當前正在使用的那一個,另一個必須使用特殊的指令來訪問(MSR,MRS指令)。這兩個堆棧指針分別是:
- 主堆棧指針(MSP),這是缺省的堆棧指針,它由OS內核、異常服務例程以及所有需要特殊訪問的應用程序代碼來使用
- 進程堆棧指針(PSP),用於常規的應用程序代碼(不處於異常服務例程中)
此時,CPU進入PendSV異常,使用的是MSP指針,此時的PSP指針保存的是入棧前的任務的當前堆棧地址
② 判斷R0是否為0, 如果為0跳到OS_CPU_PendSVHandler_nosave位置運行。此時R0寄存器表示的是PSP寄存器內容,在第一次進入任務調度時,PSP指針會被設置成0,對於第一次進行任務調度這種情況不需要執行下面的將被切換的任務的寄存器內容入棧操作
③ 手動將R4-R11寄存器內容存放到被切換出的任務的堆棧空間中,寄存器入棧,用於后續該任務重新調度時的現場恢復
④ 修改被切換出的任務的任務控制塊中記錄當前堆棧的棧頂的位置信息,即更新任務控制塊中StkPtr指針內容
①~④步驟,完成了將被切換出的任務的寄存器信息寫入到該任務的棧中,並修改任務控制塊中棧頂指針的位置。執行過程可如下圖內容所示:
⑤ 調用任務切換的鈎子函數OSTaskSwHook
⑥ 修改當前運行任務優先級變量,OSPrioCur = OSPrioHighRdy
⑦ 修改當前運行任務指針,OSTCBCurPtr = OSTCBHighRdyPtr,當前運行任務為最高優先級任務
⑧ 找到當前要運行任務的堆棧指針,將棧中保存的R4~R11寄存器信息重新加載到R4~R11寄存器中
⑨ 修改PSP堆棧指針指向新任務堆棧
⑩ 異常返回,這個時候任務堆棧中的剩下內容將會自動加載到xPSR,PC,R14,R12,R3,R2,R1,R0
異常返回時,通過修改LR的位2為1,實現模式切換,確保從MSP指針切換到PSP指針
⑥~⑩步驟,從即將要運行的任務的任務控制塊中找到該任務棧頂信息,完成新任務的恢復現場操作。執行細節可如下圖內容所示:
三、小結
uCOS-III中任務間的切換是通過觸發PendSV異常來實現的,在異常處理函數中,將當前任務現場信息存放到該任務的堆棧中,以便於后續該任務的恢復。然后找到下一個要運行的任務的堆棧地址,修改PSP指針指向該堆棧地址,執行恢復現場操作,這樣就完成了兩個任務間的切換工作