Understanding uC/OS-II (2)



    -- Kernel Structure









Critical Sections (臨界段)


和其他 kernel 一樣,uC/OS-II 為了處理 critical section 的程式碼需要 disable interrupt,處理完畢後再 enable interrupt。這使得 uC/OS-II 能夠避免同時有其他 task 或中斷服務進入 critical section 程式碼。disable interrupt 的時間是 real-time kernel 開發商應提供的最重要的指標之一,因為這個指標會影響使用者系統對即時事件的回應性。uC/OS-II 努力使 disable interrupt 時間降至最短,但就使用 uC/OS-II 而言, disable interrupt 的時間取決於微處理器的架構以及 編譯器所產生程式碼的品質


微處理器一般都有 disable/enable interrupt 指令,使用者使用的 C 語言編譯器必須提供某種機制能夠在 C 中直接實現 disable/enable interrupt 的操作。某些 C 編譯器允許在使用者的 C 程式碼中插入組合語言的語句。這使得插入微處理器指令來 disable/enable interrupt 很容易實現。而有的編譯器把從 C 語言中 disable/enable interrupt 放在語言的擴展部分。所以 uC/OS-II 定義兩個巨集 (macros) 來 disable interrupt 和 enable interrupt,以便避開不同 C 編譯器廠商選擇不同的方法來處理 disable/enable interrupt。uC/OS-II中的這兩個巨集分別是:



  • OS_ENTER_CRITICAL()
  • OS_EXIT_CRITICAL()

因為這兩個巨集的定義取決於所用的微處理器,故在檔案 OS_CPU.H 中可以找到相對應的巨集定義。每種微處理器都有自己的 OS_CPU.H 檔案。




Task (任務)


在 embedded RTOS 中,一個 task 通常是一個無限的迴圈 [see L2.1(2)],如程式清單 L2.1 所示。一個 task 看起來像其他 C 的函式一樣,有函式返回類型,有傳入參數變數,但是 task 是絕不會返回的。故返回參數必須定義成 void [see L2.1(1)]。








程式清單 L2.1 task 是一個無限迴圈



void YourTask(void *pdata) (1)
{
for (;;) { (2)
/* 使用者程式碼 */

呼叫 uC/OS-II 的某種系統服務:
OSMboxPend();
OSQPend();
OSSemPend();
OSTaskDel(OS_PRIO_SELF);
OSTaskSuspend(OS_PRIO_SELF);
OSTimeDly();
OSTimeDlyHMSM();

/* 使用者程式碼 */

}
}

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








程式清單 L2.2 task 完成後自我刪除



void YourTask (void *pdata)
{
.
.
/* 使用者程式碼 */
.
OSTaskDel(OS_PRIO_SELF);
}

傳入參數變數 [L2.1(1)] 是由使用者程式碼在第一次執行的時候傳入的。請注意,該變數的資料型別是一個指向void的指標。這是為了允許使用者應用程式傳遞任何類型的資料給 task 。這個指標好比一輛萬能的車子,如果需要的話,可以運載一個變數的位址,或一個結構,甚至是一個函式的位址。也可以建立許多相同的 task,所有 task 都使用同一個函式 (或者說是同一個 task 程式碼程式)。例如,使用者可以將四個串列口安排成每個串列口都是一個單獨的 task,而每個 task 的程式碼實際上是相同的。並不需要將程式碼複製四次,使用者可以建立一個 task,向這個 task 傳入一個指向某資料結構的指標變數,這個資料結構定義串列口的參數 (baud rate, I/O port addresses, interrupt vector number, etc.)。


uC/OS-II 可以管理多達 64 個 task,但目前版本 uC/OS-II 有兩個 task 已經被系統佔用了。作者保留了優先權為 0、1、2、3、OS_LOWEST_PRIO-3、OS_LOWEST_PRI0-2,OS_LOWEST_PRI0-1 以及 OS_LOWEST_PRI0 這 8 個 task 以被將來使用。OS_LOWEST_PRI0 是作為定義的常數在 OS_CFG.H 檔案中用定義常數語句 #define constant 定義的。因此使用者可以有多達 56 個應用 task。必須給每個 task 賦以不同的優先權,優先權可以從 0 到 OS_LOWEST_PR10-2。優先權號越低,task 的優先權越高。uC/OS-II 總是執行進入就緒態 (READY)的優先權最高的 task 。目前版本的 uC/OS-II中, task 的優先權號就是 task 編號 (ID) 。優先權號 (或 task 的ID號) 也被一些 kernel 服務函式呼叫,如改變優先權函式 OSTaskChangePrio(),以及 task 刪除函式OSTaskDel()。


為了使 uC/OS-II 能管理使用者 task,使用者必須在建立一個 task 的時候,將 task 的啟始位址與其他參數一起傳給下面兩個函式中的一個:OSTastCreat()OSTaskCreatExt()。OSTaskCreateExt() 是 OSTaskCreate() 的擴展,擴展了一些附加的功能。這兩個函式的解釋見 Task Management。




Task States


圖 2.1 是 uC/OS-II 控制下的 task States 轉換圖。在任何一個時刻,task 的狀態一定是在這五種狀態之一。


睡眠態 (DORMANT) 指 task 駐留在程式空間之中,還沒有交給 uC/OS-II 管理, (見程式清單 L2.1 或 L2.2) 。把 task 交給 uC/OS-II 是通過呼叫下述兩個函式之一: OSTaskCreate() 或 OSTaskCreateExt()。當 task 一旦建立,這個 task 就進入就緒態 (READY) 準備執行。task 的建立可以是在多工執行開始之前,也可以是動態地被一個執行著的 task 建立。如果一個 task 是被另一個 task 建立的,而這個 task 的優先權高於建立它的那個 task,則這個剛剛建立的 task 將立即得到 CPU 的控制權。一個 task 可以通過呼叫 OSTaskDel() 返回到睡眠態 (DORMANT),或通過呼叫該函式讓另一個 task 進入睡眠態 (DORMANT)。


呼叫 OSStart() 可以啟動多工。OSStart() 函式執行進入就緒態 (READY) 的優先權最高的 task。就緒的 task 只有當所有優先權高於這個 task 的 task 轉為等待狀態 (WAITING),或者是被刪除了,才能進入執行態 (RUNNING)。



圖 2.1 task 的狀態


正在執行的 task 可以通過呼叫兩個函式之一將自身延遲一段時間,這兩個函式是 OSTimeDly()OSTimeDlyHMSM()。這個 task 於是進入等待狀態 (WAITING),等待這段時間過去,下一個優先權最高的、並進入了就緒態 (READY)的 task 立刻獲得 CPU 的控制權。等待的時間過去以後,系統服務函式 OSTimeTick() 使延遲了的 task 進入就緒態 (READY)。


正在執行的 task 期待某一事件的發生時也要等待,方法是呼叫以下 3 個函式之一:OSSemPend()OSMboxPend(),或 OSQPend()。呼叫後 task 進入了等待狀態 (WAITING)。當 task 因等待事件被暫停 (PEND) ,下一個優先權最高的 task 立即得到了 CPU 的控制權。當事件發生了,被暫停的 task 進入就緒態 (READY)。事件發生的報告可以來自另一個 task,也可能來自 ISR。


正在執行的 task 是可以被中斷的,除非該 task 將中斷關了,或者 uC/OS-II 將中斷關了。被中斷了的 task 就進入了中斷服務態 (ISR)。回應中斷時,正在執行的 task 被暫停,ISR 控制了 CPU 的使用權。ISR 可能會報告一個或多個事件的發生,而使一個或多個 task 進入就緒態 (READY)。在這種情況下,由 ISR 返回之前,uC/OS-II 要判定,被中斷的 task 是否還是處於就緒態 (READY),且是 task 中優先權最高的。如果 ISR 使一個優先權更高的 task 進入了就緒態 (READY),則 ISR 返回時,就進入這個優先權更高且是就緒態 (READY) 的 task 執行,否則原來被中斷了的 task 會繼續執行。


所有的 task 都在等待事件發生或等待延遲時間結束,uC/OS-II 將會執行 idle task,執行OSTaskIdle()函式。




Task Control Blocks (TCB, OS_TCB)


一旦 task 建立了,task control block (OS_TCB) 將被賦予值 (程式清單 2.3)。TCB 是一個資料結構,當 task 的 CPU 使用權被剝奪時,uC/OS-II 用它來保存該 task 的狀態。當 task 重新得到 CPU 使用權時,TCB 能確保 task 從當時被中斷的那一點絲毫不差地繼續執行。OS_TCB 全部駐留在 RAM 中。task 建立的時候,OS_TCB 就被初始化了 (見 Task Management)。








程式清單 L2.3 Task Control Blocks



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 是指向當前 task 堆疊頂的指標。uC/OS-II 允許每個 task 有自己的堆疊,尤為重要的是,每個 task 的堆疊的容量可以是任意的。OSTCBStkPtr 是 OS_TCB 資料結構中唯一的一個能用組合語言來存取的變數 (在 task context-switching code 之中),把 OSTCBStkPtr 放在資料結構的最前面,使得從組合語言中處理這個變數時較為容易。


OSTCBExtPtr 指向使用者定義的 task 控制塊擴展。使用者可以擴展 task 控制塊而不必修改 uC/OS-II 的原始程式碼。 OSTCBExtPtr 只在函式 OstaskCreateExt() 中使用,故使用時要將 OS_TASK_CREAT_EN 設為 1,以允許建立 task 函式的擴展。例如使用者可以建立一個資料結構,這個資料結構包含每個 task 的名字,或跟蹤某個 task 的執行時間,或者跟蹤切換到某個 task 的次數。注意,將這個擴展指標變數放在緊跟著堆疊指標的位置,為的是當使用者需要在組合語言中存取這個變數時,從資料結構的頭來算偏移量比較方便。


OSTCBStkBottom 是指向 task 堆疊底的指標。如果微處理器的堆疊指標是遞減的,即堆疊記憶體從高地址向低地址方向分配,則 OSTCBStkBottom 指向 task 使用的堆疊空間的最低地址。如果微處理器的堆疊是從低位元址向高位址遞增型的,則 OSTCBStkBottom 指向 task 可以使用的堆疊空間的最高地址。函式 OSTaskStkChk() 要用到變數 OSTCBStkBottom,在執行中檢驗堆疊空間的使用情況。使用者可以用它來確定 task 實際需要的堆疊空間。這個功能只有當使用者在 task 建立時允許使用 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 時,這個變數才有效。 uC/OS-II 目前只支援 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(),在 task 建立的時候 task 堆疊檢驗功能得到了允許。OS_TASK_OPT_STK_CLR 表示 task 建立的時候 task 堆疊要清零。只有在使用者需要有堆疊檢驗功能時,才需要將堆疊清零。如果不定義 OS_TASK_OPT_STK_CLR,而後又建立、刪除了 task,堆疊檢驗功能報告的堆疊使用情況將是錯誤的。如果 task 一旦建立就決不會被刪除,而使用者初始化時,已將 RAM 清過零,則 OS_TASK_OPT_STK_CLR 不需要再定義,這可以節省程式執行時間。傳遞了 OS_TASK_OPT_STK_CLR 將增加 TaskCreateExt() 函式的執行時間,因為要將堆疊空間清零。堆疊容量越大,清零花的時間越長。最後一個選擇項 OS_TASK_OPT_SAVE_FP 通知 TaskCreateExt(),task 要做浮點運算。如果微處理器有硬體的浮點輔助運算器,則所建立的 task 在做 task 排程切換時,浮點暫存器的內容要保存。


OSTCBId 用於存儲 task 的識別碼。這個變數現在沒有使用,留給將來擴展用。


OSTCBNextOSTCBPrev 用於 task 控制塊 OS_TCBs 的雙重鏈結,該鏈表在 clock tick 函式 OSTimeTick() 中使用,用於刷新各個 task 的 task 延遲變數 OSTCBDly,每個 task 的 task control block (OS_TCB) 在 task 建立的時候被鏈結到鏈表中,在 task 刪除的時候從鏈表中被刪除。雙重連接的鏈表使得任一成員都能被快速插入或刪除。


OSTCBEventPtr 是指向事件控制塊的指標,後面的章節中會有所描述 (見 Intertask Communication & Synchronization)。


OSTCBMsg 是指向傳給 task 的消息的指標。用法將在後面的章節中提到 (見 Intertask Communication & Synchronization)。


OSTCBDly 當需要把 task 延時若干 clock tick 時要用到這個變數,或者需要把 task 暫停一段時間以等待某事件的發生,這種等待是有 time-out 限制的。在這種情況下,這個變數保存的是 task 允許等待事件發生的最多 clock tick。如果這個變數為 0,表示 task 不延時,或者表示等待事件發生的時間沒有限制。


OSTCBStat 是 task 的狀態。當 OSTCBStat 為 0,task 進入就緒態 (READY)。可以給 OSTCBStat 賦予其他的值,在檔案 uCOS_II.H 中有關於這個值的描述。


OSTCBPrio 是 task 優先權。高優先權 task 的 OSTCBPrio 值小。也就是說,這個值越小,task 的優先權越高。


OSTCBX, OSTCBY, OSTCBBitXOSTCBBitY 用於加速 task 進入就緒態 (READY) 的過程或進入等待事件發生狀態的過程 (避免在執行中去計算這些值)。這些值是在 task 建立時算好的,或者是在改變 task 優先權時算出的。這些值的演算法見程式清單 L2.4。








程式清單 L2.4 Task Control Blocks (OS_TCB) 中幾個成員的演算法



OSTCBY    = priority >> 3;
OSTCBBitY = OSMapTbl[priority >> 3];
OSTCBX = priority & 0x07;
OSTCBBitX = OSMapTbl[priority & 0x07];

OSTCBDelReq 是一個布林量,用於表示該 task 是否需要刪除,用法將在後面的章節中描述 (見Task Management)。


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



圖 2.2 List of free OS_TCBs




Ready List (就緒表)


每個 task 被賦予不同的優先權等級,從 0 級到最低優先權 OS_LOWEST_PRIO,包括 0 和 OS_LOWEST_PRIO 在內 (見檔案 OS_CFG.H)。當 uC/OS-II 初始化的時候,最低優先權 OS_LOWEST_PRIO 總是被賦給 idle task。注意,最多 task 數 OS_MAX_TASKS 和最低優先權數是沒有關係的。使用者的應用程式可以只有 10 個 task,而仍然可以有 32 個優先權的等級 (如果使用者將最低優先權數設為 31 的話)。


每個 task 的就緒態 (READY) 標誌會被都放入 ready list 中,而 ready list 是由 OSRedyGrp 和 OSRdyTbl[] 兩個變數所組成。在 OSRdyGrp 中,task 按優先權被分組 (8 個 task 為一組)。OSRdyGrp 中的每一個位元代表每一個群組中是否有任何一個 task 進入就緒態 (READY)。task 進入就緒態 (READY)時,ready list 的 OSRdyTbl[] 中的相對應元素的相對應位元也會被設置。ready list 的 OSRdyTbl[] 陣列的大小取決於 OS_LOWEST_PRIO (見檔案 OS_CFG.H)。當使用者的應用程式中 task 數目比較少時,減少 OS_LOWEST_PRIO 的值可以降低 uC/OS-II 對 RAM 的需求量。


為確定下次該那個優先權的 task 執行了,kernel 的排程器總是將 OS_LOWEST_PRIO 在 ready list 中相對應位元組的相對應位元設 1。 OSRdyGrp 和 OSRdyTbl[] 之間的關係見圖 2.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。


圖 2.3 uC/OS-II Ready List


程式清單 L2.5 中的程式是用於將 task 放入 ready list。Prio 是 task 的優先權。








程式清單 L2.5 Making a task ready-to-run



	OSRdyGrp            |= OSMapTbl[prio >> 3];
OSRdyTbl[prio >> 3] |= OSMapTbl[prio & 0x07];

可以看出,task 優先權的低三位元用於確定 task 在 total ready list 的 OSRdyTbl[] 中的所在位置。接下去的三個位元用於確定是在 OSRdyTbl[] 陣列的第幾個元素。OSMapTbl[] 是在 ROM 中的 (見檔案 OS_CORE.C)遮罩字,用於限制 OSRdyTbl[] 陣列的元素索引在 0 到 7 之間,見表 T3.1。





















T3.1 OSMapTbl[]


的值

Index



Bit Mask (Binary)



0



00000001



1



00000010



2


00000100


3


00001000


4


00010000


5


00100000


6


01000000


7


10000000


如果一個 task 被刪除了,則用程式清單 L3.6 中的程式碼做求反處理。








程式清單 L2.6 從 ready list 中刪除一個 task



	if ((OSRdyTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0)
OSRdyGrp &= ~OSMapTbl[prio >> 3];

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








程式清單 L2.7 找出進入 READY 且優先權最高的 task



	y    = OSUnMapTbl[OSRdyGrp];	/* determine Y position in OSRdyTbl[] */
x = OSUnMapTbl[OSRdyTbl[y]]; /* determine X position in OSRdyTbl[Y]*/
prio = (y << 3) + x;

例如,如果 OSRdyGrp 的值為二進位 01101000,查 OSUnMapTbl[OSRdyGrp] 得到的值是 3,它相對應於 OSRdyGrp 中的第 3 位 bit3,這裏假設最右邊的一位是第 0 位 bit0。類似地,如果 OSRdyTbl[3] 的值是二進位 11100100,則 OSUnMapTbl[OSRdyTbc[3]] 的值是 2,即第 2 位。於是 task 的優先權 Prio 就等於 26 (3*8+2)。利用這個優先權的值。查 task 控制塊優先權表 OSTCBPrioTbl[],得到指向相對應 task 的 task 控制塊 OS_TCB 的工作就完成了。


Task Scheduling (任務排程)


uC/OS-II 總是執行進入就緒態 (READY) task 中優先權最高的那一個。確定哪個 task 優先權最高,下次該哪個 task 執行了的工作是由排程器 (Scheduler) 完成的。 task 級的排程是由函式 OSSched() 完成的。中斷級的排程是由另一個函式 OSIntExit() 完成的,這個函式將在以後描述。OSSched() 的程式碼如程式清單 L2.8 所示。uC/OS-II task 排程所花的時間是常數,與應用程式中建立的 task 數無關。








程式清單 L2.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();
}

如程式清單中 [L2.8(1)] 條件語句的條件不滿足, task 排程函式 OSSched() 將退出,不做 task 排程。這個條件是:如果在 ISR 中呼叫 OSSched(),此時中斷巢狀層數 OSIntNesting > 0,或者由於使用者至少呼叫了一次給 task 排程上鎖函式 OSSchedLock(),使 OSLockNesting > 0。如果不是在 ISR 呼叫 OSSched(),並且 task 排程是允許的,即沒有上鎖,則 task 排程函式將找出那個進入就緒態 (READY) 且優先權最高的 task [L2.8(2)],進入就緒態 (READY) 的 task 在就緒 task 表中有相對應的位置位。一旦找到那個優先權最高的 task ,OSSched() 檢驗這個優先權最高的 task 是不是當前正在執行的 task,如此可以避免不必要的 task 排程 [L2.8(3)]。注意,在第一版的 uC/OS 中曾經是先得到 OSTCBHighRdy 然後和 OSTCBCur 做比較。因為這個比較是兩個指標型變數的比較,在 8 位元和一些 16 位元微處理器中這種比較相對較慢。而在 uC/OS-II 中是兩個整數的比較。並且,除非使用者實際需要做 task 切換,在查 task 控制塊優先權表 OSTCBPrioTbl[] 時,不需要用指標變數來查 OSTCBHighRdy。綜合這兩項改進,即用整數比較代替指標的比較和當需要 task 切換時再查表,使得 uC/OS-II 比 uC/OS 在 8 位元和一些 16 位元微處理器上要更快一些。


為實現 task 切換,OSTCBHighRdy 必須指向優先權最高的那個 TCB (OS_TCB),這是通過將以 OSPrioHighRdy 為索引的 OSTCBPrioTbl[] 陣列中的那個元素賦給 OSTCBHighRdy 來實現的 [L2.8(4)]。接著,統計計數器 OSCtxSwCtr 加 1,以跟蹤 task 切換次數 [L2.8(5)]。最後巨集呼叫 OS_TASK_SW() 來完成實際上的 task 切換 [L2.8(6)]。


task 切換很簡單,由以下兩步完成,將被暫停 task 的微處理器暫存器推入堆疊,然後將較高優先權的 task 的暫存器值從堆疊中恢復到暫存器中。在 uC/OS-II 中,就緒 task 的堆疊結構總是看起來跟剛剛發生過中斷一樣,所有微處理器的暫存器都保存在堆疊中。換句話說,uC/OS-II 執行就緒態 (READY) 的 task 所要做的一切,只是恢復所有的 CPU 暫存器並執行中斷返回指令。為了做 task 切換,執行 OS_TASK_SW(),人為的模仿了一次中斷。多數微處理器有軟中斷指令或者 陷阱指令 (TRAP) 來實現上述操作。ISR 或陷阱處理 (Trap hardler) ,也稱作例外處理 (exception handler),必須提供中斷向量給組合語言函式 OSCtxSw()。OSCtxSw() 除了需要 OS_TCBHighRdy 指向即將被切入的 task ,還需要讓當前 TCB 指標 (OSTCBCur) 指向即將被暫停的 task,參見 porting uC/OS-II 一文,有關於 OSCtxSw() 的更詳盡的解釋。目前,你只需要知道 OS_TASK_SW() 是要將目前低優先權的 task 進入暫停狀態,以及讓更重要的 task 重新開始執行。


OSSched() 的所有程式碼都屬 critical section 程式碼。在尋找進入 READY 且優先權最高的 task 過程中,為防止 ISR 把一個或幾個 task 的 ready 位元設置,所以中斷是被關掉的。為縮短切換時間,OSSched() 全部程式碼都可以用組合語言寫。但為增加可讀性,可攜性和將組合語言程式碼最少化,所以 OSSched() 是用 C 寫的。




Locking and UnLocking the Scheduler


給 scheduler 上鎖函式 OSSchedlock() (程式清單 L2.9) 用於禁止 task 排程,直到 task 完成後呼叫給排程器開鎖函式 OSSchedUnlock() 為止,(程式清單 L2.10)。呼叫 OSSchedlock() 的 task 保持對 CPU 的控制權,儘管有個優先權更高的 task 進入了就緒態 (READY)。然而,此時中斷是可以被識別的,中斷服務也能得到 (假設中斷是開著的)。OSSchedlock() 和 OSSchedUnlock() 必須成對使用。變數 OSLockNesting 跟蹤 OSSchedLock() 函式被呼叫的次數,以允許巢狀的函式包含 critical section 程式碼,這段程式碼其他 task 不得干預。uC/OS-II 允許巢狀深度達 255 層。當 OSLockNesting 等於零時,排程重新得到允許。函式 OSSchedLock() 和 OSSchedUnlock() 的使用要非常謹慎,因為它們影響 uC/OS-II 對 task 的正常管理。


當 OSLockNesting 減到零的時候,OSSchedUnlock() 呼叫 OSSched [L2.10(2)]。OSSchedUnlock() 是被某 task 呼叫的,在 scheduler 上鎖的期間,可能有什麼事件發生了並使一個更高優先權的 task 進入就緒態 (READY)。


呼叫 OSSchedLock() 以後,使用者的應用程式不得使用任何能將現行 task 暫停的系統呼叫。也就是說,使用者程式不得呼叫 OSMboxPend()、OSQPend()、OSSemPend()、OSTaskSuspend (OS_PRIO_SELF)、OSTimeDly() 或 OSTimeDlyHMSM(),直到 OSLockNesting 回零為止。因為排程器上了鎖,使用者就鎖住了系統,任何其他 task 都不能執行。


當低優先權的 task 要發消息給多工的郵箱、消息佇列、信號量時(見 task 間通訊和同步),使用者不希望高優先權的 task 在郵箱、佇列和信號量沒有得到消息之前就取得了 CPU 的控制權,此時,使用者可以使用禁止排程器函式。








程式清單 L2.9 Locking the Scheduler



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









程式清單 L2.10 UnLocking the Scheduler



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


uC/OS-II 總是會建立一個 idle task,這個 task 在沒有其他 task 進入就緒態 (READY) 時會進入執行。這個 idle task [OSTaskIdle()] 永遠設為最低優先權,即 OS_LOWEST_PRI0。idle task [OSTaskIdle()] 什麼也不做,只是在不停地給一個 32 位元的名叫 OSIdleCtr 的計數器加 1,statistics task 則使用這個計數器以確定現行應用軟體實際消耗的 CPU 時間。程式清單 L2.11 是 idle task 的程式碼。在計數器加 1 前後,中斷是先關掉再開啟的,因為 8 位元以及大多數 16 位元微處理器的 32 位元元加 1 需要多條指令,為防止高優先權的 task 或 ISR 從中打斷。此外,idle task 不能被應用程式刪除。








程式清單 L2.11 uC/OS-II 的 idle task



void OSTaskIdle (void *pdata)
{
pdata = pdata;

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



Statistics Task


uC/OS-II 有一個提供執行時間做統計的 task 。這個 task 叫做 OSTaskStat(),如果使用者將系統定義常數 OS_TASK_STAT_EN ( 見檔案 OS_CFG.H) 設為 1,這個 task 就會建立。一旦得到了允許,OSTaskStat() 每秒鐘執行一次 (見檔案 OS_CORE.C),計算當前 CPU 的 usage。換句話說,OSTaskStat() 會告訴使用者應用程式使用了多少 CPU 時間,用百分比表示,這個值放在一個有號 8 位元整數 OSCPUsage 中,精度是 1%。


如果使用者的應用程式打算使用 statistics task,使用者必須在初始化時建立一個唯一的 task,在這個 task 中呼叫 OSStatInit() (見檔案 OS_CORE.C)。換句話說,在呼叫系統啟動函式 OSStart() 之前,使用者初始程式碼必須先建立一個 task,在這個 task 中呼叫系統統計初始化函式 OSStatInit(),然後再建立應用程式中的其他 task 。程式清單 L2.12 是 statistics task 的 pseudo-code 程式碼。








程式清單 L2.11 uC/OS-II 的 idle task



void main (void)
{
OSInit(); /* 初始化 uC/OS-II (1) */

/* 安裝uC/OS-II的 task 切換向量 */
/* 建立使用者起始 task (為了方便討論,這裏以 TaskStart() 作為起始 task) (2) */

OSStart(); /* 開始多工排程 (3) */
}


void TaskStart (void *pdata)
{
/* 安裝並啟動uC/OS-II的時鐘節拍 (4) */

OSStatInit(); /* 初始化 statistics task (5) */

/* 創立使用者應用程式 task */

for ( ; ; ) {
/* 這裏是 TaskStart() 的程式碼 */
}
}

因為使用者的應用程式必須先建立一個啟始 task [TaskStart()],當主程序 main() 呼叫系統啟動函式 OSStart() 的時候, uC/OS-II 只有 3 個要管理的 task :TaskStart()、OSTaskIdle() 和 OSTaskStat()。請注意,task TaskStart() 的名稱是無所謂的,叫什麼名字都可以。因為 uC/OS-II 已經將 idle task 的優先權設為最低,即 OS_LOWEST_PRI0,statistics task 的優先權設為次低, OS_LOWEST_PRI0-1。啟動 task [TaskStart()] 總是優先權最高的 task 。


圖 F2.4 解釋初始化 statistics task 時的流程。使用者必須先呼叫的是 uC/OS-II 中的系統初始化函式 OSInit(),該函式初始化 uC/OS-II [圖 F2.4(2)]。有的處理器 (例如 Motorola 的 MC68HC11),不需要 "設置" 中斷向量,中斷向量已經在 ROM 中有了。使用者必須呼叫 OSTaskCreat() 或者 OSTaskCreatExt() 以建立 TaskStart() [圖 F2.4(3)]。進入多工的條件準備好了以後,呼叫系統啟動函式 OSStart()。這個函式將使 TaskStart() 開始執行,因為 TaskStart() 是優先權最高的 task [圖F2.4(4)]。



圖 F2.4統計 task 的初始化


TaskStart() 負責初始化和啟動 clock tick [圖 F2.4(5)]。在這裏啟動 clock tick 是必要的,因為使用者不會希望在多工還沒有開始時就接收到 clock tick 的中斷。接下去 TaskStart() 呼叫統計初始化函式 OSStatInit() [圖 F2.4(6)]。 統計初始化函式 OSStatInit() 決定在沒有其他應用 task 執行時,空閒計數器 (OSIdleCtr) 的計數有多快。以奔騰 II 微處理器執行在 333MHz 時,加 1 操作可以使該計數器的值達到每秒 15,000,000 次。OSIdleCtr 的值離 32 位元計數器的溢位極限值 4,294,967,296 還差得遠。所以微處理器越來越快,使用者要注意這裏可能會是將來的一個潛在問題。就是空閒計數器溢位


系統統計初始化 task 函式 OSStatInit() 呼叫延遲函式 OSTimeDly() 將自身延時 2 個 clock tick 之後就停止自身的執行 [ 圖 F2.4(7)]。這是為了使 OSStatInit() 與 clock tick 同步。uC/OS-II 然後選下一個優先權最高的進入就緒態 (READY) 的 task 執行,這恰好是統計 task OSTaskStat()。我們會在後面讀到 OSTaskStat() 的程式碼,但現在粗略的看一下,OSTaskStat() 所要做的第一件事就是查看統計 task 就緒標誌是否為 "FALSE";,如果是的話,也要延時兩個時鐘節拍 [圖 F2.4(8)]。一定會是這樣,因為標誌 OSStatRdy 已被 OSInit() 函式初始化為 "FALSE";,所以實際上 OSTaskStat 也將自己推入休眠態 (Sleep) 兩個時鐘節拍 [圖 F2.4(9)]。於是 task 切換到 idle task ,OSTaskIdle() 開始執行,這是唯一一個就緒態 (READY) task 了。CPU 處在空閒 task OSTaskIdle() 中,直到 TaskStart() 的延遲兩個時鐘節拍完成 [圖 F2.4(10)]。兩個時鐘節拍之後,TaskStart() 恢復執行 [圖 F2.4(11)]。在執行 OSStartInit() 時,空閒計數器 OSIdleCtr 被清零 [圖F2.4(12)]。然後,OSStatInit() 將自身延時整整一秒 [圖 F2.4(13)]。因為沒有其他進入就緒態 (READY)的 task ,OSTaskIdle() 又獲得了 CPU 的控制權 [圖 F2.4(14)]。一秒鐘以後,TaskStart() 繼續執行,還是在 OSStatInit() 中,空閒計數器將 1 秒鐘內計數的值存入空閒計數器最大值 OSIdleCtrMax 中 [圖 F2.4(15)]。


OSStarInit() 將統計 task 就緒標誌 OSStatRdy 設為 "TRUE" [圖F2.4 (16) ],以此來允許兩個 clock tick 以後 OSTaskStat() 開始計算 CPU 的 usage。


統計 task 的初始化函式 OSStatInit() 的程式碼如程式清單 L2.13 所示。








程式清單 L2.13 statistics task 的初始化



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();
}

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



[3.1] 運算式 Need to typeset the equation.


一旦上述計算完成,OSTaskStat() 呼叫 task 統計外界接入函式 OSTaskStatHook() [L2.14(3)],這是一個使用者可定義的函式,這個函式能使統計 task 得到擴展。這樣,使用者可以計算並顯示所有 task 總的執行時間,每個 task 執行時間的百分比以及其他資訊。








程式清單 L2.14 statistics task



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);
}
}



uC/OS 中的中斷處理


在 uC/OS 中,ISR 要用組合語言來寫。然而,如果使用者使用的 C 語言編譯器支援 inline 組合語言的話,使用者可以直接將 ISR 的程式碼放在 C 語言的程式檔案中。ISR 的 pseudo-code 如程式清單 L2.15 所示。








程式清單 L2.15 uC/OS-II 中的 ISR



使用者中斷服務副程式:
保存全部 CPU 暫存器; (1)
呼叫 OSIntEnter 或 OSIntNesting 直接加 1; (2)
執行使用者程式碼做中斷服務; (3)
呼叫 OSIntExit(); (4)
恢復所有 CPU 暫存器; (5)
執行中斷返回指令; (6)

使用者程式碼應該將全部 CPU 暫存器推入當前 task 堆疊 [L2.15(1)]。注意,有些微處理器,例如 Motorola 68020 (及 68020 以上的微處理器),做中斷服務時使用另外的堆疊。


uC/OS-II 可以用在這類微處理器中,當 task 切換時,暫存器是保存在被中斷了的那個 task 的堆疊中的。


uC/OS-II 需要知道使用者在做中斷服務,故使用者應該呼叫 OSIntEnter(),或者將全域變數 OSIntNesting [L2.15(2)] 直接加 1,如果使用者使用的微處理器有記憶體直接加 1 的單條指令的話就可以只使用後者。如果使用者使用的微處理器沒有這樣的指令,必須先將 OSIntNesting 讀入暫存器,再將暫存器加 1,然後再寫回到變數 OSIatNesting 中去,就不如呼叫 OSIntEnter()。 OSIntNesting 是共用資源。OSIntEnter() 把上述三條指令用開中斷、關中斷保護起來,以保證處理 OSIntNesting 時的排它性。直接給 OSIntNesting 加 1 比呼叫 OSIntEnter() 快得多,可能的話,直接加 1 更好。要當心的是,在有些情況下,從 OSIntEnter() 返回時,會把中斷開了。遇到這種情況,在呼叫 OSIntEnter() 之前要先清除中斷源,否則,中斷將連續反覆進入,使用者應用程式就會崩潰!


上述兩步完成以後,使用者可以開始服務於叫中斷的設備了 [L2.15(3)]。這一段完全取決於應用。uC/OS-II 允許中斷巢狀,因為 uC/OS-II 會跟蹤巢狀層數 (OSIntNesting)。然而,為允許中斷巢狀,在多數情況下,使用者應在開中斷之前必須先清中斷源。


呼叫脫離中斷函式 OSIntExit() [L2.15(4)] 表示著中斷服務副程式的終結,OSIntExit() 將中斷巢狀層數計數器減 1。當巢狀計數器減到零時,所有中斷,包括巢狀的中斷就都完成了,此時 uC/OS-II 要判定有沒有優先權較高的 task 被 ISR (或任一巢狀的中斷) 喚醒了。如果有優先權高的 task 進入了就緒態 (READY),uC/OS-II 就返回到那個高優先權的 task,OSIntExit() 返回到呼叫點 [L2.15(5)]。保存的暫存器的值是在這時恢復的,然後是執行中斷返回指令 [L2.16(6)]。注意,如果排程被禁止了 (OSIntNesting > 0) ,uC/OS-II 將被返回到被中斷了的 task。


以上描述的詳細的解釋如圖 F2.5 所示。中斷來到了 [F2.5(1)] 但還不能被 CPU 識別,也許是因為中斷被 uC/OS-II 或使用者應用程式關了,或者是因為 CPU 還沒執行完當前指令。一旦 CPU 回應了這個中斷 [F2.5(2)],CPU 的中斷向量 (至少大多數微處理器是如此) 跳轉到 ISR [F2.5(3)]。如上所述,ISR 需保存 CPU 的暫存器 (也叫做 CPU context) [F2.5(4)],一旦做完,使用者的 ISR 通知 uC/OS-II 進入 ISR 了,辦法是呼叫 OSIntEnter() 或者給 OSIntNesting 直接加 1 [F2.5(5)]。然後使用者中斷服務程式碼開始執行 [F2.5(6)]。使用者中斷服務中做的事要盡可能地少,要把大部分工作留給 task 去做。ISR 通知某 task 去做事的手段是呼叫以下函式之一:OSMboxPost(),OSQPost(),OSQPostFront(),OSSemPost()。中斷發生並由上述函式發出消息時,接收到消息的 task 可能是,也可能不是暫停在郵箱、佇列或信號量上的 task 。使用者中斷服務完成以後,要呼叫 OSIntExit() [F2.5(7)]。從時序圖上可以看出,對被中斷了的 task 說來,如果沒有高優先權的 task 被 ISR 啟動而進入就緒態 (READY),OSIntExit() 只佔用很短的執行時間。進而,在這種情況下,CPU 暫存器只是簡單地恢復 [F2.5(8)] 並執行中斷返回指令 [F2.5(9)]。如果 ISR 使一個高優先權的 task 進入了就緒態 (READY),則 OSIntExit() 將佔用較長的執行時間,因為這時要做 task 切換 [F2.5(10)]。新 task 的暫存器內容要恢復並執行中斷返回指令 [F2.5(12)]。



圖 F2.5 中斷服務




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








程式清單 L2.16 通知 uC/OS-II,ISR 開始了



void OSIntEnter (void)
{
OS_ENTER_CRITICAL();
OSIntNesting++;
OS_EXIT_CRITICAL();
}









程式清單 L2.17 通知 uC/OS-II,脫離了中斷服務



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 [L2.17(2)] 而排程函式 OSSched() 的排程條件是:中斷巢狀層數計數器和鎖定巢狀計數器 (OSLockNesting) 二者都必須是零。第二個不同點是, OSRdyTbl[] 所需的檢索值 Y 是保存在全域變數 OSIntExitY 中的 [L2.17(3)]。這是為了避免在 task 堆疊中安排自動變數。這個變數在哪兒和中斷 task 切換函式 OSIntCtxSw() 有關。最後一點,如果需要做 task switch,OSIntExit() 將呼叫 OSIntCtxSw() [L2.17(4)] 而不是呼叫 OS_TASK_SW(),正像在 OSSched() 函式中那樣。


在這呼叫中斷切換函式 OSIntCtxSw() 而不呼叫 task 切換函式 OS_TASK_SW(),有兩個原因,首先是,如程式清單中 L2.5(1) 和圖 F2.6(1) 所示,一半的工作,即 CPU 暫存器入堆疊的工作已經做完了。第二個原因是,在 ISR 中呼叫 OSIntExit() 時,將返回位址推入了堆疊 [L2.15(4) 和 F2.6(2)]。OSIntExit() 中的進入 critical section 函式 OS_ENTER_CRITICAL() 或許將 CPU 的狀態字組也推入了堆疊 L2.7(1) 和 F2.6(3)。這取決於中斷是怎麼被關掉的 (見porting uC/OS-II)。最後,呼叫 OSIntCtxSw() 時的返回位址又被推入了堆疊 [L2.17(4) 和 F2.1(4)],除了堆疊中不相關的部分,當 task 暫停時,堆疊結構應該與 uC/OS-II 所規定的完全一致。OSIntCtxSw() 只需要對堆疊指標做簡單的調整,如圖 F2.6(5) 所示。換句話說,調整堆疊結構要是保證所有暫停的 task 的堆疊結構看起來是一樣的。



圖 F2.6 中斷中的 task 切換函式 OSIntCtxSw() 調整堆疊結構


有的微處理器,像 Motorola 68HC11 中斷發生時 CPU 暫存器是自動入堆疊的,且要想允許中斷巢狀的話,在 ISR 中要重新開中斷,這可以視作一個優點。的確,如果使用者 ISR 執行得非常快,使用者不需要通知 task 自身進入了中斷服務,只要不在中斷服務期間開中斷,也不需要呼叫 OSIntEnter() 或 OSIntNesting 加 1。程式清單 L3.18 中的 pseudo-code 表示這種情況。一個 task 和這個 ISR 通訊的唯一方法是通過全域變數。








程式清單 L2.18 Motorola 68HC11 中的 ISR



M68HC11_ISR:                    /* 快中斷服務程式,必須禁止中斷 */
所有暫存器被 CPU 自動保存;
執行使用者程式碼以回應中斷;
執行中斷返回指令;



Clock Ticks (時鐘節拍)


uC/OS-II 需要使用者提供周期性信號源,用於實現時間延時和確認超時。節拍率應在每秒 10 次到 100 次之間,或者說 10 到 100Hz。時鐘節拍率越高,系統的額外負荷就越重。clock tick 的實際頻率取決於使用者應用程式的精度。 clock tick 的來源可以是專門的硬體計時器,也可以是來自 50/60Hz 交流電源的信號。


使用者必須在多工系統啟動以後再開啟時 clock tick,也就是在呼叫 OSStart()之後。 換句話說,在呼叫 OSStart() 之後做的第一件事是初始化計時器中斷。通常,容易犯的錯誤是將允許 clock tick 中斷放在系統初始化函式 OSInit() 之後,在調啟動多工系統啟動函式 OSStart() 之前,如程式清單 L2.19 所示。








程式清單 L2.19 啟動時鐘就節拍器的不正確做法



void main(void)
{
.
.
OSInit(); /* 初始化 uC/OS-II */
.
.
/* 應用程式初始化程式碼 ... */

/* ... 通過呼叫 OSTaskCreate() 建立至少一個 task */
.
.
允許時鐘節拍 (TICKER) 中斷; /* 千萬不要在這裏允許時鐘節拍中斷! */
.
.
OSStart(); /* 開始多工排程 */
}

這裏潛在地危險是,clock tick 中斷有可能在 uC/OS-II 啟動第一個 task 之前發生,此時 uC/OS-II 是處在一種不確定的狀態之中,使用者應用程式有可能會 crash。


uC/OS-II 中的 clock tick 服務是通過在 ISR 中呼叫 OSTimeTick() 實現的。clock tick 中斷服從所有前面章節中描述的規則。 tick ISR 的 pseudo-code 如程式清單 L2.20 所示。這段程式碼必須用組合語言編寫,因為在 C 語言裏不能直接處理 CPU 的暫存器。








程式清單 L2.20 Pseudo code for Tick ISR



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

時鐘節拍函式 OSTimeTick() 的程式碼如程式清單 L2.21 所示。OSTimtick() 以呼叫可由使用者定義的時鐘節拍外連函式 OSTimTickHook() 開始,這個外連函式可以將時鐘節拍函式 OSTimtick() 予以擴展 [L2.2(1)]。uC/OS-II 決定首先呼叫 OSTimTickHook() 是打算在 clock tick 中斷服務一開始就給使用者一個可以做點兒什麼的機會,因為使用者可能會有一些時間要求苛刻的工作要做。OSTimtick() 中最大量的工作是給每個使用者 TCB (OS_TCB) 中的時間延時變數 OSTCBDly 減 1 (如果該項不為零的話)。OSTimTick() 會由 OSTCBList 開始,沿著 OS_TCB link list 往下做,一直做到 idle task [L2.21(3)]。當某個 task 的 TCB 中的時間延時項 OSTCBDly 減到了零,這個 task 就進入了就緒態 (READY) [L2.21(5)]。而被 task 暫停函式 OSTaskSuspend() 暫停的 task 則不會進入就緒態 (READY) [L2.21(4)]。OSTimTick() 的執行時間直接與應用程式中建立了多少個 task 成正比。








程式清單 L2.21 Code to service a tick



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() [L2.21(7)] 累加從開機以來的時間,用的是一個無號數 32 位元變數。注意,在給 OSTime() 加 1 之前使用了關中斷,因為多數微處理器給 32 位元數加 1 的操作都得使用較多條指令。


中斷服務副程式似乎就得寫這麼長,如果使用者不喜歡將中斷服務程式寫這麼長,可以從將 OSTimeTick() 由 ISR 中移至 task 裡來呼叫,如程式清單 L2.22 所示。要想這麼做,得建立一個高於應用程式中所有其他 task 優先權的 task。 Tick ISR 利用 semaphore 或 message mailbox 發信號給這個高優先權的 task 。








程式清單 L2.22 Code to service a tick



void TickTask (void *pdata)
{
pdata = pdata;

for ( ; ; ) {
OSMboxPend(...); /* 等待從 tick ISR 發來的信號 */
OSTimeTick();
}
}

使用者當然需要先建立一個 mailbox (初始化成 NULL) 用於發信號給上述任何告知 tick ISR 已經發生了 (程式清單 L2.23)。








程式清單L2.23 Code to service a tick



void OSTickISR(void)
{
保存處理器暫存器的值;
呼叫 OSIntEnter() 或是將 OSIntNesting 加 1;
.
發送一個 'dummy' message (例如, (void *) 1) 到 tick mailbox;
.
呼叫 OSIntExit();
恢復處理器暫存器的值;
執行中斷返回指令;
}



uC/OS-II Initialization


在呼叫 uC/OS-II 的任何其他服務之前,uC/OS-II 要求使用者首先呼叫系統初始化函式 OSIint()。OSIint() 初始化 uC/OS-II 所有的變數和資料結構 (見 OS_CORE.C)。


OSInit() 建立 idle task,這個 task 總是處於就緒態 (READY) 的。idle task [OSTaskIdle()] 的優先權總是設成最低,即 OS_LOWEST_PRIO。如果 OS_TASK_STAT_EN 和 OS_TASK_CREATE_EXT_EN 都被設為 1 (see OS_CFG.H),則 OSInit() 還會建立 statistic task [OSTaskStat()] 並且讓其進入就緒態 (READY)。OSTaskStat() 的優先權總是設為 OS_LOWEST_PRIO-1。


圖 F2.7 表示呼叫 OSInit() 之後,一些 uC/OS-II 變數和資料結構之間的關係。其解釋是基於以下假設的:




  • 在檔案 OS_CFG.H 中,OS_TASK_STAT_EN 是設為 1 的。



  • 在檔案 OS_CFG.H 中,OS_LOWEST_PRIO 是設為 63 的。



  • 在檔案 OS_CFG.H 中,OS_MAX_TASKS 是設成大於 2 的。




圖 F2.7 Data structures after calling OSInit()


這兩個 task 的 TCB (OS_TCB) 是用雙向 link list 連接在一起的。OSTCBList 指向這個 link list 的啟始處。當建立一個 task 時,這個 task 總是被放在這個 link list 的啟始處。換句話說,OSTCBList 總是指向最後建立的那個 task。list 的終點指向空字元 NULL (也就是零)。


因為這兩個 task 都處在就緒態 (READY),在 ready list 的 OSRdyTbl[] 中的相對應位是設為 1 的。還有,因為這兩個 task 的相對應位元是在 OSRdyTbl[] 的同一行上,即屬同一組,故 OSRdyGrp 中只有 1 個位元是被設為 1 的。


uC/OS-II 還初始化了 4 個空的資料結構緩衝區,如圖 F2.8 所示。每個緩衝區都是單向 link list,允許 uC/OS-II 從緩衝區中迅速得到或釋放一個緩衝區中的元素。注意,空的 TCB 在空緩衝區中的數目取決於 OS_MAX_TASKS 的設定值,這個 OS_MAX_TASKS 是在 OS_CFG.H 檔案中定義的。uC/OS-II 自動安排 OS_N_SYS_TASKS (見檔案 uCOS-II.H)。TCB (OS_TCB) 的數目也就自動確定了。當然,包括足夠的 TCB 分配給 statistic task 和 idle task。指向空事件表 OSEventFreeList 和空佇列表 OSQFreeList 的指標將在後面 Intertask Communication & Synchronization 中討論。指向空的存儲區的指標表 OSMemFreeList 將在後面 Memory Management 中討論。



圖 F2.8 Free Pools




Starting uC/OS-II


多工的啟動是透過使用者呼叫 OSStart() 實現的。然而,啟動 uC/OS-II 之前,使用者至少要建立一個應用 task ,如程式清單 L2.24 所示。








程式清單 L2.24 Initializing and Starting uC/OS-II



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

OSStart() 的程式碼如程式清單 L2.25 所示。當呼叫 OSStart() 時,OSStart() 從 ready list 中找出那個使用者建立的優先權最高 task 的 TCB [L2.25(1)]。然後,OSStart() 呼叫高優先權就緒 task 啟動函式 OSStartHighRdy() [L3.25(2)],(見組合語言檔案 OS_CPU_A.ASM),這個檔案與選擇的微處理器有關。實質上,函式 OSStartHighRdy() 是將 task 堆疊中保存的值存回到 CPU 暫存器中,然後執行一條中斷返回指令,中斷返回指令強制執行該 task 程式碼。高優先權就緒 task 啟動函式 OSStartHighRdy()。注意,OSStartHighRdy() 將永遠不返回到 OSStart()。








程式清單 L2.25 Starting multitasking



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)
}
}

多工啟動以後變數與資料結構中的內容如圖 F2.9 所示。這裏假設使用者建立的 task 優先權為 6,注意,OSTaskCtr指出已經建立了 2 個 task。OSRunning 已設為 "TRUE",指出多工已經開始,OSPrioCur 和 OSPrioHighRdy 存放的是使用者應用 task 的優先權,OSTCBCur 和 OSTCBHighRdy 二者都指向使用者 task 的 TCB。



圖 F2.9 Variables and Data Structures after calling OSStart()




Obtaining uC/OS-II's version


應用程式呼叫 OSVersion() [程式清單 L2.26] 可以得到當前 uC/OS-II 的版本碼。OSVersion() 函式返回版本號值乘以 100。換言之,200 表示版本碼是 2.00。








程式清單 L2.26 Getting uC/OS-II's version.



INT16U OSVersion (void)
{
return (OS_VERSION);
}



OSEvent???() functions


目前有 4 個 OS_CORE.C 中的函式沒有在本篇中提到。這 4 個函式是 OSEventWaitListInit(),OSEventTaskRdy(), OSEventTaskWait(),OSEventTO()。這幾個函式是放在檔案 OS_CORE.C 中的,而如何使用這些函式到 Intertask Communication & Synchronization 再討論。


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 立你斯 的頭像
    立你斯

    立你斯學習記錄

    立你斯 發表在 痞客邦 留言(0) 人氣()