1 UCOSII定義的關鍵數據結構
OS_EXT INT8U OSIntNesting;
OSIntNesting用於判斷當前系統是否正處於中斷處理例程中。
OS_EXT INT8U OSPrioCur;
OSPrioCur表示當前進程的優先級。
OS_EXT INT8U OSPrioHighRdy;
OSPrioHighRdy表示最高優先級任務的優先級。
OS_EXT OS_PRIO OSRdyGrp;
OSRdyGrp主要來標記可運行任務優先級除去低位3位或4位后的group bit位。例如任務的優先級為65(假設超過了系統最大的優先級超過了63,那么OS_PRIO應該有16位),由於超過了63,所以這個任務的group bit位為2^(65>>4)=2^4,也就是OSRdyGrp的第5位(從低位開始算起)為1。
OS_EXT OS_PRIO OSRdyTbl[OS_RDY_TBL_SIZE];
准備運行的任務標記,對應位為1表示任務可以運行,否則不能。
OS_EXT BOOLEAN OSRunning;
標志內核是否正在運行。
OS_EXT INT8U OSTaskCtr;
任務被創建的個數。
OS_EXT OS_TCB *OSTCBCur;
指向當前正在運行任務的TCB(任務控制塊)。
OS_EXT OS_TCB *OSTCBFreeList;
空閑TCB塊指針。
OS_EXT OS_TCB *OSTCBHighRdy;
指向最高優先級任務的TCB塊。
OS_EXT OS_TCB *OSTCBList;
正在使用的TCB塊的雙向鏈表。
OS_EXT OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1u];
每個優先級對應的已創建的TCB塊。
OS_EXT OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS];
系統靜態分配的一個系統中所有的TCB塊,包括正在使用的和空閑的。
typedef struct os_tcb { OS_STK *OSTCBStkPtr; //指向任務的棧頂 struct os_tcb *OSTCBNext; //指向TCB鏈表中的下一個TCB塊 struct os_tcb *OSTCBPrev; //指向TCB鏈表中的前一個TCB塊 INT32U OSTCBDly; //任務延遲時間 INT8U OSTCBStat; //任務當前狀態 INT8U OSTCBStatPend; //任務懸掛狀態 INT8U OSTCBPrio; //任務優先級(0表示最高優先級) INT8U OSTCBX; //與任務優先級一致group bit位(一般是OSTCBPrio的低3位) INT8U OSTCBY; //與任務優先級一致的ready table(一般是OSTCBPrio高5位) OS_PRIO OSTCBBitX; //訪問ready table的bit mask OS_PRIO OSTCBBitY; //訪問ready group的bit mask } OS_TCB
TCB塊是任務的描述符,描述了任務的狀態和運行時必須的信息。
2 系統初始化
在OSInit函數中初始化系統的各個數據結構,具體如下:
1、調用函數OS_InitMisc初始化OSIntNesting,OSTaskCtr,OSRunning等一些變量。
2、調用函數OS_InitRdyList初始化准備運行的OSRdyTbl表位都為空。
3、調用函數OS_InitTCBList和OSTCBPrioTbl表的數據都初始化為0,第i個控制塊指向第i+1個控制塊,並且都加入到OSTCBFreeList指向的鏈表中,把OSTCBList初始化為空鏈表。
4、調用函數OS_InitTaskIdle來創建一個Idle任務,該任務是優先級最低的任務,任務號為65535,優先級為63。
3 創建啟動任務
然后我們自己調用OSTaskCreate函數創建我們自己的任務,具體如下:
1、先判斷系統當前是否處於中斷,如果是,則返回,因為當系統處於中斷時是不允許創建任務的。
2、判斷需要創建任務的優先級是否存在其它相同優先級的任務,如果存在,則返回,即相同優先級的任務只能有一個。
3、調用函數OSTaskStkInit初始化任務的棧。
4、調用函數OS_TCBInit初始化任務的TCB塊。這個函數主要是從OSTCBFreeList鏈表獲取空閑的TCB塊並初始化這個控制塊,然后將這個控制塊從OSTCBFreeList鏈表中刪除,並插入到OSTCBList鏈表中。
如果現在內核正在運行,那么調用函數OS_Sched切換任務。
OS_Sched函數主要用來切換任務,如果當前系統不處於中斷例程中,就通過調用函數OS_SchedNew來獲取當前已經准備好的最高優先級的任務(最高優先級主要通過OSRdyGrp和OSRdyTbl來計算得到),然后調用函數OS_TASK_SW來將任務切換到當前優先級最高的任務。
OS_TASK_SW函數則調用os_cpu_a.asm文件中的匯編函數OSCtxSw來觸發PendSV異常來完成任務的切換,至於為什么要這樣做可以參考前面講的重點的PendSV。
PendSV異常的處理的偽代碼如下:
PendSVHandler: if (PSP != NULL) {//判斷程序的堆棧指針是否為空,為空說明這是第一個任務 //當調用PendSVHandler()時, //CPU 就會自動保存xPSR、PC、LR、R12、R0-R3 寄存器到堆棧 //保存后,CUP 的棧SP指針會切換到使用主堆棧指針MSP上 //我們只需檢測進入棧指針PSP是否為NULL就知道是否進行任務切換 //因此當我們第一次啟動任務是,OSStartHighRdy()就把PSP設為NULL, //避免系統以為已經進行任務切換 Save R4-R11 onto task stack; //手動保存 R4-R11 OSTCBCur->OSTCBStkPtr = SP; //保存進入棧指針PSP到任務控制塊 //以便下次繼續任務運行時繼續使用原來的棧 } OSTaskSwHook(); //此處便於我們使用鈎子函數來拓展功能 OSPrioCur = OSPrioHighRdy; //獲取最高優先級就緒任務的優先級 OSTCBCur = OSTCBHighRdy; //獲取最高優先級就緒任務的任務控制塊指針 PSP = OSTCBHighRdy->OSTCBStkPtr; //保存進入棧指針 Restore R4-R11 from new task stack; //從新的棧恢復 R4-R11 寄存器 Return from exception; //返回
PendSV異常處理代碼很簡單,就是保存現場,將當前程序的堆棧指針保存到TCB塊中,並將下一個要運行任務的棧指針更新到PSP中,同時恢復下一個運行任務的現場。
4 運行系統
一切都已經准備就緒,那么我們就調用函數OSStart運行系統。該函數和任務切換函數OS_TASK_SW差不多,只不過它會調用函數OSStartHighRdy將cpu的PSP初始化為0來運行我們系統中的第一個任務並將OSRunning標記為true來表示我們的內核開始運行了。
5 任務切換時機
從OS_Sched函數和OSStartHighRdy函數可以看出,它們總是選擇當前可運行的最高優先級任務來運行,所以只有最高優先級的任務不讓出cpu,低優先級的任務永遠也不可能運行,所以發生任務切換的唯一時機就是高優先級的進行主動讓出cpu。高優先級的任務可以調用函數OSTimeDlyHMSM來讓自己延遲從而讓出cpu,OSTimeDlyHMSM本質上是調用函數OSTimeDly來實現讓出cpu的操作。下面來看看這個函數的具體實現:
void OSTimeDly (INT32U ticks) { if (OSIntNesting > 0u) { //系統正處於中斷中 return; } if (OSLockNesting > 0u) { //調度器加鎖了 return; } if (ticks > 0u) { //延遲時間大於0 OS_ENTER_CRITICAL(); y = OSTCBCur->OSTCBY; //獲取當前優先級的高位 OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX; //清空表中對應當前任務的bit位 if (OSRdyTbl[y] == 0u) { OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY; } OSTCBCur->OSTCBDly = ticks; //設置當前任務的延遲時間 OS_EXIT_CRITICAL(); OS_Sched(); //切換任務 } }
這個函數其實就是將本任務在OSRdyTbl表和OSRdyGrp對應的標記位清0,使得后面的調度函數OS_Sched可以選擇其它優先級的任務,即當前任務讓出cpu。這個函數還做的事就是設置當前任務的延遲時間到任務的TCB塊中,這個時間會在SysTick處理函數中每個節拍的減少,直到減為0時,會將這個任務對應的bit位加入到OSRdyTbl表和OSRdyGrp中來獲取cpu。
6 任務調度算法
UCOSII涉及的調度算法比較的簡單,正如在任務切換時機里說的,只要高優先級的任務不讓出cpu,低優先級的任務永遠不可能得到cpu,調度器總是選擇系統中已經就緒的最高優先級任務運行。問題就來了,調度器怎樣確定到當前系統中任務的最高優先級是多少呢?當然最笨的方法是將OSTCBList鏈表都遍歷一遍,這樣肯定是可以的。但這樣效率是很低的,特別是系統中就緒任務比較多的時候,它的時間復雜度位O(n),n為系統中就緒任務的個數。UCOSII采用了一種比較巧妙的方法實現在O(1)的復雜度獲得這個最高優先級值。
當一個任務就緒時,這個任務的優先級OSTCBPrio被拆分成兩個部分,低4位的值存放在任務控制塊的OSTCBX中,高4位存放在OSTCBY中。然后將2^OSTCBX的值存放在控制塊的OSTCBBitX中,將2^OSTCBY存放在OSTCBBitY中。通過下面方法更新OSRdyTbl表和OSRdyGrp變量。
OSRdyGrp |= ptcb->OSTCBBitY;
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
OSRdyGrp存放了當前系統中所有就緒任務優先級高4位表示的bit位,OSRdyTbl表其實就是一個位圖,用來標記系統中所有就緒任務的優先級,每一個優先級對應一個bit位。
有了這些信息,怎樣來獲取當前就緒任務中最高優先級的值呢?系統維護了一張表:
INT8U const OSUnMapTbl[256] = {
0u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x00 to 0x0F*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x10 to 0x1F*/
5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x20 to 0x2F*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x30 to 0x3F*/
6u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x40 to 0x4F*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x50 to 0x5F*/
5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x60 to 0x6F*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x70 to 0x7F*/
7u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x80 to 0x8F*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x90 to 0x9F*/
5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xA0 to 0xAF*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xB0 to 0xBF*/
6u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xC0 to 0xCF*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xD0 to 0xDF*/
5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xE0 to 0xEF*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u /* 0xF0 to 0xFF*/
};
通過函數OS_SchedNew來獲取最高的優先級值:
static void OS_SchedNew (void) { INT8U y; OS_PRIO *ptbl; if ((OSRdyGrp & 0xFFu) != 0u) { y = OSUnMapTbl[OSRdyGrp & 0xFFu]; } else { y = OSUnMapTbl[(OS_PRIO)(OSRdyGrp >> 8u) & 0xFFu] + 8u; } ptbl = &OSRdyTbl[y]; if ((*ptbl & 0xFFu) != 0u) { OSPrioHighRdy = (INT8U)((y << 4u) + OSUnMapTbl[(*ptbl & 0xFFu)]); } else { OSPrioHighRdy = (INT8U)((y << 4u) + OSUnMapTbl[(OS_PRIO)(*ptbl >> 8u) & 0xFFu] + 8u); } }
這個函數先根據OSRdyGrp值這張表來獲得最高優先級的高4位的值y,然后通過OSRdyTbl[y]來獲得低4位的值x,這樣最終的值就是y<<4 + x。為什么這樣就可以得到最高優先級呢?現在來看一個例子:假設OSRdyGrp = 10,對應的二進制值為1010,那么就緒任務中優先級前4位最小的值為1,由於OSRdyGrp的二進制值第二位為1(這個可以通過前面計算OSRdyGrp的方法來理解)。然后通過這個1,我們可以知道最高優先級在OSRdyTbl對應的bit位在OSRdyTbl[1]中,然后通過OSRdyTbl[1]來查表OSUnMapTbl同樣可以獲得最高優先級的低4位的值。然后合並這兩個值就可以獲得最高優先級的值了。