1. 程式人生 > >經典:uC/OS-II系統的學習教程之(三)

經典:uC/OS-II系統的學習教程之(三)

核心結構
 本章給出μC/OS-Ⅱ的主要結構概貌。讀者將學習以下一些內容;
μC/OS-Ⅱ是怎樣處理臨界段程式碼的;
什麼是任務,怎樣把使用者的任務交給μC/OS-Ⅱ;
任務是怎樣排程的;
應用程式CPU的利用率是多少,μC/OS-Ⅱ是怎樣知道的;
怎樣寫中斷服務子程式;
什麼是時鐘節拍,μC/OS-Ⅱ是怎樣處理時鐘節拍的;
μC/OS-Ⅱ是怎樣初始化的,以及
怎樣啟動多工;
本章還描述以下函式,這些服務於應用程式:
OS_ENTER_CRITICAL() 和 OS_EXIT_CRITICAL(),
OSInit(),
OSStart(),
OSIntEnter() 和 OSIntExit(),
OSSchedLock() 和 OSSchedUnlock(), 以及
OSVersion().

臨界段(Critical Sections)
  和其它核心一樣,μC/OS-Ⅱ為了處理臨界段程式碼需要關中斷,處理完畢後再開中斷。這使得μC/OS-Ⅱ能夠避免同時有其它任務或中斷服務進入臨界段代 碼。關中斷的時間是實時核心開發商應提供的最重要的指標之一,因為這個指標影響使用者系統對實時事件的響應性。μC/OS-Ⅱ努力使關中斷時間降至最短,但 就使用μC/OS-Ⅱ而言,關中斷的時間很大程度上取決於微處理器的架構以及編譯器所生成的程式碼質量。
 微處理器一般都有關中斷/開中斷指令,用 戶使用的C語言編譯器必須有某種機制能夠在C中直接實現關中斷/開中斷地操作。某些C編譯器允許在使用者的C原始碼中插入組合語言的語句。這使得插入微處理 器指令來關中斷/開中斷很容易實現。而有的編譯器把從C語言中關中斷/開中斷放在語言的擴充套件部分。μC/OS-Ⅱ定義兩個巨集(macros)來關中斷和開 中斷,以便避開不同C編譯器廠商選擇不同的方法來處理關中斷和開中斷。μC/OS-Ⅱ中的這兩個巨集呼叫分別是:OS_ENTER_CRITICAL()和 OS_EXIT_CRITICAL()。因為這兩個巨集的定義取決於所用的微處理器,故在檔案OS_CPU.H中可以找到相應巨集定義。每種微處理器都有自己 的OS_CPU.H檔案。
任務
 一個任務通常是一個無限的迴圈[L3.1(2)],如程式清單3.1所示。一個任務看起來像其它C的函式一樣,有函式返回型別,有形式引數變數,但是任務是絕不會返回的。故返回引數必須定義成void[L3.1(1)]。


 程式清單 L3.1 任務是一個無限迴圈 
    void YourTask (void *pdata)                      (1) 
    { 
       for (;;) {                                    (2) 
          /* 使用者程式碼 */ 
          呼叫uC/OS-II的某種系統服務: 
          OSMboxPend(); 
          OSQPend(); 
          OSSemPend(); 
          OSTaskDel(OS_PRIO_SELF); 
          OSTaskSuspend(OS_PRIO_SELF); 
          OSTimeDly(); 
          OSTimeDlyHMSM(); 
          /* 使用者程式碼 */ 
       } 
    } 

 不同的是,當任務完成以後,任務可以自我刪除,如清單L3.2所示。注意任務程式碼並非真的刪除了,μC/OS-Ⅱ只是簡單地不再理會這個任務了,這個任務的程式碼也不會再執行,如果任務呼叫了OSTaskDel(),這個任務絕不會返回什麼。


 程式清單 L 3.2 . 任務完成後自我刪除 
    void YourTask (void *pdata) 
    { 
       /* 使用者程式碼 */ 
       OSTaskDel(OS_PRIO_SELF); 
    } 

  形式引數變數[L3.1(1)]是由使用者程式碼在第一次執行的時候帶入的。請注意,該變數的型別是一個指向void的指標。這是為了允許使用者應用程式傳遞任 何型別的資料給任務。這個指標好比一輛萬能的車子,如果需要的話,可以運載一個變數的地址,或一個結構,甚至是一個函式的地址。也可以建立許多相同的任 務,所有任務都使用同一個函式(或者說是同一個任務程式碼程式), 見第一章的例1。例如,使用者可以將四個序列口安排成每個序列口都是一個單獨的任務,而每個任務的程式碼實際上是相同的。並不需要將程式碼複製四次,使用者可以建 立一個任務,向這個任務傳入一個指向某資料結構的指標變數,這個資料結構定義序列口的引數(波特率、I/O口地址、中斷向量號等)。
 μC/OS- Ⅱ可以管理多達64個任務,但目前版本的μC/OS-Ⅱ有兩個任務已經被系統佔用了。作者保留了優先順序為0、1、2、3、OS_LOWEST_PRIO- 3、OS_LOWEST_PRI0-2,OS_LOWEST_PRI0-1以及OS_LOWEST_PRI0這8個任務以被將來使用。 OS_LOWEST_PRI0是作為定義的常數在OS_CFG.H檔案中用定義常數語句#define constant定義的。因此使用者可以有多達56個應用任務。必須給每個任務賦以不同的優先順序,優先順序可以從0到OS_LOWEST_PR10-2。優先 級號越低,任務的優先順序越高。μC/OS-Ⅱ總是執行進入就緒態的優先順序最高的任務。目前版本的μC/OS-Ⅱ中,任務的優先順序號就是任務編號(ID)。 優先順序號(或任務的ID號)也被一些核心服務函式呼叫,如改變優先順序函式OSTaskChangePrio(),以及任務刪除函式OSTaskDel ()。
 為了使μC/OS-Ⅱ能管理使用者任務,使用者必須在建立一個任務的時候,將任務的起始地址與其它引數一起傳給下面兩個函式中的一個: OSTastCreat或OSTaskCreatExt()。OSTaskCreateExt()是OSTaskCreate()的擴充套件,擴充套件了一些附加 的功能。,這兩個函式的解釋見第四章,任務管理。
任務狀態
 圖3.1是μC/OS-Ⅱ控制下的任務狀態轉換圖。在任一給定的時刻,任務的狀態一定是在這五種狀態之一。
  睡眠態(DORMANT)指任務駐留在程式空間之中,還沒有交給μC/OS-Ⅱ管理,(見程式清單L3.1或L3.2)。把任務交給μC/OS-Ⅱ是通過 呼叫下述兩個函式之一:OSTaskCreate()或OSTaskCreateExt()。當任務一旦建立,這個任務就進入就緒態準備執行。任務的建立 可以是在多工執行開始之前,也可以是動態地被一個執行著的任務建立。如果一個任務是被另一個任務建立的,而這個任務的優先順序高於建立它的那個任務,則這 個剛剛建立的任務將立即得到CPU的控制權。一個任務可以通過呼叫OSTaskDel()返回到睡眠態,或通過呼叫該函式讓另一個任務進入睡眠態。
 呼叫OSStart()可以啟動多工。OSStart()函式執行進入就緒態的優先順序最高的任務。就緒的任務只有當所有優先順序高於這個任務的任務轉為等待狀態,或者是被刪除了,才能進入執行態。

 圖3.1 任務的狀態
 
  正在執行的任務可以通過呼叫兩個函式之一將自身延遲一段時間,這兩個函式是OSTimeDly()或OSTimeDlyHMSM()。這個任務於是進入等 待狀態,等待這段時間過去,下一個優先順序最高的、並進入了就緒態的任務立刻被賦予了CPU的控制權。等待的時間過去以後,系統服務函式 OSTimeTick()使延遲了的任務進入就緒態(見3.10節,時鐘節拍)。
 正在執行的任務期待某一事件的發生時也要等待,手段是呼叫以下 3個函式之一:OSSemPend(),OSMboxPend(),或OSQPend()。呼叫後任務進入了等待狀態(WAITING)。當任務因等待事 件被掛起(Pend),下一個優先順序最高的任務立即得到了CPU的控制權。當事件發生了,被掛起的任務進入就緒態。事件發生的報告可能來自另一個任務,也 可能來自中斷服務子程式。
 正在執行的任務是可以被中斷的,除非該任務將中斷關了,或者μC/OS-Ⅱ將中斷關了。被中斷了的任務就進入了中斷服 務態(ISR)。響應中斷時,正在執行的任務被掛起,中斷服務子程式控制了CPU的使用權。中斷服務子程式可能會報告一個或多個事件的發生,而使一個或多 個任務進入就緒態。在這種情況下,從中斷服務子程式返回之前,μC/OS-Ⅱ要判定,被中斷的任務是否還是就緒態任務中優先順序最高的。如果中斷服務子程式 使一個優先順序更高的任務進入了就緒態,則新進入就緒態的這個優先順序更高的任務將得以執行,否則原來被中斷了的任務才能繼續執行。
 當所有的任務都在等待事件發生或等待延遲時間結束,μC/OS-Ⅱ執行空閒任務(idle task),執行OSTaskIdle()函式。
任務控制塊(Task Control Blocks, OS_TCBs)
  一旦任務建立了,任務控制塊OS_TCBs將被賦值(程式清單3.3)。任務控制塊是一個數據結構,當任務的CPU使用權被剝奪時,μC/OS-Ⅱ用 它來儲存該任務的狀態。當任務重新得到CPU使用權時,任務控制塊能確保任務從當時被中斷的那一點絲毫不差地繼續執行。OS_TCBs全部駐留在 RAM中。讀者將會注意到筆者在組織這個資料結構時,考慮到了各成員的邏輯分組。任務建立的時候,OS_TCBs就被初始化了(見第四章 任務管理)。

 程式清單 L 3.3  µC/OS-II任務控制塊. 
typedef struct os_tcb { 
    OS_STK        *OSTCBStkPtr; 
 
#if OS_TASK_CREATE_EXT_EN 
    void          *OSTCBExtPtr; 
    OS_STK        *OSTCBStkBottom; 
    INT32U         OSTCBStkSize; 
    INT16U         OSTCBOpt; 
    INT16U         OSTCBId; 
#endif 
 
    struct os_tcb *OSTCBNext; 
    struct os_tcb *OSTCBPrev; 
 
#if (OS_Q_EN && (OS_MAX_QS >= 2)) || OS_MBOX_EN || OS_SEM_EN 
    OS_EVENT      *OSTCBEventPtr; 
#endif 
 
#if (OS_Q_EN && (OS_MAX_QS >= 2)) || OS_MBOX_EN 
    void          *OSTCBMsg; 
#endif 
 
    INT16U         OSTCBDly; 
    INT8U          OSTCBStat; 
    INT8U          OSTCBPrio; 
 
    INT8U          OSTCBX; 
    INT8U          OSTCBY; 
    INT8U          OSTCBBitX; 
    INT8U          OSTCBBitY; 
 
#if OS_TASK_DEL_EN 
    BOOLEAN        OSTCBDelReq; 
#endif 
} OS_TCB; 

.OSTCBStkPtr 是指向當前任務棧頂的指標。μC/OS-Ⅱ允許每個任務有自己的棧,尤為重要的是,每個任務的棧的容量可以是任意的。有些商業核心要求所有任務棧的容量都 一樣,除非使用者寫一個複雜的介面函式來改變之。這種限制浪費了RAM,當各任務需要的棧空間不同時,也得按任務中預期棧容量需求最多的來分配棧空間。 OSTCBStkPtr是OS_TCB資料結構中唯一的一個能用匯編語言來處置的變數(在任務切換段的程式碼Context-switching code之中,)把OSTCBStkPtr放在資料結構的最前面,使得從組合語言中處理這個變數時較為容易。
.OSTCBExtPtr 指向使用者定義的任務控制塊擴充套件。使用者可以擴充套件任務控制塊而不必修改μC/OS-Ⅱ的原始碼。.OSTCBExtPtr只在函式 OstaskCreateExt()中使用,故使用時要將OS_TASK_CREAT_EN設為1,以允許建立任務函式的擴充套件。例如使用者可以建立一個數據 結構,這個資料結構包含每個任務的名字,或跟蹤某個任務的執行時間,或者跟蹤切換到某個任務的次數(見例3)。注意,筆者將這個擴充套件指標變數放在緊跟著堆 棧指標的位置,為的是當用戶需要在組合語言中處理這個變數時,從資料結構的頭上算偏移量比較方便。
.OSTCBStkBottom是指向任務棧底 的指標。如果微處理器的棧指標是遞減的,即棧儲存器從高地址向低地址方向分配,則OSTCBStkBottom指向任務使用的棧空間的最低地址。類似地, 如果微處理器的棧是從低地址向高地址遞增型的,則OSTCBStkBottom指向任務可以使用的棧空間的最高地址。函式OSTaskStkChk()要 用到變數OSTCBStkBottom,在執行中檢驗棧空間的使用情況。使用者可以用它來確定任務實際需要的棧空間。這個功能只有當用戶在任務建立時允許使 用OSTaskCreateExt()函式時才能實現。這就要求使用者將OS_TASK_CREATE_EXT_EN設為1,以便允許該功能。
.OSTCBStkSize 存有棧中可容納的指標元數目而不是用位元組(Byte)表示的棧容量總數。也就是說,如果棧中可以儲存1,000個入口地址,每個地址寬度是32位的,則實 際棧容量是4,000位元組。同樣是1,000個入口地址,如果每個地址寬度是16位的,則總棧容量只有2,000位元組。在函式OSStakChk()中要 呼叫OSTCBStkSize。同理,若使用該函式的話,要將OS_TASK_CREAT_EXT_EN設為1。
.OSTCBOpt把“選擇項” 傳給OSTaskCreateExt(),只有在使用者將OS_TASK_CREATE_EXT_EN設為1時,這個變數才有效。μC/OS-Ⅱ目前只支援 3個選擇項(見uCOS_II.H):OS_TASK_OTP_STK_CHK, OS_TASK_OPT_STK_CLR和OS_TASK_OPT_SAVE_FP。 OS_TASK_OTP_STK_CHK  用於告知TaskCreateExt(),在任務建立的時候任務棧檢驗功能得到了允許。OS_TASK_OPT_STK_CLR表示任務建立的時候任務棧 要清零。只有在使用者需要有棧檢驗功能時,才需要將棧清零。如果不定義OS_TASK_OPT_STK_CLR,而後又建立、刪除了任務,棧檢驗功能報告的 棧使用情況將是錯誤的。如果任務一旦建立就決不會被刪除,而使用者初始化時,已將RAM清過零,則OS_TASK_OPT_STK_CLR不需要再定義,這 可以節約程式執行時間。傳遞了OS_TASK_OPT_STK_CLR將增加TaskCreateExt()函式的執行時間,因為要將棧空間清零。棧容量 越大,清零花的時間越長。最後一個選擇項OS_TASK_OPT_SAVE_FP通知TaskCreateExt(),任務要做浮點運算。如果微處理器有 硬體的浮點協處理器,則所建立的任務在做任務排程切換時,浮點暫存器的內容要儲存。
.OSTCBId用於儲存任務的識別碼。這個變數現在沒有使用,留給將來擴充套件用。
.OSTCBNext 和.OSTCBPrev用於任務控制塊OS_TCBs的雙重連結,該連結串列在時鐘節拍函式OSTimeTick()中使用,用於重新整理各個任務的任務延遲變 量.OSTCBDly,每個任務的任務控制塊OS_TCB在任務建立的時候被連結到連結串列中,在任務刪除的時候從連結串列中被刪除。雙重連線的連結串列使得任一成員 都能被快速插入或刪除。
.OSTCBEventPtr是指向事件控制塊的指標,後面的章節中會有所描述(見第6章 任務間通訊與同步)。
.OSTCBMsg是指向傳給任務的訊息的指標。用法將在後面的章節中提到(見第6章任務間通訊與同步)。
.OSTCBDly 當需要把任務延時若干時鐘節拍時要用到這個變數,或者需要把任務掛起一段時間以等待某事件的發生,這種等待是有超時限制的。在這種情況下,這個變數儲存的 是任務允許等待事件發生的最多時鐘節拍數。如果這個變數為0,表示任務不延時,或者表示等待事件發生的時間沒有限制。
.OSTCBStat是任務的狀態字。當.OSTCBStat為0,任務進入就緒態。可以給.OSTCBStat賦其它的值,在檔案uCOS_II.H中有關於這個值的描述。
.OSTCBPrio是任務優先順序。高優先順序任務的.OSTCBPrio值小。也就是說,這個值越小,任務的優先順序越高。
.OSTCBX, .OSTCBY, .OSTCBBitX和 .OSTCBBitY用於加速任務進入就緒態的過程或進入等待事件發生狀態的過程(避免在執行中去計算這些值)。這些值是在任務建立時算好的,或者是在改 變任務優先順序時算出的。這些值的演算法見程式清單L3.4。

 程式清單 L 3.4 任務控制塊OS_TCB中幾個成員的演算法 
OSTCBY = priority >> 3; 
OSTCBBitY = OSMapTbl[priority >> 3]; 
OSTCBX = priority & 0x07; 
OSTCBBitX = OSMapTbl[priority & 0x07]; 


.OSTCBDelReq是一個布林量,用於表示該任務是否需要刪除,用法將在後面的章節中描述(見第4章 任務管理)

  應用程式中可以有的最多工數(OS_MAX_TASKS)是在檔案OS_CFG.H中定義的。這個最多工數也是μC/OS-Ⅱ分配給使用者程式的最多任 務控制塊OS_TCBs的數目。將OS_MAX_TASKS的數目設定為使用者應用程式實際需要的任務數可以減小RAM的需求量。所有的任務控制塊 OS_TCBs都是放在任務控制塊列表陣列OSTCBTbl[]中的。請注意,μC/OS-Ⅱ分配給系統任務OS_N_SYS_TASKS若干個任務控制 塊,見檔案μC/OS-Ⅱ.H,供其內部使用。目前,一個用於空閒任務,另一個用於任務統計(如果OS_TASK_STAT_EN是設為1的)。在 μC/OS-Ⅱ初始化的時候,如圖3.2所示,所有任務控制塊OS_TCBs被連結成單向空任務連結串列。當任務一旦建立,空任務控制塊指標 OSTCBFreeList指向的任務控制塊便賦給了該任務,然後OSTCBFreeList的值調整為指向下連結串列中下一個空的任務控制塊。一旦任務被刪 除,任務控制塊就還給空任務連結串列。

              圖3.2 空任務列表
 就緒表(Ready List)
  每個任務被賦予不同的優先順序等級,從0級到最低優先順序OS_LOWEST_PR1O,包括0和OS_LOWEST_PR1O在內(見檔案 OS_CFG.H)。當μC/OS-Ⅱ初始化的時候,最低優先順序OS_LOWEST_PR1O總是被賦給空閒任務idle task。注意,最多工數目OS_MAX_TASKS和最低優先順序數是沒有關係的。使用者應用程式可以只有10個任務,而仍然可以有32個優先順序的級別 (如果使用者將最低優先順序數設為31的話)。
 每個任務的就緒態標誌都放入就緒表中的,就緒表中有兩個變數OSRedyGrp和OSRdyTbl []。在OSRdyGrp中,任務按優先順序分組,8個任務為一組。OSRdyGrp中的每一位表示8組任務中每一組中是否有進入就緒態的任務。任務進入就 緒態時,就緒表OSRdyTbl[]中的相應元素的相應位也置位。就緒表OSRdyTbl[]陣列的大小取決於OS_LOWEST_PR1O(見檔案 OS_CFG.H)。當用戶的應用程式中任務數目比較少時,減少OS_LOWEST_PR1O的值可以降低μC/OS-Ⅱ對RAM(資料空間)的需求量。
 為確定下次該哪個優先順序的任務運行了,核心排程器總是將OS_LOWEST_PR1O在就緒表中相應位元組的相應位置1。OSRdyGrp和OSRdyTbl[]之間的關係見圖3.3,是按以下規則給出的:
 當OSRdyTbl[0]中的任何一位是1時,OSRdyGrp的第0位置1,
    當OSRdyTbl[1]中的任何一位是1時,OSRdyGrp的第1位置1,
 當OSRdyTbl[2]中的任何一位是1時,OSRdyGrp的第2位置1,
 當OSRdyTbl[3]中的任何一位是1時,OSRdyGrp的第3位置1,
 當OSRdyTbl[4]中的任何一位是1時,OSRdyGrp的第4位置1,
    當OSRdyTbl[5]中的任何一位是1時,OSRdyGrp的第5位置1,
 當OSRdyTbl[6]中的任何一位是1時,OSRdyGrp的第6位置1,
    當OSRdyTbl[7]中的任何一位是1時,OSRdyGrp的第7位置1,
 
程式清單3.5中的程式碼用於將任務放入就緒表。Prio是任務的優先順序。

 程式清單 L3.5 使任務進入就緒態 
OSRdyGrp            |= OSMapTbl[prio >> 3]; 
OSRdyTbl[prio >> 3] |= OSMapTbl[prio & 0x07]; 


 表 T3.1 OSMapTbl[]的值
 
Index Bit Mask (Binary) 
0 00000001 
1 00000010 
2 00000100 
3 00001000 
4 00010000 
5 00100000 
6 01000000 
7 10000000 


  讀者可以看出,任務優先順序的低三位用於確定任務在總就緒表OSRdyTbl[]中的所在位。接下去的三位用於確定是在OSRdyTbl[]陣列的第幾個元 素。OSMapTbl[]是在ROM中的(見檔案OS_CORE.C)遮蔽字,用於限制OSRdyTbl[]陣列的元素下標在0到7之間,見表3.1

圖3.3μC/OS-Ⅱ就緒表

 如果一個任務被刪除了,則用程式清單3.6中的程式碼做求反處理。

 程式清單 L3.6 從就緒表中刪除一個任務 
if ((OSRdyTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0) 
    OSRdyGrp &= ~OSMapTbl[prio >> 3]; 

  以上程式碼將就緒任務表陣列OSRdyTbl[]中相應元素的相應位清零,而對於OSRdyGrp,只有當被刪除任務所在任務組中全組任務一個都沒有進入就 緒態時,才將相應位清零。也就是說OSRdyTbl[prio>>3]所有的位都是零時,OSRdyGrp的相應位才清零。為了找到那個進入 就緒態的優先順序最高的任務,並不需要從OSRdyTbl[0]開始掃描整個就緒任務表,只需要查另外一張表,即優先順序判定表OSUnMapTbl ([256])(見檔案OS_CORE.C)。OSRdyTbl[]中每個位元組的8位代表這一組的8個任務哪些進入就緒態了,低位的優先順序高於高位。利用 這個位元組為下標來查OSUnMapTbl這張表,返回的位元組就是該組任務中就緒態任務中優先順序最高的那個任務所在的位置。這個返回值在0到7之間。確定進 入就緒態的優先順序最高的任務是用以下程式碼完成的,如程式清單L3.7所示。
程式清單 L3.7 找出進入就緒態的優先順序最高的任務 
y    = OSUnMapTbl[OSRdyGrp]; 
x    = OSUnMapTbl[OSRdyTbl[y]]; 
prio = (y << 3) + x; 

  例如,如果OSRdyGrp的值為二進位制01101000,查OSUnMapTbl[OSRdyGrp]得到的值是3,它相應於OSRdyGrp中的第3 位bit3,這裡假設最右邊的一位是第0位bit0。類似地,如果OSRdyTbl[3]的值是二進位制11100100,則OSUnMapTbl [OSRdyTbc[3]]的值是2,即第2位。於是任務的優先順序Prio就等於26(3*8+2)。利用這個優先順序的值。查任務控制塊優先順序表 OSTCBPrioTbl[],得到指向相應任務的任務控制塊OS_TCB的工作就完成了。
任務排程(Task Scheduling)
 μC/OS -Ⅱ總是執行進入就緒態任務中優先順序最高的那一個。確定哪個任務優先順序最高,下面該哪個任務運行了的工作是由排程器(Scheduler)完成的。任務級 的排程是由函式OSSched()完成的。中斷級的排程是由另一個函式OSIntExt()完成的,這個函式將在以後描述。OSSched()的程式碼如程 序清單L3.8所示。

 程式清單 L3.8 任務排程器(the Task Scheduler) 
void OSSched (void) 

    INT8U y; 
 
 
    OS_ENTER_CRITICAL(); 
    if ((OSLockNesting | OSIntNesting) == 0) { (1) 
        y             = OSUnMapTbl[OSRdyGrp]; (2) 
        OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]); (2) 
        if (OSPrioHighRdy != OSPrioCur) { (3) 
            OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; (4) 
            OSCtxSwCtr++;                                              (5) 
            OS_TASK_SW();                                              (6) 
        } 
    } 
    OS_EXIT_CRITICAL(); 

 μC/OS -Ⅱ任務排程所花的時間是常數,與應用程式中建立的任務數無關。如程式清單中[L3.8(1)]條件語句的條件不滿足,任務排程函式OSSched()將 退出,不做任務排程。這個條件是:如果在中斷服務子程式中呼叫OSSched(),此時中斷巢狀層數OSIntNesting>0,或者由於使用者至 少呼叫了一次給任務排程上鎖函式OSSchedLock(),使OSLockNesting>0。如果不是在中斷服務子程式呼叫OSSched (),並且任務排程是允許的,即沒有上鎖,則任務排程函式將找出那個進入就緒態且優先順序最高的任務[L3.8(2)],進入就緒態的任務在就緒任務表中有 相應的位置位。一旦找到那個優先順序最高的任務,OSSched()檢驗這個優先順序最高的任務是不是當前正在執行的任務,以此來避免不必要的任務排程 [L3.8(3)]。注意,在μC/OS中曾經是先得到OSTCBHighRdy然後和OSTCBCur做比較。因為這個比較是兩個指標型變數的比較,在 8位和一些16位微處理器中這種比較相對較慢。而在μC/OS-Ⅱ中是兩個整數的比較。並且,除非使用者實際需要做任務切換,在查任務控制塊優先順序表 OSTCBPrioTbl[]時,不需要用指標變數來查OSTCBHighRdy。綜合這兩項改進,即用整數比較代替指標的比較和當需要任務切換時再查 表,使得μC/OS-Ⅱ比μC/OS在8位和一些16位微處理器上要更快一些。
 為實現任務切換,OSTCBHighRdy必須指向優先順序最高的 那個任務控制塊OS_TCB,這是通過將以OSPrioHighRdy為下標的OSTCBPrioTbl[]陣列中的那個元素賦給 OSTCBHighRdy來實現的[L3.8(4)]。接著,統計計數器OSCtxSwCtr加1,以跟蹤任務切換次數[L3.8(5)]。最後巨集呼叫 OS_TASK_SW()來完成實際上的任務切換[L3.8(6)]。
 任務切換很簡單,由以下兩步完成,將被掛起任務的微處理器暫存器推入堆 棧,然後將較高優先順序的任務的暫存器值從棧中恢復到暫存器中。在μC/OS-Ⅱ中,就緒任務的棧結構總是看起來跟剛剛發生過中斷一樣,所有微處理器的寄存 器都儲存在棧中。換句話說,μC/OS-Ⅱ執行就緒態的任務所要做的一切,只是恢復所有的CPU暫存器並執行中斷返回指令。為了做任務切換,執行 OS_TASK_SW(),人為模仿了一次中斷。多數微處理器有軟中斷指令或者陷阱指令TRAP來實現上述操作。中斷服務子程式或陷阱處理(Trap hardler),也稱作事故處理(exception handler),必須提供中斷向量給組合語言函式OSCtxSw()。OSCtxSw()除了需要OS_TCBHighRdy指向即將被掛起的任務,還 需要讓當前任務控制塊OSTCBCur指向即將被掛起的任務,參見第8章,移植μC/OS-Ⅱ,有關於OSCtxSw()的更詳盡的解釋。
 OSSched ()的所有程式碼都屬臨界段程式碼。在尋找進入就緒態的優先順序最高的任務過程中,為防止中斷服務子程式把一個或幾個任務的就緒位置位,中斷是被關掉的。為縮短 切換時間,OSSched()全部程式碼都可以用匯編語言寫。為增加可讀性,可移植性和將組合語言程式碼最少化,OSSched()是用C寫的。
給排程器上鎖和開鎖(Locking and UnLocking the Scheduler)
  給排程器上鎖函式OSSchedlock()(程式清單L3.9)用於禁止任務排程,直到任務完成後呼叫給排程器開鎖函式OSSchedUnlock() 為止,(程式清單L3.10)。呼叫OSSchedlock()的任務保持對CPU的控制權,儘管有個優先順序更高的任務進入了就緒態。然而,此時中斷是可 以被識別的,中斷服務也能得到(假設中斷是開著的)。OSSchedlock()和OSSchedUnlock()必須成對使用。變數 OSLockNesting跟蹤OSSchedLock()函式被呼叫的次數,以允許巢狀的函式包含臨界段程式碼,這段程式碼其它任務不得干預。μC/OS- Ⅱ允許巢狀深度達255層。當OSLockNesting等於零時,排程重新得到允許。函式OSSchedLock()和OSSchedUnlock() 的使用要非常謹慎,因為它們影響μC/OS-Ⅱ對任務的正常管理。
 當OSLockNesting減到零的時候,OSSchedUnlock()呼叫OSSched[L3.10(2)]。OSSchedUnlock()是被某任務呼叫的,在排程器上鎖的期間,可能有什麼事件發生了並使一個更高優先順序的任務進入就緒態。
  呼叫OSSchedLock()以後,使用者的應用程式不得使用任何能將現行任務掛起的系統呼叫。也就是說,使用者程式不得呼叫OSMboxPend()、 OSQPend()、OSSemPend()、OSTaskSuspend(OS_PR1O_SELF)、OSTimeDly()或 OSTimeDlyHMSM(),直到OSLockNesting回零為止。因為排程器上了鎖,使用者就鎖住了系統,任何其它任務都不能執行。
 當低優先順序的任務要發訊息給多工的郵箱、訊息佇列、訊號量時(見第6章 任務間通訊和同步),使用者不希望高優先順序的任務在郵箱、佇列和訊號量沒有得到訊息之前就取得了CPU的控制權,此時,使用者可以使用禁止排程器函式。

程式清單 L3.9 給排程器上鎖 
void OSSchedLock (void) 

    if (OSRunning == TRUE) { 
        OS_ENTER_CRITICAL(); 
        OSLockNesting++; 
        OS_EXIT_CRITICAL(); 
    } 


 程式清單 L3.10 給排程器開鎖. 
void OSSchedUnlock (void) 

    if (OSRunning == TRUE) { 
        OS_ENTER_CRITICAL(); 
        if (OSLockNesting > 0) { 
            OSLockNesting--; 
            if ((OSLockNesting | OSIntNesting) == 0) {             (1) 
                OS_EXIT_CRITICAL(); 
                OSSched();                                         (2) 
            } else { 
                OS_EXIT_CRITICAL(); 
            } 
        } else { 
            OS_EXIT_CRITICAL(); 
        } 
    } 


空閒任務(Idle Task)
 μC/OS -Ⅱ總是建立一個空閒任務,這個任務在沒有其它任務進入就緒態時投入執行。這個空閒任務[OSTaskIdle()]永遠設為最低優先順序,即 OS_LOWEST_PRI0。空閒任務OSTaskIdle()什麼也不做,只是在不停地給一個32位的名叫OSIdleCtr的計數器加1,統計任務 (見3.08節,統計任務)使用這個計數器以確定現行應用軟體實際消耗的CPU時間。程式清單L3.11是空閒任務的程式碼。在計數器加1前後,中斷是先關 掉再開啟的,因為8位以及大多數16位微處理器的32位加1需要多條指令,要防止高優先順序的任務或中斷服務子程式從中打入。空閒任務不可能被應用軟體刪 除。

 程式清單 L3.11 μC/OS-Ⅱ的空閒任務. 
void OSTaskIdle (void *pdata) 

    pdata = pdata; 
    for (;;) { 
        OS_ENTER_CRITICAL(); 
        OSIdleCtr++; 
        OS_EXIT_CRITICAL(); 
    } 

統計任務
 μC/OS -Ⅱ有一個提供執行時間統計的任務。這個任務叫做OSTaskStat(),如果使用者將系統定義常數OS_TASK_STAT_EN(見檔案 OS_CFG.H)設為1,這個任務就會建立。一旦得到了允許,OSTaskStat()每秒鐘執行一次(見檔案OS_CORE.C),計算當前的CPU 利用率。換句話說,OSTaskStat()告訴使用者應用程式使用了多少CPU時間,用百分比表示,這個值放在一個有符號8位整數OSCPUsage中, 精讀度是1個百分點。
 如果使用者應用程式打算使用統計任務,使用者必須在初始化時建立一個唯一的任務,在這個任務中呼叫OSStatInit() (見檔案OS_CORE.C)。換句話說,在呼叫系統啟動函式OSStart()之前,使用者初始程式碼必須先建立一個任務,在這個任務中呼叫系統統計初始化 函式OSStatInit(),然後再建立應用程式中的其它任務。程式清單L3.12是統計任務的示意性程式碼。

 程式清單 L3.12 初始化統計任務. 
void main (void) 

    OSInit();                 /* 初始化uC/OS-II (1)*/ 
    /* 安裝uC/OS-II的任務切換向量 */ 
    /* 建立使用者起始任務(為了方便討論,這裡以TaskStart()作為起始任務) (2)*/ 
    OSStart();                /* 開始多工排程 (3)*/ 

 
 
 
void TaskStart (void *pdata) 

    /* 安裝並啟動uC/OS-II的時鐘節拍 (4)*/ 
    OSStatInit();             /* 初始化統計任務 (5)*/ 
    /* 建立使用者應用程式任務 */ 
    for (;;) { 
        /* 這裡是TaskStart()的程式碼! */ 
    } 

  因為使用者的應用程式必須先建立一個起始任務[TaskStart()],當主程式main()呼叫系統啟動函式OSStcnt()的時候,μC/OS-Ⅱ 只有3個要管理的任務:TaskStart()、OSTaskIdle()和OSTaskStat()。請注意,任務TaskStart()的名稱是無所 謂的,叫什麼名字都可以。因為μC/OS-Ⅱ已經將空閒任務的優先順序設為最低,即OS_LOWEST_PR10,統計任務的優先順序設為次低, OS_LOWEST_PR10-1。啟動任務TaskStart()總是優先順序最高的任務。
 圖F3.4解釋初始化統計任務時的流程。使用者必須首 先呼叫的是μC/OS-Ⅱ中的系統初始化函式OSInit(),該函式初始化μC/OS-Ⅱ[圖F3.4(2)]。有的處理器(例如Motorola的 MC68HC11),不需要“設定”中斷向量,中斷向量已經在ROM中有了。使用者必須呼叫OSTaskCreat()或者OSTaskCreatExt ()以建立TaskStart()[圖F3.4(3)]。進入多工的條件準備好了以後,呼叫系統啟動函式OSStart()。這個函式將使 TaskStart()開始執行,因為TaskStart()是優先順序最高的任務[圖F3.4(4)]]。

圖F3.4統計任務的初始化

TaskStart ()負責初始化和啟動時鐘節拍[圖F3.4(5)]。在這裡啟動時鐘節拍是必要的,因為使用者不會希望在多工還沒有開始時就接收到時鐘節拍中斷。接下去 TaskStart()呼叫統計初始化函式OSStatInit()[圖F3.4(6)]。統計初始化函式OSStatInit()決定在沒有其它應用任 務執行時,空閒計數器(OSIdleCtr)的計數有多快。奔騰II微處理器以333MHz執行時,加1操作可以使該計數器的值達到每秒 15,000,000次。OSIdleCtr的值離32位計數器的溢位極限值4,294,967,296還差得遠。微處理器越來越快,使用者要注意這裡可能 會是將來的一個潛在問題。
 系統統計初始化任務函式OSStatInit()呼叫延遲函式OSTimeDly()將自身延時2個時鐘節拍以停止自 身的執行[圖F3.4(7)]。這是為了使OSStatInit()與時鐘節拍同步。μC/OS-Ⅱ然後選下一個優先順序最高的進入就緒態的任務執行,這恰 好是統計任務OSTaskStat()。讀者會在後面讀到OSTaskStat()的程式碼,但粗看一下,OSTaskStat()所要做的第一件事就是查 看統計任務就緒標誌是否為“假”,如果是的話,也要延時兩個時鐘節拍[圖F3.4(8)]。一定會是這樣,因為標誌OSStatRdy已被OSInit ()函式初始化為“假”,所以實際上DSTaskStat也將自己推入休眠態(Sleep)兩個時鐘節拍[圖F3.4(9)]。於是任務切換到空閒任務, OSTaskIdle()開始執行,這是唯一一個就緒態任務了。CPU處在空閒任務OSTaskIdle中,直到TaskStart()的延遲兩個時鐘節 拍完成[圖3.4(10)]。兩個時鐘節拍之後,TaskStart()恢復執行[圖F3.4(11)]。 在執行OSStartInit()時,空閒計 數器OSIdleCtr被清零[圖F3.4(12)]。然後,OSStatInit()將自身延時整整一秒[圖F3.4(13)]。因為沒有其它進入就緒 態的任務,OSTaskIdle()又獲得了CPU的控制權[圖F3.4(14)]。一秒鐘以後,TaskStart()繼續執行,還是在 OSStatInit()中,空閒計數器將1秒鐘內計數的值存入空閒計數器最大值OSIdleCtrMax中[圖F3.4(15)]。
OSStarInit()將統計任務就緒標誌OSStatRdy設為“真”[圖F3.4(16)],以此來允許兩個時鐘節拍以後OSTaskStat()開始計算CPU的利用率。
統計任務的初始化函式OSStatInit()的程式碼如程式清單 L3.13所示。

 程式清單 L3.13 統計任務的初始化. 
void OSStatInit (void) 

    OSTimeDly(2); 
    OS_ENTER_CRITICAL(); 
    OSIdleCtr    = 0L; 
    OS_EXIT_CRITICAL(); 
    OSTimeDly(OS_TICKS_PER_SEC); 
    OS_ENTER_CRITICAL(); 
    OSIdleCtrMax = OSIdleCtr; 
    OSStatRdy    = TRUE; 
    OS_EXIT_CRITICAL(); 

統 計任務OSStat()的程式碼程式清單L3.14所示。在前面一段中,已經討論了為什麼要等待統計任務就緒標誌OSStatRdy[L3.14(1)]。 這個任務每秒執行一次,以確定所有應用程式中的任務消耗了多少CPU時間。當用戶的應用程式程式碼加入以後,執行空閒任務的CPU時間就少了, OSIdleCtr就不會像原來什麼任務都不執行時有那麼多計數。要知道,OSIdleCtr的最大計數值是OSStatInit()在初始化時儲存在計 數器最大值OSIdleCtrMax中的。CPU利用率(表示式[3.1])是儲存在變數OSCPUsage[L3.14(2)]中的:

[3.1]表示式  Need to typeset the equation.

一 旦上述計算完成,OSTaskStat()呼叫任務統計外界接入函式OSTaskStatHook() [L3.14(3)],這是一個使用者可定義的函式,這個函式能使統計任務得到擴充套件。這樣,使用者可以計算並顯示所有任務總的執行時間,每個任務執行時間的百 分比以及其它資訊(參見1.09節例3)。

 程式清單 L3.14 統計任務 
void OSTaskStat (void *pdata) 

    INT32U run; 
    INT8S usage; 
 
 
    pdata = pdata; 
    while (OSStatRdy == FALSE) {                                  (1) 
        OSTimeDly(2 * OS_TICKS_PER_SEC); 
    } 
    for (;;) { 
        OS_ENTER_CRITICAL(); 
        OSIdleCtrRun = OSIdleCtr; 
        run          = OSIdleCtr; 
        OSIdleCtr    = 0L; 
        OS_EXIT_CRITICAL(); 
        if (OSIdleCtrMax > 0L) { 
            usage = (INT8S)(100L - 100L * run / OSIdleCtrMax);    (2) 
            if (usage > 100) { 
                OSCPUUsage = 100; 
            } else if (usage < 0) { 
                OSCPUUsage = 0; 
            } else { 
                OSCPUUsage = usage; 
            } 
        } else { 
            OSCPUUsage = 0; 
        } 
        OSTaskStatHook();                                         (3) 
        OSTimeDly(OS_TICKS_PER_SEC); 
    } 

μC/OS中的中斷處理

μC/OS中,中斷服務子程式要用匯編語言來寫。然而,如果使用者使用的C語言編譯器支援線上組合語言的話,使用者可以直接將中斷服務子程式程式碼放在C語言的程式檔案中。中斷服務子程式的示意碼如程式清單L3.15所示。

 程式清單 L3.15 μC/OS-II中的中斷服務子程式. 
使用者中斷服務子程式:                                                             
    儲存全部CPU暫存器;                                      (1)                       
 呼叫OSIntEnter或OSIntNesting直接加1;               (2)  
    執行使用者程式碼做中斷服務;                                 (3)                   
    呼叫OSIntExit();                                     (4)                      
    恢復所有CPU暫存器;                                     (5)                       
    執行中斷返回指令;                                      (6)          


 使用者程式碼應該將全部CPU暫存器推入當前任務棧[L3.15(1)]。注意,有些微處理器,例如Motorola68020(及68020以上的微處理器),做中斷服務時使用另外的堆疊。
μC/OS-Ⅱ可以用在這類微處理器中,當任務切換時,暫存器是儲存在被中斷了的那個任務的棧中的。
 μC/OS -Ⅱ需要知道使用者在做中斷服務,故使用者應該呼叫OSIntEnter(),或者將全程變數OSIntNesting[L3.15(2)]直接加1,如果用 戶使用的微處理器有儲存器直接加1的單條指令的話。如果使用者使用的微處理器沒有這樣的指令,必須先將OSIntNesting讀入暫存器,再將暫存器加 1,然後再寫回到變數OSIatNesting中去,就不如呼叫OSIatEnter()。OSIntNesting是共享資源。OSIntEnter ()把上述三條指令用開中斷、關中斷保護起來,以保證處理OSIntNesting時的排它性。直接給OSIntNesting加1比呼叫 OSIntEnter()快得多,可能時,直接加1更好。要當心的是,在有些情況下,從OSIntEnter()返回時,會把中斷開了。遇到這種情況,在 呼叫OSIntEnter()之前要先清中斷源,否則,中斷將連續反覆打入,使用者應用程式就會崩潰!
 上述兩步完成以後,使用者可以開始服務於叫中斷的裝置了[L3.15(3)]。這一段完全取決於應用。μC/OS-Ⅱ允許中斷巢狀,因為μC/OS-Ⅱ跟蹤巢狀層數OSIntNesting。然而,為允許中斷巢狀,在多數情況下,使用者應在開中斷之前先清中斷源。
  呼叫脫離中斷函式OSIntExit()[L3.15(4)]標誌著中斷服務子程式的終結,OSIntExit()將中斷巢狀層數計數器減1。當巢狀計數 器減到零時,所有中斷,包括巢狀的中斷就都完成了,此時μC/OS-Ⅱ要判定有沒有優先順序較高的任務被中斷服務子程式(或任一巢狀的中斷)喚醒了。如果有 優先順序高的任務進入了就緒態,μC/OS-Ⅱ就返回到那個高優先順序的任務,OSIntExit()返回到呼叫點[L3.15(5)]。儲存的暫存器的值是 在這時恢復的,然後是執行中斷返回指令[L3.16(6)]。注意,如果排程被禁止了(OSIntNesting>0),μC/OS-Ⅱ將被返回到 被中斷了的任務。
 以上描述的詳細解釋如圖F3.5所示。中斷來到了[F3.5(1)]但還不能被被CPU識別,也許是因為中斷被μC/OS-Ⅱ 或使用者應用程式關了,或者是因為CPU還沒執行完當前指令。一旦CPU響應了這個中斷[F3.5(2)],CPU的中斷向量(至少大多數微處理器是如此) 跳轉到中斷服務子程式[F3.5(3)]。如上所述,中斷服務子程式儲存CPU暫存器(也叫做 CPU context)[F3.5(4)],一旦做完,使用者中斷服務子程式通知μC/OS-Ⅱ進入中斷服務子程式了,辦法是呼叫OSIntEnter()或者給 OSIntNesting直接加1[F3.5(5)]。然後使用者中斷服務程式碼開始執行[F3.5(6)]。使用者中斷服務中做的事要儘可能地少,要把大部分 工作留給任務去做。中斷服務子程式通知某任務去做事的手段是呼叫以下函式之一:OSMboxPost(),OSQPost(),OSQPostFront (),OSSemPost()。中斷髮生並由上述函式發出訊息時,接收訊息的任務可能是,也可能不是掛起在郵箱、佇列或訊號量上的任務。使用者中斷服務完成 以後,要呼叫OSIntExit()[F3.5(7)]。從時序圖上可以看出,對被中斷了的任務說來,如果沒有高優先順序的任務被中斷服務子程式啟用而進入 就緒態,OSIntExit()只佔用很短的執行時間。進而,在這種情況下,CPU暫存器只是簡單地恢復[F3.5(8)]並執行中斷返回指令[F3.5 (9)]。如果中斷服務子程式使一個高優先順序的任務進入了就緒態,則OSIntExit()將佔用較長的執行時間,因為這時要做任務切換[F3.5 (10)]。新任務的暫存器內容要恢復並執行中斷返回指令[F3.5(12)]。

圖3.5 中斷服務

 進入中斷函式OSIntEnter()的程式碼如程式清單L3.16所示,從中斷服務中退出函式OSIntExit()的程式碼如程式清單L3.17所示。如前所述,OSIntEnter()所做的事是非常少的。

 程式清單 L3.16 通知μC/OS-Ⅱ,中斷服務子程式開始了. 
void OSIntEnter (void) 

    OS_ENTER_CRITICAL(); 
    OSIntNesting++; 
    OS_EXIT_CRITICAL(); 


 程式清單 L3.17 通知μC/OS-Ⅱ,脫離了中斷服務 
void OSIntExit (void) 

    OS_ENTER_CRITICAL();                                             (1) 
    if ((--OSIntNesting | OSLockNesting) == 0) {                     (2) 
        OSIntExitY    = OSUnMapTbl[OSRdyGrp];                        (3) 
        OSPrioHighRdy = (INT8U)((OSIntExitY << 3) + 
                        OSUnMapTbl[OSRdyTbl[OSIntExitY]]); 
        if (OSPrioHighRdy != OSPrioCur) { 
            OSTCBHighRdy  = OSTCBPrioTbl[OSPrioHighRdy]; 
            OSCtxSwCtr++; 
            OSIntCtxSw();                                            (4) 
        } 
    } 
    OS_EXIT_CRITICAL(); 


 OSIntExit ()看起來非常像OSSched()。但有三點不同。第一點,OSIntExit()使中斷巢狀層數減1[L3.17(2)]而排程函式OSSched ()的排程條件是:中斷巢狀層數計數器和鎖定巢狀計數器(OSLockNesting)二者都必須是零。第二個不同點是,OSRdyTbl[]所需的檢索 值Y是儲存在全程變數OSIntExitY中的[L3.17(3)]。這是為了避免在任務棧中安排區域性變數。這個變數在哪兒和中斷任務切換函式 OSIntCtxSw()有關,(見9.04.03節,中斷任務切換函式)。最後一點,如果需要做任務切換,OSIntExit()將呼叫 OSIntCtxSw()[L3.17(4)]而不是呼叫OS_TASK_SW(),正像在OSSched()函式中那樣。
 呼叫中斷切換函式 OSIntCtxSw()而不呼叫任務切換函式OS_TASK_SW(),有兩個原因,首先是,如程式清單中L3.5(1)和圖F3.6(1)所示,一半 的工作,即CPU暫存器入棧的工作已經做完了。第二個原因是,在中斷服務子程式中呼叫OSIntExit()時,將返回地址推入了堆疊[L3.15(4) 和F3.6(2)]。OSIntExit()中的進入臨界段函式OS_ENTER_CRITICAL()或許將CPU的狀態字也推入了堆疊L3.7(1) 和F3.6(3)。這取決於中斷是怎麼被關掉的(見第8章移植μC/OS-Ⅱ)。最後,呼叫OSIntCtxSw()時的返回地址又被推入了堆疊 [L3.17(4)和F3.1(4)],除了棧中不相關的部分,當任務掛起時,棧結構應該與μC/OS-Ⅱ所規定的完全一致。OSIntCtxSw()只 需要對棧指標做簡單的調整,如圖F3.6(5)所示。換句話說,調整棧結構要保證所有掛起任務的棧結構看起來是一樣的。

圖3.6中斷中的任務切換函式OSIntCtxSw()調整棧結構
 
  有的微處理器,像Motorola 68HC11中斷髮生時CPU暫存器是自動入棧的,且要想允許中斷巢狀的話,在中斷服務子程式中要重新開中斷,這可以視作一個優點。確實,如果使用者中斷服 務子程式執行得非常快,使用者不需要通知任務自身進入了中斷服務,只要不在中斷服務期間開中斷,也不需要呼叫OSIntEnter()或 OSIntNesting加1。程式清單L3。18中的示意程式碼表示這種情況。一個任務和這個中斷服務子程式通訊的唯一方法是通過全程變數。

 程式清單 L3.18 Motorola 68HC11中的中斷服務子程式 
M68HC11_ISR:                  /* 快中斷服務程式,必須禁止中斷*/ 
    所有暫存器被CPU自動儲存; 
    執行使用者程式碼以響應中斷; 
    執行中斷返回指令; 

時鐘節拍
 μC/OS 需要使用者提供週期性訊號源,用於實現時間延時和確認超時。節拍率應在每秒10次到100次之間,或者說10到100Hz。時鐘節拍率越高,系統的額外負荷 就越重。時鐘節拍的實際頻率取決於使用者應用程式的精度。時鐘節拍源可以是專門的硬體定時器,也可以是來自50/60Hz交流電源的訊號。
 使用者必 須在多工系統啟動以後再開啟時鐘節拍器,也就是在呼叫OSStart()之後。換句話說,在呼叫OSStart()之後做的第一件事是初始化定時器中 斷。通常,容易犯的錯誤是將允許時鐘節拍器中斷放在系統初始化函式OSInit()之後,在調啟動多工系統啟動函式OSStart()之前,如程式清單 L3.19所示。

 程式清單 L3.19 啟動時鐘就節拍器的不正確做法. 
void main(void) 

    . 
    . 
    OSInit();                 /* 初始化uC/OS-II                */ 
    . 
    . 
    /* 應用程式初始化程式碼 ...                          */ 
    /* ... 通過呼叫OSTaskCreate()建立至少一個任務        */ 
    . 
    . 
    允許時鐘節拍(TICKER)中斷; /* 千萬不要在這裡允許時鐘節拍中斷!!!             */ 
    . 
    . 
    OSStart();                /* 開始多工排程                 */ 


這裡潛在地危險是,時鐘節拍中斷有可能在μC/OS-Ⅱ啟動第一個任務之前發生,此時μC/OS-Ⅱ是處在一種不確定的狀態之中,使用者應用程式有可能會崩潰。
 μC/OS -Ⅱ中的時鐘節拍服務是通過在中斷服務子程式中呼叫OSTimeTick()實現的。時鐘節拍中斷服從所有前面章節中描述的規則。時鐘節拍中斷服務子程式 的示意程式碼如程式清單L3.20所示。這段程式碼必須用匯編語言編寫,因為在C語言裡不能直接處理CPU的暫存器。

 程式清單 L3.20 時鐘節拍中斷服務子程式的示意程式碼 
void OSTickISR(void) 

    儲存處理器暫存器的值; 
    呼叫OSIntEnter()或是將OSIntNesting加1; 
    呼叫OSTimeTick(); 
 
    呼叫OSIntExit(); 
    恢復處理器暫存器的值; 
    執行中斷返回指令; 

  時鐘節拍函式OSTimeTick()的程式碼如程式清單3.21所示。OSTimtick()以呼叫可由使用者定義的時鐘節拍外連函式 OSTimTickHook()開始,這個外連函式可以將時鐘節拍函式OSTimtick()予以擴充套件[L3.2(1)]。筆者決定首先呼叫 OSTimTickHook()是打算在時鐘節拍中斷服務一開始就給使用者一個可以做點兒什麼的機會,因為使用者可能會有一些時間要求苛刻的工作要做。 OSTimtick()中量大的工作是給每個使用者任務控制塊OS_TCB中的時間延時項OSTCBDly減1(如果該項不為零的話)。OSTimTick ()從OSTCBList開始,沿著OS_TCB連結串列做,一直做到空閒任務[L3.21(3)]。當某任務的任務控制塊中的時間延時項OSTCBDly減 到了零,這個任務就進入了就緒態[L3.21(5)]。而確切被任務掛起的函式OSTaskSuspend()掛起的任務則不會進入就緒態[L3.21 (4)]。OSTimTick()的執行時間直接與應用程式中建立了多少個任務成正比。

 程式清單 L3.21 時鐘節拍函式 OSTimtick() 的一個節拍服務 
void OSTimeTick (void) 

    OS_TCB *ptcb; 
 
    OSTimeTickHook();                                              (1) 
    ptcb = OSTCBList;                                              (2) 
    while (ptcb->OSTCBPrio != OS_IDLE_PRIO) {                      (3) 
        OS_ENTER_CRITICAL(); 
        if (ptcb->OSTCBDly != 0) { 
            if (--ptcb->OSTCBDly == 0) { 
                if (!(ptcb->OSTCBStat & OS_STAT_SUSPEND)) {        (4) 
                    OSRdyGrp               |= ptcb->OSTCBBitY;     (5) 
                    OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; 
                } else { 
                    ptcb->OSTCBDly = 1; 
                } 
            } 
        } 
        ptcb = ptcb->OSTCBNext; 
        OS_EXIT_CRITICAL(); 
    } 
    OS_ENTER_CRITICAL();                                           (6) 
    OSTime++;                                                      (7) 
    OS_EXIT_CRITICAL(); 


 OSTimeTick()還通過呼叫OSTime()[L3.21(7)]累加從開機以來的時間,用的是一個無符號32位變數。注意,在給OSTime加1之前使用了關中斷,因為多數微處理器給32位數加1的操作都得使用多條指令。
  中斷服務子程式似乎就得寫這麼長,如果使用者不喜歡將中斷服務程式寫這麼長,可以從任務級呼叫OSTimeTick(),如程式清單L3.22所示。要想這 麼做,得建立一個高於應用程式中所有其它任務優先順序的任務。時鐘節拍中斷服務子程式利用訊號量或郵箱發訊號給這個高優先順序的任務。

程式清單 L3.22 時鐘節拍任務 TickTask() 作時鐘節拍服務. 
void TickTask (void *pdata) 

    pdata = pdata; 
    for (;;) { 
        OSMboxPend(...);    /* 等待從時鐘節拍中斷服務程式發來的訊號 */ 
        OSTimeTick(); 
    } 

 使用者當然需要先建立一個郵箱(初始化成NULL)用於發訊號給上述任何告知時鐘節拍中斷已經發生了(程式清單L3.23)。

 程式清單L3.23時鐘節拍中斷服務函式OSTickISR()做節拍服務。 
void OSTickISR(void) 

    儲存處理器暫存器的值; 
    呼叫OSIntEnter()或是將OSIntNesting加1; 
 
    傳送一個‘空’訊息(例如, (void *)1)到時鐘節拍的郵箱; 
 
    呼叫OSIntExit(); 
    恢復處理器暫存器的值; 
    執行中斷返回指令; 


μC/OS-Ⅱ初始化
 在呼叫μC/OS-Ⅱ的任何其它服務之前,μC/OS-Ⅱ要求使用者首先呼叫系統初始化函式OSIint()。OSIint()初始化μC/OS-Ⅱ所有的變數和資料結構(見OS_CORE.C)。
 OSInit ()建立空閒任務idle task,這個任務總是處於就緒態的。空閒任務OSTaskIdle()的優先順序總是設成最低,即OS_LOWEST_PRIO。如果統計任務允許 OS_TASK_STAT_EN和任務建立擴充套件允許都設為1,則OSInit()還得建立統計任務OSTaskStat()並且讓其進入就緒態。 OSTaskStat的優先順序總是設為OS_LOWEST_PRIO-1。
 圖F3.7表示呼叫OSInit()之後,一些μC/OS-Ⅱ變數和資料結構之間的關係。其解釋是基於以下假設的:
在檔案OS_CFG.H中,OS_TASK_STAT_EN是設為1的。
在檔案OS_CFG.H中,OS_LOWEST_PRIO是設為63的。
在檔案OS_CFG.H中, 最多工數OS_MAX_TASKS是設成大於2的。
  以上兩個任務的任務控制塊(OS_TCBs)是用雙向連結串列連結在一起的。OSTCBList指向這個連結串列的起始處。當建立一個任務時,這個任務總是被放在 這個連結串列的起始處。換句話說,OSTCBList總是指向最後建立的那個任務。鏈的終點指向空字元NULL(也就是零)。
 因為這兩個任務都處在就緒態,在就緒任務表OSRdyTbl[]中的相應位是設為1的。還有,因為這兩個任務的相應位是在OSRdyTbl[]的同一行上,即屬同一組,故OSRdyGrp中只有1位是設為1的。
 μC/OS -Ⅱ還初始化了4個空資料結構緩衝區,如圖F3.8所示。每個緩衝區都是單向連結串列,允許μC/OS-Ⅱ從緩衝區中迅速得到或釋放一個緩衝區中的元素。注 意,空任務控制塊在空緩衝區中的數目取決於最多工數OS_MAX_TASKS,這個最多工數是在OS_CFG.H檔案中定義的。μC/OS-Ⅱ自動安 排總的系統任務數OS_N_SYS_TASKS(見檔案μC/OS-Ⅱ.H)。控制塊OS_TCB的數目也就自動確定了。當然,包括足夠的任務控制塊分配 給統計任務和空閒任務。指向空事件表OSEventFreeList和空隊列表OSFreeList的指標將在第6章,任務間通訊與同步中討論。指向空存 儲區的指標表OSMemFreeList將在第7章儲存管理中討論。
μC/OS-Ⅱ的啟動
 多工的啟動是使用者通過呼叫OSStart()實現的。然而,啟動μC/OS-Ⅱ之前,使用者至少要建立一個應用任務,如程式清單L3.24所示。

 程式清單 L3.24 初始化和啟動μC/OS-Ⅱ 
void main (void) 

    OSInit();           /* 初始化uC/OS-II                            */ 
    . 
    . 
    通過呼叫OSTaskCreate()或OSTaskCreateExt()建立至少一個任務; 
    . 
    . 
    OSStart();          /* 開始多工排程!OSStart()永遠不會返回 */ 


圖3.7 呼叫OSInit()之後的資料結構

圖3.8 空緩衝區

 OSStart ()的程式碼如程式清單L3.25所示。當呼叫OSStart()時,OSStart()從任務就緒表中找出那個使用者建立的優先順序最高任務的任務控制塊 [L3.25(1)]。然後,OSStart()呼叫高優先順序就緒任務啟動函式OSStartHighRdy()[L3,25(2)],(見組合語言檔案 OS_CPU_A.ASM),這個檔案與選擇的微處理器有關。實質上,函式OSStartHighRdy()是將任務棧中儲存的值彈回到CPU暫存器中, 然後執行一條中斷返回指令,中斷返回指令強制執行該任務程式碼。見9.04.01節,高優先順序就緒任務啟動函式OSStartHighRdy()。那一節詳 細介紹對於80x86微處理器是怎麼做的。注意,OSStartHighRdy()將永遠不返回到OSStart()。

 程式清單 L3.25 啟動多工. 
void OSStart (void) 

    INT8U y; 
    INT8U x; 
 
    if (OSRunning == FALSE) { 
        y             = OSUnMapTbl[OSRdyGrp]; 
        x             = OSUnMapTbl[OSRdyTbl[y]]; 
        OSPrioHighRdy = (INT8U)((y << 3) + x); 
        OSPrioCur     = OSPrioHighRdy; 
        OSTCBHighRdy  = OSTCBPrioTbl[OSPrioHighRdy];              (1) 
        OSTCBCur      = OSTCBHighRdy; 
        OSStartHighRdy();                                         (2) 
    } 

  多工啟動以後變數與資料結構中的內容如圖F3.9所示。這裡筆者假設使用者建立的任務優先順序為6,注意,OSTaskCtr指出已經建立了3個任務。 OSRunning已設為“真”,指出多工已經開始,OSPrioCur和OSPrioHighRdy存放的是使用者應用任務的優先順序,OSTCBCur 和OSTCBHighRdy二者都指向使用者任務的任務控制塊。
獲取當前μC/OS-Ⅱ的版本號
 應用程式呼叫OSVersion()[程式清單L3.26]可以得到當前μC/OS-Ⅱ的版本號。OSVersion()函式返回版本號值乘以100。換言之,200表示版本號2.00。

 程式清單 L3.26 得到μC/OS-Ⅱ當前版本號 
INT16U OSVersion (void) 

    return (OS_VERSION); 

 為找到μC/OS-Ⅱ的最新版本以及如何做版本升級,使用者可以與出版商聯絡,或者檢視μC/OS-Ⅱ得正式網站WWW. uCOS-II.COM

圖3.9呼叫OSStart()以後的變數與資料結構
OSEvent???()函式
讀 者或許注意到有4個OS_CORE.C中的函式沒有在本章中提到。這4個函式是OSEventWaitListInit(), OSEventTaskRdy(),OSEventTaskWait(),OSEventTO()。這幾個函式是放在檔案OS_CORE.C中的,而對如 何使用這個函式的解釋見第6章,任務間的通訊與同步。