本章重點講解空閑任務的建立過程
任務建立函數定義如下:
1 INT8U OSTaskCreate (void (*task)(void *p_arg), 2 void *p_arg, 3 OS_STK *ptos, 4 INT8U prio) 5 { 6 OS_STK *psp; 7 INT8U err; 8 #if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */ 9 OS_CPU_SR cpu_sr = 0u; 10 #endif 11 12 13 14 #ifdef OS_SAFETY_CRITICAL_IEC61508 15 if (OSSafetyCriticalStartFlag == OS_TRUE) { 16 OS_SAFETY_CRITICAL_EXCEPTION(); 17 } 18 #endif 19 20 #if OS_ARG_CHK_EN > 0u 21 if (prio > OS_LOWEST_PRIO) { /* Make sure priority is within allowable range */ 22 return (OS_ERR_PRIO_INVALID); 23 } 24 #endif 25 OS_ENTER_CRITICAL(); 26 if (OSIntNesting > 0u) { /* Make sure we don't create the task from within an ISR */ 27 OS_EXIT_CRITICAL(); 28 return (OS_ERR_TASK_CREATE_ISR); 29 } 30 if (OSTCBPrioTbl[prio] == (OS_TCB *)0) { /* Make sure task doesn't already exist at this priority */ 31 OSTCBPrioTbl[prio] = OS_TCB_RESERVED;/* Reserve the priority to prevent others from doing ... */ 32 /* ... the same thing until task is created. */ 33 OS_EXIT_CRITICAL(); 34 psp = OSTaskStkInit(task, p_arg, ptos, 0u); /* Initialize the task's stack */ 35 err = OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u); 36 if (err == OS_ERR_NONE) { 37 if (OSRunning == OS_TRUE) { /* Find highest priority task if multitasking has started */ 38 OS_Sched(); 39 } 40 } else { 41 OS_ENTER_CRITICAL(); 42 OSTCBPrioTbl[prio] = (OS_TCB *)0;/* Make this priority available to others */ 43 OS_EXIT_CRITICAL(); 44 } 45 return (err); 46 } 47 OS_EXIT_CRITICAL(); 48 return (OS_ERR_PRIO_EXIST); 49 }
21~23行,判斷我們傳遞進來的參數優先級是否合法,如果不滿足,直接退出(當前系統支持最大64個任務,因此優先級必須小於64)。
26~29行,判斷當前系統的中斷狀態,變量OSIntNesting的意義之前講過,如果它大於0,那就代表目前處於中斷服務程序中,在中斷中系統是不允許建立新任務的。
30行,首先判斷任務是否已經存在,如果已經存在則跳出,數組OSTCBPrioTbl[prio]的下標是優先級,內容是任務的相關的信息,如果該優先級的任務已建立,那么其內容必然不等於0.
31行,當任務不存在時,隨便賦個值給這個任務的管理數組,相當於是在這個元素上做個標志,告訴系統,這個位置要保留下來不允許別人動,直到這個任務真正建立成功。
34行所調用的函數是初始化任務的堆棧,我們現在詳細看看內部的處理,到底是如何進行初始化的。
棧空間
函數OSTaskStkInit定義如下:
1 OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt) 2 { 3 OS_STK *stk; 4 5 6 (void)opt; /* 'opt' is not used, prevent warning */ 7 stk = ptos; /* Load stack pointer */ 8 9 /* Registers stacked as if auto-saved on exception */ 10 *(stk) = (INT32U)0x01000000L; /* xPSR */ 11 *(--stk) = (INT32U)task; /* Entry Point */ 12 *(--stk) = (INT32U)0xFFFFFFFEL; /* R14 (LR) (init value will cause fault if ever used)*/ 13 *(--stk) = (INT32U)0x12121212L; /* R12 */ 14 *(--stk) = (INT32U)0x03030303L; /* R3 */ 15 *(--stk) = (INT32U)0x02020202L; /* R2 */ 16 *(--stk) = (INT32U)0x01010101L; /* R1 */ 17 *(--stk) = (INT32U)p_arg; /* R0 : argument */ 18 19 /* Remaining registers saved on process stack */ 20 *(--stk) = (INT32U)0x11111111L; /* R11 */ 21 *(--stk) = (INT32U)0x10101010L; /* R10 */ 22 *(--stk) = (INT32U)0x09090909L; /* R9 */ 23 *(--stk) = (INT32U)0x08080808L; /* R8 */ 24 *(--stk) = (INT32U)0x07070707L; /* R7 */ 25 *(--stk) = (INT32U)0x06060606L; /* R6 */ 26 *(--stk) = (INT32U)0x05050505L; /* R5 */ 27 *(--stk) = (INT32U)0x04040404L; /* R4 */ 28 29 return (stk); 30 }
這個函數該怎么解釋呢?
它其實並不是真正的去初始化了CPU的棧空間,只是對一個模擬的棧進行了初始化。
做過單片機程序的同學都很清楚,在程運行中總遇到中斷發生,當這個時候就必須要跳進中斷中去運行,但在跳進中斷服務函數之前,我們必須要做一些處理,比如保存現場,將當前的數據和寄存器值押入棧中,然后在去執行中斷函數,因為只有這樣做了,在執行完中斷函數以后才能找回原點繼續執行剩下的代碼(匯編語言需要自己壓棧和出棧,C語言由系統自動完成)。
單片機裸機程序是單線程,代碼的執行邏輯始終處於一條時間線,就算是發生了中斷嵌套,但在某一個具體的時間點,它也是單線的,因此只需要保存一個現場就可以回到原點。
但在嵌入式操作系統中同時執行了多個任務,每個任務之間都有可能發生切換,任務與任務之間不存在調用的關系,是相對獨立的存在,任務的這個切換可以理解為單片機中發生的中斷,而且切換的順序並不是線性的,一旦在任務過多的情況下,如果依然使用CPU自己的棧空間,那這樣就會導致棧空間不夠用,棧溢出,而且而且非常不方便。
所以,UCOSII系統對於每一個任務都創建了一個數組來模擬它的棧空間,比如這個空閑任務,我們跟蹤它的參數可以知道:
OS_EXT OS_STK OSTaskIdleStk[OS_TASK_IDLE_STK_SIZE]; /* Idle task stack */
#define OS_TASK_IDLE_STK_SIZE 128u /* Idle task stack size (# of OS_STK wide entries) */
系統定義好了一個128長度的數組來模擬空閑任務的棧空間,專門用來存在和這個任務有關的寄存器數據,當我們從空閑任務跳出去的時候,就會把相應的信息保存在里面,等需要跳回的時候,就會從里面取出相應的信息,功能和CPU的棧空間完全一樣。
我們可以用keil仿真一下。
首先在下面進入棧初始化之前打上斷點:
這個時候我們可以看看空閑任務的棧數組,也就是OSTaskIdleStk[],他此刻的狀態如下:
這個數組的長度是128個字節,我不可能全部截完,不過它其中的內容都如上所示,全部是0x0000。
現在我們在函數的出口后打上斷點,然后全速運行,觀察一下空閑任務的棧初始化完成以后是什么樣子的,代碼如下:
棧空間初始化完后結果如下:
我們能看出來有些空間被填充了,由於M3的棧空間是由高往低增長,因此數組的最后幾個字節被填充上的內容,其中第126個元素保存的是空閑任務的任務函數地址,第120個元素保存的傳遞進來的參數,剩下的那些立即數是模擬的CPU的寄存器,至於那些立即數的內容不用關心,基本是用於開發人員調試用的。
在我們以前進行單片機C語言的編程中,在一個函數被調用之前,它的堆棧信息的初始化,壓棧,出棧都是mcu自行進行的,不需要工程師干預,而在UCOSII系統中,由於MCU的棧空間只有一個,而我們的任務又太多,因此便需要由人工來進行管理,實際上OSTaskStkInit函數便是模擬了這種動作。
任務信息
空閑任務的棧空間初始化完成以后,接下來便需要對任務本身的一些信息進行處理,請接着看下一個函數:OS_TCBInit()
err = OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u);
其簡化后,定義如下:
1 INT8U OS_TCBInit (INT8U prio, 2 OS_STK *ptos, 3 OS_STK *pbos, 4 INT16U id, 5 INT32U stk_size, 6 void *pext, 7 INT16U opt) 8 { 9 OS_TCB *ptcb; 10 #if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */ 11 OS_CPU_SR cpu_sr = 0u; 12 #endif 13 #if OS_TASK_REG_TBL_SIZE > 0u 14 INT8U i; 15 #endif 16 17 18 OS_ENTER_CRITICAL(); 19 ptcb = OSTCBFreeList; /* Get a free TCB from the free TCB list */ 20 if (ptcb != (OS_TCB *)0) { 21 OSTCBFreeList = ptcb->OSTCBNext; /* Update pointer to free TCB list */ 22 OS_EXIT_CRITICAL(); 23 ptcb->OSTCBStkPtr = ptos; /* Load Stack pointer in TCB */ 24 ptcb->OSTCBPrio = prio; /* Load task priority into TCB */ 25 ptcb->OSTCBStat = OS_STAT_RDY; /* Task is ready to run */ 26 ptcb->OSTCBStatPend = OS_STAT_PEND_OK; /* Clear pend status */ 27 ptcb->OSTCBDly = 0u; /* Task is not delayed */ 28 30 pext = pext; /* Prevent compiler warning if not used */ 31 stk_size = stk_size; 32 pbos = pbos; 33 opt = opt; 34 id = id; 35 36 #if OS_TASK_DEL_EN > 0u 37 ptcb->OSTCBDelReq = OS_ERR_NONE; 38 #endif 39 40 ptcb->OSTCBY = (INT8U)(prio >> 3u); 41 ptcb->OSTCBX = (INT8U)(prio & 0x07u); 42 43 ptcb->OSTCBBitY = (OS_PRIO)(1uL << ptcb->OSTCBY); 44 ptcb->OSTCBBitX = (OS_PRIO)(1uL << ptcb->OSTCBX); 45 46 #if OS_TASK_REG_TBL_SIZE > 0u /* Initialize the task variables */ 47 for (i = 0u; i < OS_TASK_REG_TBL_SIZE; i++) { 48 ptcb->OSTCBRegTbl[i] = 0u; 49 } 50 #endif 51 52 OSTCBInitHook(ptcb); 53 54 OSTaskCreateHook(ptcb); /* Call user defined hook */ 55 56 OS_ENTER_CRITICAL(); 57 OSTCBPrioTbl[prio] = ptcb; 58 ptcb->OSTCBNext = OSTCBList; /* Link into TCB chain */ 59 ptcb->OSTCBPrev = (OS_TCB *)0; 60 if (OSTCBList != (OS_TCB *)0) { 61 OSTCBList->OSTCBPrev = ptcb; 62 } 63 OSTCBList = ptcb; 64 OSRdyGrp |= ptcb->OSTCBBitY; /* Make task ready to run */ 65 OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; 66 OSTaskCtr++; /* Increment the #tasks counter */ 67 OS_EXIT_CRITICAL(); 68 return (OS_ERR_NONE); 69 } 70 OS_EXIT_CRITICAL(); 71 return (OS_ERR_TASK_NO_MORE_TCB); 72 }
第19行,ptcb只是一個局部指針,暫時還沒有指向任何地址,因此首先需要從剛才建立的空閑鏈表中取出一個實際地址。
第20行,判斷該地址是否有效(不等於0)。
第21行,由於我們從空閑鏈表取出了一塊地址,那么這個空閑鏈表的首地址就不能用了,所以需要把它的首地址指向下一個位置,以便今后其他任務的建立。
第23~27行,這幾句話是設定任務的參數,比如棧地址,優先級,任務狀態,延時參數等等。
第40~44行,這幾句話和優先級策略以及今后的任務切換有關,詳細可參考:http://www.cnblogs.com/han-bing/p/8882375.html
第52~53行,這是鈎子函數的調用,無需多言。
第57行,這是將已經初始化完成的任務鏈表TCB賦值給全局的任務數組,這個數組一共有64個元素,其中是按照優先級從高到低來排序。
第58~59行,更新任務鏈表的下一個元素和上一個元素,也就是把新建的這個任務塊,鏈接到總的任務控制鏈接里面。
第60~62行,判斷當前任務鏈表的狀態(由於空閑任務是第一個創建的任務,因此任務控制鏈表還是空的,上一個鏈接自然也不存在,所以並不會執行if判斷里面代碼,ptcb->OSTCBPrev被賦值為0)。
第63行,把我們新建的這個局部任務塊,鏈接到總任務鏈表里面,從這時開始,OSTCBList就不在是空的了,里面有了第一個成員。
第64~65行,這兩句話是任務優先級的管理表更新,詳細可參考:http://www.cnblogs.com/han-bing/p/8882375.html
第66行,任務個數管理變量加1,代表系統建立了多少個任務。
----------------------------------------------------------------------------------------------------------------------
任務建立的過程總體而言不算簡單也不算復雜,如果你已經掌握了鏈表等數據結構的知識,那么對於這段應該很容易掌握,不過若是以前對鏈表之類的完全不了解,那就需要多花費一點時間了。
對於新手而言,上面出現的各式各樣的鏈表和數組可能看起來似乎有些眼花繚亂,覺得UCOSII系統太耗費資源,那么大的結構體作成一個64個元素的數組,而且還有好幾個……其實,鏈表和數組不同,不管所管理的空間有多大,鏈表並不會占據實際的數據結構內存(當然,指針本身還是要占一點內存的,只不過僅僅是一個指針的地址,並不會占據數據結構那么大的內存),別看我們上面提到了那么多種鏈表,但不管是任務管理鏈表(OSTCBList),空閑鏈表(OSTCBFreeList)其實他們管理的都是同一段內存,也就是數組OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS]。
就像一條街道擁有的不同叫法,老年人叫“愛國路”,年輕人叫“歡樂路”,小朋友叫“玩耍路”……他們所知的都是同一條街道,只是名字不同罷了。
如果不明白,請看看代碼中的定義:
1 OS_EXT OS_TCB *OSTCBCur; /* Pointer to currently running TCB */ 2 OS_EXT OS_TCB *OSTCBFreeList; /* Pointer to list of free TCBs */ 3 OS_EXT OS_TCB *OSTCBHighRdy; /* Pointer to highest priority TCB R-to-R */ 4 OS_EXT OS_TCB *OSTCBList; /* Pointer to doubly linked list of TCBs */ 5 OS_EXT OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1u]; /* Table of pointers to created TCBs */ 6 OS_EXT OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS]; /* Table of TCBs */
前面5個數據定義的類型都是指向別的數據的指針,只有最后一個數組定義的才是占據實際內存的數據,UCOSII系統中所謂的任務管理,其實也就是把這幾個鏈表指向的地址根據需要反復的修改,但是永遠限定在OSTCBTbl數組中,就像孫猴子無論怎么飛永遠也逃不出如來佛的手掌心,這樣從而達到管理任務的目的。
當空閑任務未建立之前,我們看看具體的數據結構:
首先是那個數組:OSTCBTbl
這是它的內存分部狀態,它是實際占據內存的數據。
在系統剛剛初始化開始,還沒有新建任何任務的時候,我們用這個數組和空閑鏈表做一個比較。
數組:OSTCBTbl 空閑鏈表:OSTCBFreeList
由以上兩張地址信息對比,可以看出,空閑鏈表中指向下一個結構的地址,其實就是數組元素的地址,因此它管理的是數組元素。
這個時候(沒有任何任務建立,包括空閑任務),別的鏈表都是空:
當空閑任務建立之后,我們再看看具體的數據結構:
首先是那個數組:OSTCBTbl
這個數組的內容當然是不會變的,不管任務是否建立,不管任務是否存在,它元素的地址永遠都不會變。
在系統建完成空閑任務后,我們用這個數組和空閑鏈表做一個比較。
數組:OSTCBTbl 空閑鏈表:OSTCBFreeList
兩張圖一對比便不難發現,空閑鏈表的數據內容發生了變化,具體便是,它所有的地址都往前縮進了一位,數組OSTCBTbl的首地址:0x20000A48,已經無法在空閑鏈表中找到了,空閑鏈表的首地址變成了,數組的第二個元素的地址。
此時此刻空閑任務已經建立完成,因此,空閑鏈表最開始的那個地址已經不空閑了,名花有主了,所以它的首地址只能指向像一個還沒有被分配出去的地址,如果我們再建立一個任務,那么空閑鏈表的首地址還會繼續向上縮進一個,因為它所指向的,必須是這個數組中還沒有被分配出去的那段空間。
我們再繼續看看別的鏈表。
比如說任務管理鏈表的狀態:
數組:OSTCBTbl 任務管理鏈表:OSTCBList
因為我們已經建立了一個任務,因此任務管理鏈表的首地址,自然就指向了那個僅存的空閑任務,相當於從空閑鏈表中拿出去,放進了任務管理鏈表中,每建立一個任務都是如此,不停的從空閑鏈表中拿出去,然后放進任務管理鏈表,任務的刪除便是反向操作,從任務管理鏈表中拿出去,放進空閑鏈表。
至於別的兩個鏈表,此刻依然是空,因為雖然建立了一個任務,但這個任務還沒跑起來,等它跑起來后這兩個鏈表里肯定就不是空了。
優先級鏈表:OSTCBPrioTbl
由於空閑任務的建立,優先級管理鏈表中也有數據了,由於空閑任務的優先級是63,所以按照它的排序規則,放進最后一個元素中,可以看到它的地址正是那個空閑任務的地址,這也就意味着,他們管理的是同一段數據。
小結:在UCOSII中,系統所占據的內存其實並不大,雖然里面的各種鏈表比較多,但都管理的同一段內存,只是根據需要和意義賦予了不同的名字。
鐵打的數組,流水的鏈表,數組就像一棵巍然不動的千年老松,任憑風吹雨打,那些鏈表就像一堆迎風就倒的牆頭小草,它們所管理的范圍是變動的。
任務管理其實也很簡單,它的本質,就是對任務數組的操作,增加、刪除、排序……等等。
任務數組:OSTCBTbl,它里面是按照任務建立的時間來存放任務。
優先級數組:OSTCBPrioTbl,它里面是按照任務的優先級,對任務數組進行了一次排序。
因此,空閑任務的建立過程,首先是把空閑任務的信息放進數組OSTCBTbl的第1個位置(建立時間最早),再把空閑任務的信息放進數組OSTCBPrioTbl的最后一個位置(優先級最低)。
待續……