Understanding uC/OS-II (5)
-- Intertask Communication & Synchronization
在 uC/OS-II 中,有多種方法可以保護 task 之間的共用資料和提供 task 之間的溝通。在前面的文章中,已經講到了其中的兩種:
一是利用巨集 OS_ENTER_CRITICAL() 和 OS_EXIT_CRITICAL() 來關閉中斷和打開中斷。當兩個 task 或者一個 task 和一個 ISR 共用某些資料時,可以採用這種方法,詳見 critical section、OS_ENTER_CRITICAL() 和 OS_EXIT_CRITICAL() 及檔案 OS_CPU.H。
二是利用函式 OSSchedLock() 和 OSSchekUnlock() 對 uC/OS-II 中的 task 排程器上鎖和開鎖。用這種方法也可以實現資料的共用,詳見 Locking and Unlocking the Scheduler。
本篇將介紹另外三種用於資料共用和 task 溝通的方法: Semaphores, Message mailboxes and Message queues。
圖 F6.1 畫出了 task 和 ISR 之間是如何進行溝通的。
Figure 6.1 Use of Event Control Blocks
一個 task 或者 ISR 可以透過 ECB (Event Control Blocks, ECB) 來向另外的 task 發出 signal [F6.1A(1)]。這裏,所有的信號都被看成是事件 (Event)。這也說明為什麼上面把用於溝通的資料結構叫做 ECB (ECB)。一個 task 還可以等待另一個 task 或 ISR 給它發送信號 [F6.1A(2)]。這裏要注意的是,只有 task 可以等待事件的發生,ISR 是不能這樣做的。對於處於等待狀態的 task,還可以給它指定一個最長的等待時間,以此來防止因為等待的事件沒有發生而無限期地等下去 [F6.1A(3)]。
多個 task 可以同時等待同一個事件的發生 [F6.1B]。在這種情況下,當該事件發生後,所有等待該事件的 task 中,優先權最高的 task 得到了該事件並進入就緒狀態,準備執行。上面講到的事件,可以是 semaphore,message mailbox 或者 message queues。
當 ECB 被使用如同是一個 semaphore 時,task 可以等待它,也可以給它發送 signal [F6.1C(4)]。
Event Control Blocks
uC/OS-II 透過 uCOS_II.H 中定義 OS_EVENT 的資料結構來維護一個 ECB 所有資訊 [程式清單 L6.1],也就是本篇開始時提到的 ECB。該結構中除了包含了事件本身的定義,如用於 semaphore 的計數器,用於指向 mailbox 的指標,以及指向 message queue 的指標陣列等,還定義了等待該事件的所有 task 的列表。程式清單 L6.1 是該資料結構的定義。
程式清單 L6.1 Event Control Block data structure |
typedef struct { |
.OSEventType 定義了事件的具體類型。它可以是 semaphore (OS_EVENT_SEM)、mailbox (OS_EVENT_TYPE_MBOX) 或 message queue (OS_EVENT_TYPE_Q) 中的一種。使用者要根據該欄位的值來呼叫相對應的系統函式,以保證對其進行的操作的正確性。
.OSEventPtr 事件指標,只有在所定義的事件是 mailbox 或者 message queue 時才使用。當所定義的事件是 mailbox 時,它指向一個消息,而當所定義的事件是 message queue 時,它指向一個資料結構。
.OSEventTbl[] 和 .OSEventGrp 很像前面講到的 OSRdyTbl[] 和 OSRdyGrp,只不過前兩者包含的是等待某事件的 task,而後兩者包含的是系統中處於 READY 狀態的 task。
.OSEventCnt 當事件是一個 semaphore 時,OSEventCnt 是用於 semaphore 的計數器。
每個等待事件發生的 task 都被加入到該事件的 ECB 中的 wait list 中,該 list 包括 .OSEventGrp 和 .OSEventTbl[] 兩個欄位。在這裏,所有的 task 的優先權被分成 8 個群組 (每群組有 8 個優先權),分別相對應 .OSEventGrp 中的 8 個位元。當某群組中有 task 處於等待該事件的狀態時,.OSEventGrp 中相對應的位元就被設置。相相對應的,該 task 在 .OSEventTbl[] 中的相對應欄位也被設置。 .OSEventTbl[] 陣列的大小由系統中 task 的最低優先權決定,這個值由 uCOS_II.H 中的 OS_LOWEST_PRIO 常數定義。這樣,所以在 task 優先權比較少的情況下,可以減少 uC/OS-II 對系統 RAM 的占用量。
當一個事件發生後,該事件的 event wait list 中優先權最高的 task,也即在 .OSEventTbl[] 中,所有被設為 1 的位元中,優先權值最小的 task 得到該事件。圖 F6.2 畫出了 .OSEventGrp 和 .OSEventTbl[] 之間的相對應關係。該關係可以描述為:
當.OSEventTbl[0] 中的任何一位為 1 時,.OSEventGrp 中的第 0 位為 1。
當.OSEventTbl[1] 中的任何一位為 1 時,.OSEventGrp 中的第 1 位為 1。
當.OSEventTbl[2] 中的任何一位為 1 時,.OSEventGrp 中的第 2 位為 1。
當.OSEventTbl[3] 中的任何一位為 1 時,.OSEventGrp 中的第 3 位為 1。
當.OSEventTbl[4] 中的任何一位為 1 時,.OSEventGrp 中的第 4 位為 1。
當.OSEventTbl[5] 中的任何一位為 1 時,.OSEventGrp 中的第 5 位為 1。
當.OSEventTbl[6] 中的任何一位為 1 時,.OSEventGrp 中的第 6 位為 1。
當.OSEventTbl[7] 中的任何一位為 1 時,.OSEventGrp 中的第 7 位為 1。
Figure 6.2 Wait list for task waiting for an event to occur
下面的程式碼將一個 task 放到事件的 wait list 中。
程式清單 L6.2 將一個 task 插入到事件的 wait list 中 |
pevent->OSEventGrp |= OSMapTbl[prio >> 3]; |
其中,prio 是 task 的優先權,pevent 是指向 ECB 的指標。
從程式清單 L6.2可以看出,插入一個 task 到等待 task 列表中所花的時間是相同的,和表中現有多少個 task 無關。從圖 F6.2 中可以看出該演算法的原理:task 優先權的最低 3 位決定了該 task 在相對應的 .OSEventTbl[] 中的位置,緊接著的 3 位則決定了該 task 優先權在 .OSEventTbl[] 中的位元組索引。該演算法中用到的查找表 OSMapTbl[] (定義在 OS_CORE.C 中) 一般在 ROM 中實現。
表 T6.1 OSMapTbl[] 的值 | |
Index | Bit Mask (Binary) |
0 | 00000001 |
1 | 00000010 |
2 | 00000100 |
3 | 00001000 |
4 | 00010000 |
5 | 00100000 |
6 | 01000000 |
7 | 10000000 |
從 wait list 中刪除一個 task 的演算法則正好相反,如程式清單 L6.3 所示。
程式清單 L6.3 從 wailt list 中刪除一個 task |
if ((pevent->OSEventTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0) { |
該程式碼清除了 task 在 .OSEventTbl[] 中的相應位元,並且,如果其所在的群組中不再有處於等待該事件的 task 時 (即 .OSEventTbl[prio>>3] == 0),將 .OSEventGrp 中的相相對應位元也清除了。和上面的由 task 優先權來決定該 task 在 wait list 中的位置的演算法類似,從 wait list 中查找處於等待狀態且優先權最高的 task 的演算法,也不是從 .OSEventTbl[0] 開始逐個查詢,而是採用了查詢另一個表 OSUnMapTbl[256] (見檔案 OS_CORE.C)。這裏,用於索引的 8 位元分別代表相對應的 8 群組中有 task 處於等待狀態,其中的最低位元具有最高的優先權。用這個值來索引,首先得到最高優先權 task 所在的組的位置 (0 ~ 7 之間的一個數)。然後利用 .OSEventTbl[] 中相對應位元組再在 OSUnMapTbl[] 中查找,就可以得到最高優先權 task 在群組中的位置 (也是 0 ~ 7 之間的一個數)。這樣,最終就可以得到處於等待該事件狀態的最高優先權 task 了。程式清單 L6.4 是該演算法的程式碼。
程式清單 L6.4 在 wait list 中查找最高優先權的 task |
y = OSUnMapTbl[pevent->OSEventGrp]; |
舉例來說,如果 .OSEventGrp 的值是 01101000 (二進位),而相對應的 OSUnMapTbl[.OSEventGrp] 值為 3,說明最高優先權 task 所在的群組是 3。類似地,如果 .OSEventTbl[3] 的值是 11100100 (二進位),OSUnMapTbl[.OSEventTbl[3]] 的值為 2,則處於等待狀態的 task 的最高優先權是 3 x 8 + 2 = 26。
在 uC/OS-II 中,ECB 的總數由使用者所需要的 semaphore、mailbox 和 message queue 的總數決定。該值由 OS_CFG.H 中的 #define OS_MAX_EVENTS定義。在呼叫 OSInit() 時,所有 ECB 被鏈結成一個單向 link list -- 未使用的 ECB link list (圖 F6.3)。每當建立一個 semaphore、mailbox 或者 message queue 時,就從該 link list 中取出一個未使用的 ECB,並對它進行初始化。因為 semaphore、mailbox 和 message queue 一旦建立就不能刪除,所以 ECB 也不能放回到未使用的 ECB link list 中。
Figure 6.3 List of free ECBs
對於 ECBs 進行的一些通用的操作包括:
初始化一個 ECB
使一個 task 進入 READY
使一個 task 進入等待該事件的狀態
因為等待 time-out 而使一個 task 進入 READY
為了避免程式碼重復和減短程式碼長度,uC/OS-II 將上面的操作用 4 個系統函式實現,它們是:OSEventWaitListInit(), OSEventTaskRdy(),OSEventWait() 和 OSEventTO()。
Initializing an ECB, OSEventWaitListInit()
程式清單 L6.5 是函式 OSEventWaitListInit() 的程式碼。當建立一個 semaphore、mailbox 或者 message queue 時,相對應的建立函式 OSSemInit(),OSMboxCreate(),或者 OSQCreate() 透過呼叫 OSEventWaitListInit() 對 ECB 中的 wait list 進行初始化。該函式初始化一個空的 wait list,其中沒有任何 task 。該函式的呼叫參數只有一個,就是指向需要初始化的 ECB 的指標 pevent。
程式清單 L6.5 Initializing the wait list |
void OSEventWaitListInit (OS_EVENT *pevent) |
Making a task ready, OSEventTaskRdy()
程式清單 L6.6 是函式 OSEventTaskRdy() 的程式碼。當發生了某個事件,該事件 wait list 的最高優先權 task (Highest Priority Task - 以下簡稱 HPT) 要設為 READY 時,該事件相對應的 OSSemPost(),OSMboxPost(),OSQPost() 和 OSQPostFront() 函式會呼叫 OSEventTaskRdy() 實現該操作。換句話說,該函式從 wait list 中刪除 HPT,並把該 task 設為 READY。圖 F6.4 畫出了 OSEventTaskRdy() 函式最開始的 4 個動作。
該函式首先計算 HPT 在 .OSEventTbl[] 中的位元組索引 [L6.6/F6.4(1)],其結果是一個從 0 到 OS_LOWEST_PRIO/8+1 之間的數,並利用該索引得到該優先權 task 在 .OSEventGrp 中的位元遮罩 [L6.6/F6.4(2)]。然後,OSEventTaskRdy() 函式判斷 HPT 在 .OSEventTbl[] 中相應位元的位置 [L6.6/F6.4(3)],其結果是一個從 0 到 OS_LOWEST_PRIO/8+1 之間的數,以及相對應的位元遮罩 [L6.6/F6.4(4)]。根據以上結果,OSEventTaskRdy() 函式計算出 HPT 的優先權值 [L6.6(5)],然後就可以從 wait list 中刪除該 task 了 [L6.6(6)]。
task 的 TCB 中包含有需要改變的資訊。知道了 HPT 的優先權值,就可以得到指向該 task 的 TCB 的指標 [L6.6(7)]。因為最高優先權 task 運行條件已經得到滿足,必須停止 OSTimeTick() 函式對 .OSTCBDly 欄位的遞減操作,所以 OSEventTaskRdy() 直接將該欄位清除為 0 [L6.6(8)]。因為該 task 不再等待該事件的發生,所以 OSEventTaskRdy() 函式將其 TCB 中指向 ECB 的指標指向 NULL [L6.6(9)]。如果 OSEventTaskRdy() 是由 OSMboxPost() 或者 OSQPost() 呼叫的,該函式還要將相對應的 message 遞給 HPT task,放在它的 TCB 中 [L6.6(10)]。另外,當 OSEventTaskRdy() 被呼叫時,位元遮罩碼 msk 作為參數傳遞給它。該參數是用於對 TCB 中的清除位元的位元遮罩碼,和所發生事件的類型相對應 [L6.6(11)]。最後,根據 .OSTCBStat 判斷該 task 是否已處於 READY 狀態 [L6.6(12)]。如果是,則將 HPT 插入到 uC/OS-II 的 ready list 中 [L6.6(13)]。注意,HPT 得到該事件後不一定進入 READY 狀態,也許該 task 已經由於其他原因變成休眠狀態了 (DORMANT)。
此外,.OSEventTaskRdy() 函式要在關閉中斷的情況下呼叫。
程式清單 L6.6 Making a task ready-to-run |
void OSEventTaskRdy (OS_EVENT *pevent, void *msg, INT8U msk) |
Figure 6.4 Making a task ready-to-run
Making a task wait for an event, OSEventTaskWait()
程式清單 L6.7 是 OSEventTaskWait() 函式的程式碼。當某個 task 要等待一個事件的發生時,可以呼叫這些函式 OSSemPend(), OSMboxPend() 或者 OSQPend(),而這些函式會呼叫 OSEventTaskWait() 函式。換句話說,OSSemPend(),OSMboxPend() 或者 OSQPend() 函式會呼叫 OSEventTaskWait() 函式將目前的 task 從 ready list 中刪除,並放到相對應事件的 ECB 的 wait list 中。
程式清單 L6.7 Making a task wait on an ECB |
void OSEventTaskWait (OS_EVENT *pevent) |
在該函式中,首先將指向 ECB 的指標放到 task 的 TCB 中 [L6.7(1)],接著將 task 從 ready list 中刪除 [L6.7(2)],並把該 task 放到 ECB 的 wait list 中 [L6.7(3)]。
Making a task ready because of a timeout, OSEventTO()
程式清單 L6.8 是 OSEventTO() 函式的程式碼。當在預先指定的時間內 task 等待的事件沒有發生時,OSTimeTick() 函式會因為等待 time-out 而將 task 的狀態設為 READY。在這種情況下,事件的 OSSemPend(),OSMboxPend() 或者 OSQPend() 函式會呼叫 OSEventTO() 來完成這項工作。該函式負責從 ECB 中的 wait list 裏將 task 刪除 [L6.8(1)],並把它設成 READY 狀態 [L6.8(2)]。最後,從 TCB 中將指向 ECB 的指標刪除 [L6.8(3)]。使用者要注意,呼叫 OSEventTO() 也必須先關中斷。
程式清單 L6.8 Making a task ready because of a timeout |
void OSEventTO (OS_EVENT *pevent) |
Semaphores
uC/OS-II 中的 semaphore 由兩部分組成:一個是 semaphore 的計數值,它是一個 16 位元的無符號整數 (0 到 65,535 之間);另一個是由等待該 semaphore 的 task 組成的 wait list。使用者要在 OS_CFG.H 中將 OS_SEM_EN 常數定義設成 1,這樣 uC/OS-II 才能支援 semaphore。
在使用一個 semaphore 之前,首先要建立該 semaphore,也即呼叫 OSSemCreate() 函式,來對 semaphore 的初始計數值賦予值。該初始值為 0 到 65,535 之間的一個數。如果 semaphore 是用來表示一個或者多個事件的發生,那麼該 semaphore 的初始值應設為 0。如果 semaphore 是用於對共用資源的存取,那麼該 semaphore 的初始值應設為 1 (例如,把它當作開/關使用,二值 semaphore)。最後,如果該 semaphore 是用來表示允許 task 存取 n 個相同的資源,那麼該初始值顯然應該是 n,並把該 semaphore 作為一個可計數的 semaphore 使用。
Figure 6.5 Relationship between tasks, ISRs and a semaphore
uC/OS-II 提供了 5 個對 semaphore 進行操作的函式。它們是:OSSemCreate(),OSSemPend(),OSSemPost(), OSSemAccept() 和 OSSemQuery() 函式。圖 F6.5 說明了 task、ISR 和 semaphore 之間的關係。圖中用鑰匙或者旗幟的符號來表示 semaphore:如果 semaphore 用於對共用資源的存取,那麼 semaphore 就用鑰匙符號。符號旁邊的數值 N 代表可用資源數。對於二值 semaphore,該值就是 1;如果 semaphore 用於表示某事件的發生,那麼就用旗幟符號。這時的數值 N 代表事件已經發生的次數。從圖 F6.5 中可以看出 OSSemPost() 函式可以由 task 或者 ISR 呼叫,而 OSSemPend() 和 OSSemQuery() 函式只能有 task 程式呼叫。
Creating a Semaphore, OSSemCreate()
程式清單 L6.9 是 OSSemCreate() 函式的程式碼。首先,它從未使用的 ECB list 中得到一個 ECB [L6.9(1)],並對未使用的 ECB link list 的指標進行適當的調整,使它指向下一個未使用的 ECB [L6.9(2)]。如果這時有 ECB 可用 [L6.9(3)],就將該 ECB 的 OSEventType 設成 OS_EVENT_TYPE_SEM [L6.9(4)]。其他的 semaphore 操作函式 OSSem???() 透過檢查該欄位來保證所操作的 ECB 的方法是正確的。例如,這可以防止呼叫 OSSemPost() 函式對一個用作 mailbox 的 ECB 進行操作。接著,用 semaphore 的初始值對 ECB 進行初始化 [L6.9(5)],並呼叫 OSEventWaitListInit() 函式對 ECB 的 wait list 進行初始化 [L6.9(6)]。因為 semaphore 正在被初始化,所以這時沒有任何 task 等待該 semaphore。最後,OSSemCreate() 返回給呼叫者一個指向 ECB 的指標。以後對 semaphore 的所有操作,如 OSSemPend(),OSSemPost(),OSSemAccept() 和 OSSemQuery() 都是透過該指標完成的。因此,這個指標實際上就是該 semaphore 的控制碼。如果系統中沒有可用的 ECB,OSSemCreate() 將返回一個 NULL 指標。
值得注意的是,在 uC/OS-II 中,semaphore 一旦建立就不能刪除了,因此也就不可能將一個已分配的 ECB 再放回到未使用的 ECB link list 中。如果有 task 正在等待某個 semaphore,或者某 task 的運行依賴於某 semaphore 的出現時,刪除該 task 是很危險的。
程式清單 L6.9 Creating a semaphore |
OS_EVENT *OSSemCreate (INT16U cnt) |
Waiting on a Semaphore, OSSemPend()
程式清單 L6.10 是 OSSemPend() 函式的程式碼。它首先檢查指標 pevent 所指的 ECB 是否是由 OSSemCreate() 建立的 [L6.10(1)]。如果 semaphore 目前是可用的 (semaphore 的計數值大於 0) [L6.10(2)],將 semaphore 的計數值減 1 [L6.10(3)],然後函式將 "無錯" 錯誤碼返回給它的呼叫者。顯然,如果正在等待 semaphore,這時的輸出正是我們所希望的,也是運行 OSSemPend() 函式最快的路徑。
如果此時 semaphore 無效 (計數器的值是 0),OSSemPend() 函式要進一步檢查它的呼叫函式是不是 ISR [L6.10(4)]。在正常情況下, ISR 是不會呼叫 OSSemPend() 函式的。這裏加入這些程式碼,只是為了以防萬一。當然,在 semaphore 有效的情況下,即使是在 ISR 呼叫的 OSSemPend(),函式也會成功返回,不會出任何錯誤。
如果 semaphore 的計數值為 0,而 OSSemPend() 函式又不是由 ISR 呼叫的,則呼叫 OSSemPend() 函式的 task 要進入睡眠狀態,等待另一個 task (或者是 ISR) 發出該 semaphore。OSSemPend() 允許使用者定義一個最長等待時間作為它的參數,這樣可以避免該 task 無止盡地等待下去。如果該參數值是一個大於 0 的值,那麼該 task 將一直等到 semaphore 有效或者等待 time-out 。如果該參數值為 0,該 task 將一直等待下去。OSSemPend() 函式透過將 TCB 中的狀態欄位 .OSTCBStat 設為 1,把 task 設為睡眠狀態 [L6.10(5)],等待時間也同時設入 TCB 中 [L6.10(6)],該值在 OSTimeTick() 函式中會被逐次遞減。注意,OSTimeTick() 函式對每個 task 的 TCB 的 .OSTCBDly 欄位做遞減操作 (只要該欄位不為 0)。而真正將 task 設為睡眠狀態的操作在 OSEventTaskWait() 函式中執行 [L6.10(7)]。
因為目前 task 已經不是 READY 狀態了,所以 task 排程器將下一個最高優先權的 task 調入,準備運行 [L6.10(8)]。當 semaphore 有效或者等待時間到後,呼叫 OSSemPend() 函式的 task 將再一次成為最高優先權 task。這時 OSSched() 函式返回。這之後,OSSemPend() 要檢查 TCB 中的狀態欄位,看該 task 是否仍處於等待 semaphore 的狀態 [L6.10(9)]。如果是,說明該 task 還沒有被 OSSemPost() 函式發出的 semaphore 喚醒。事實上,該 task 是因為等待 time-out 而由 TimeTick() 函式把它設為 READY 的狀態。這種情況下,OSSemPend() 函式呼叫 OSEventTO() 函式將 task 從 wait list 中刪除 [L6.10(10)],並返回給它的呼叫者一個 "time-out" 的錯誤碼。如果 task 的 TCB 中的 OS_STAT_SEM 位元沒有被設立,就認為呼叫 OSSemPend() 的 task 已經得到了該 semaphore,將指向 ECB 的指標從該 task 的 wait list 中刪除,並返回給呼叫者一個 "無錯" 的錯誤碼 [L6.10(11)]。
程式清單 L6.10 Waiting for a semaphore |
void OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *err) |
Signaling a Semaphore, OSSemPost()
程式清單 L6.11 是OSSemPost() 函式的程式碼。它首先檢查參數指標 pevent 所指向的 ECB 是否是 OSSemCreate() 函式建立的 [L6.11(1)],接著檢查是否有 task 在等待該 semaphore [L6.11(2)]。如果該 ECB 中的 .OSEventGrp 欄位不是 0,說明有 task 正在等待該 semaphore。這時,就呼叫函式 OSEventTaskRdy(),把其中的 HPT 從 wait list 中刪除 [L6.11(3)] 並使它進入 READY 狀態。然後呼叫 OSSched(),排程器會檢查該 task 是否是系統中的最高優先權且 READY 的 task [L6.11(4)]。如果是,這時就要進行 task switch [當 OSSemPost() 函式是在 task 中呼叫的],準備執行該 READY 的 task。如果不是,OSSched() 直接返回,呼叫 OSSemPost() 的 task 得以繼續執行。如果這時沒有 task 在等待該 semaphore,該 semaphore 的計數值就簡單地加 1 [L6.11(5)]。
上面是由 task 呼叫 OSSemPost() 時的情況。當 ISR 呼叫該函式時,不會發生上面的 task switch。如果需要,task switch 要等到巢狀中斷的最外層 ISR 呼叫 OSIntExit() 函式後才能進行。
程式清單 L6.11 Signaling a semaphore |
INT8U OSSemPost (OS_EVENT *pevent) |
Getting a Semaphore without waiting, OSSemAccept()
當一個 task 請求一個semaphore時,如果該semaphore暫時無效,也可以讓該 task 簡單地返回,而不是進入睡眠等待狀態。這種情況下的操作是由 OSSemAccept() 函式完成的,其源程式碼見程式清單 L6.12。該函式在最開始也是檢查參數指標 pevent 指向的 ECB 是否是由 OSSemCreate() 函式建立的 [L6.12(1)],接著從該 semaphore 的 ECB 中取出當前計數值 [L6.12(2)],並檢查該 semaphore 是否有效 (計數值是否為非 0 值) [L6.12(3)]。如果有效,則將 semaphore 的計數值減 1 [L6.12(4)],然後將 semaphore 的原有計數值返回給呼叫者 [L6.12(5)]。呼叫者必須對該返回值進行檢查。如果該值是 0,說明該 semaphore 無效。如果該值大於 0,說明該 semaphore 有效,同時該值也暗示著該 semaphore 當前可用的資源數。應該注意的是,這些可用資源中,已經被該呼叫者自身佔用了一個 (該計數值已經被減 1)。 ISR 要請求 semaphore 時,只能用 OSSemAccept() 而不能用 OSSemPend(),這是因為 ISR 是不允許等待的。
程式清單 L6.12 Getting a semaphore without waiting |
INT16U OSSemAccept (OS_EVENT *pevent) |
Obtaining the status of a semaphore, OSSemQuery()
在應用程式中,使用者隨時可以呼叫函式 OSSemQuery() [程式清單 L6.13] 來查詢一個 semaphore 的當前狀態。該函式有兩個參數:一個是指向 semaphore 相對應 ECB 的指標 pevent。該指標是在生產 semaphore 時,由 OSSemCreate() 函式返回的;另一個是指向用於記錄 semaphore 資訊的資料結構 OS_SEM_DATA (見 uCOS_II.H) 的指標 pdata。因此,呼叫該函式前,使用者必須先定義該結構變數,用於存儲 semaphore 的有關資訊。在這裏,之所以使用一個新的資料結構的原因在於,呼叫函式應該只關心那些和特定 semaphore 有關的資訊,而不是像 OS_EVENT 資料結構包含的是很全面的資訊。該資料結構只包含 semaphore 計數值 .OSCnt 和 wait list 的 .OSEventTbl[] 、.OSEventGrp,而 OS_EVENT 中還包含了另外的兩個欄位 .OSEventType 和 .OSEventPtr。
和其他與 semaphore 有關的函式一樣,OSSemQuery() 也是先檢查 pevent 指向的 ECB 是否是 OSSemCreate() 產生的 [L6.13(1)],然後將 wait list [L6.13(2)] 和計數值 [L6.13(3)] 從 OS_EVENT 結構拷貝到 OS_SEM_DATA 結構變數中去。
程式清單 L6.13 Obtaining the status of a semaphore |
INT8U OSSemQuery (OS_EVENT *pevent, OS_SEM_DATA *pdata) |
Message Mailboxes
message mailbox (又簡稱 mailbox) 是 uC/OS-II 中另一種溝通機制,它可以使一個 task 或者 ISR 向另一個 task 發送一個指標型的變數。該指標指向一個包含了特定 "message" 的資料結構。為了在 uC/OS-II 中使用 mailbox,必須將 OS_CFG.H 中的 OS_MBOX_EN 常數設為 1。
使用 mailbox 之前,必須先建立該 mailbox。該操作可以透過呼叫 OSMboxCreate() 函式來完成,並且要指定指標的初始值。一般情況下,這個初始值是 NULL,但也可以初始化一個 mailbox,使其在最開始就包含一條消息。如果使用 mailbox 的目的是用來通知一個事件的發生 (發送一條消息) ,那麼就要初始化該 mailbox 為 NULL,因為在開始時,事件還沒有發生。如果使用者用 mailbox 來共用某些資源,那麼就要初始化該 mailbox 為一個非 NULL 的指標。在這種情況下, mailbox 被當成一個二值 semaphore 使用。
uC/OS-II 提供了 5 種對 mailbox 的操作:OSMboxCreate(),OSMboxPend(),OSMboxPost(),OSMboxAccept() 和 OSMboxQuery() 函式。圖 F6.6 描述了 task、ISR 和 mailbox 之間的關係,這裏用符號 "I" 表示 mailbox。mailbox 包含的內容是一個指向一條消息的指標。一個 mailbox 只能包含一個這樣的指標 (mailbox 為滿時),或者一個指向 NULL 的指標 (mailbox 為空時)。從圖 F6.6 可以看出,task 或者 ISR 可以呼叫函式 OSMboxPost(),但是只有 task 可以呼叫函式 OSMboxPend() 和 OSMboxQuery()。
Figure 6.6 Relationship between tasks, ISRs and a message mailbox
Creating a Mailbox, OSMboxCreate()
程式清單 L6.14 是 OSMboxCreate() 函式的程式碼,基本上和函式 OSSemCreate() 相似。不同之處在於 ECB 的類型被設成 OS_EVENT_TYPE_MBOX [L6.14(1)],以及使用 .OSEventPtr 欄位來容納 message 指標,而不是使用 .OSEventCnt 欄位 [L6.14(2)]。
OSMboxCreate() 函式的返回值是一個指向 ECB 的指標 [L6.14(3)]
。這個指標在呼叫函式 OSMboxPend(),OSMboxPost(), OSMboxAccept() 和 OSMboxQuery() 時使用。因此,該指標可以看作是相對應 mailbox 的控制碼。值得注意的是,如果系統中已經沒有 ECB 可用,函式 OSMboxCreate() 將返回一個 NULL 指標。
mailbox 一旦建立,是不能被刪除的。比如說,如果有 task 正在等待一個 mailbox 的資訊,這時刪除該 mailbox,將有可能產生災難性的後果。
程式清單 L6.14 Creating a mailbox |
OS_EVENT *OSMboxCreate (void *msg) |
Waiting for a message at a Mailbox, OSMboxPend()
程式清單 L6.15 是 OSMboxPend() 函式的源程式碼。同樣,它和 OSSemPend() 也很相似,因此,在這裏只講述其中的不同之處。 OSMboxPend() 首先檢查該 ECB 是由 OSMboxCreate() 函式建立的 [L6.15(1)]。當 .OSEventPtr 欄位是一個非 NULL 的指標時,說明該 mailbox 中有可用的消息 [L6.15(2)]。這種情況下,OSMboxPend() 函式將該欄位的值複製到局部變數 msg 中,然後將 .OSEventPtr 設為 NULL [L6.15(3)]。這正是我們所期望的,也是執行 OSMboxPend() 函式最快的路徑。
如果此時 mailbox 中沒有消息是可用的 (.OSEventPtr 欄位是 NULL 指標),OSMboxPend() 函式檢查它的呼叫者是否是 ISR [L6.15(4)]。像 OSSemPend() 函式一樣,不能在 ISR 中呼叫 OSMboxPend(),因為 ISR 是不能等待的。這裏的程式碼同樣是為了以防萬一。但是,如果 mailbox 中有可用的消息,即使從 ISR 中呼叫 OSMboxPend() 函式,也一樣是成功的。
如果 mailbox 中沒有可用的消息,OSMboxPend() 的呼叫 task 就會進入休眠的狀態,直到 mailbox 中有了消息或者等待 time-out [L6.15(5)]。當有其他的 task 向該 mailbox 發送了消息後 (或者等待時間 time-out),這時,該 task 再一次成為 HPT, OSSched() 返回。這時,OSMboxPend() 函式要檢查是否有消息被放到該 task 的 TCB 中 [L6.15(6)]。如果有,那麼該次函式呼叫成功,相對應的消息被返回到呼叫者。
程式清單 L6.15 Waiting for a message to arrive at a mailbox |
void *OSMboxPend (OS_EVENT *pevent, INT16U timeout, INT8U *err) |
在 OSMboxPend() 函式中,透過檢查 TCB 中的 .OSTCBStat 欄位中的 OS_STAT_MBOX 位元,可以知道是否等待 time-out 。如果該欄位被設為 1,說明 task 等待已經 time-out [L6.15(7)]。這時,透過呼叫函式 OSEventTo() 可以將 task 從 mailbox 的 wait list 中刪除 [L6.15(8)]。因為此時 mailbox 中沒有消息,所以返回的指標是 NULL [L6.15(9)]。如果 OS_STAT_MBOX 位元沒有被設為 1,說明所等待的消息已經被發出。OSMboxPend() 的呼叫者將會得到指向消息的指標 [L6.15(10)]。此後,OSMboxPend() 函式透過將 ECB 的 .OSEventPtr 欄位設為 NULL 來清空該 mailbox,並且要將 task 的 TCB 中指向 ECB 的指標刪除 [L6.15(12)]。
Sending a message to a mailbox, OSMboxPost()
程式清單 L6.16 是OSMboxPost() 函式的程式碼。檢查了 ECB 是否是一個 mailbox 後 [L6.16(1)],OSMboxPost() 函式還要檢查是否有 task 在等待該 mailbox 中的消息[L6.16(2)]
。如果 ECB 中的OSEventGrp欄位包含非零值,就暗示著有 task 在等待該消息。這時,呼叫OSEventTaskRdy()將其中的最高優先權 task 從等待列表中刪除[見6.02節,使一個 task 進入就緒狀態,OSEventTaskRdy()][L6.16(3)],加入系統的就緒 task 列表中,準備運行。然後,呼叫OSSched()函式[L6.16(4)],檢查該 task 是否是系統中最高優先權的就緒 task 。如果是,執行 task 切換[僅當OSMboxPost()函式是由 task 呼叫時],該 task 得以執行。如果該 task 不是最高優先權的 task ,OSSched()返回,OSMboxPost()的呼叫函式繼續執行。如果沒有任何 task 等待該消息,指向消息的指標就被保存到 mailbox 中[L6.16(6)] (假設此時 mailbox 中的指標不是非NULL的[L6.16(5)]) 。這樣,下一個呼叫OSMboxPend()函式的 task 就可以立刻得到該消息了。
注意,如果OSMboxPost()
函式是從 ISR 中呼叫的,那麼,這時並不發生 context switch。如果需要,ISR 引起的 context switch 只會發生在巢狀中斷的最外層 ISR 對 OSIntExit() 函式呼叫的時後。
程式清單 L6.16 Depositing a message in a mailbox |
INT8U OSMboxPost (OS_EVENT *pevent, void *msg) |
Getting a message without waiting, OSMboxAccept()
應用程式也可以用不需等待的方式從 mailbox 中得到消息。這可以透過程式清單 L6.17 中的 OSMboxAccept() 函式來實現。 OSMboxAccept() 函式開始也是檢查 ECB 是否是由 OSMboxCreate() 函式建立的 [L6.17(1)]。接著,它得到 mailbox 中目前的內容 [L6.17(2)],並判斷是否有消息是可用的 [L6.17(3)]。如果 mailbox 中有消息,就把 mailbox 清空 [L6.17(4)],而 mailbox 中原來指向消息的指標被返回給 OSMboxAccept() 的呼叫者 [L6.17(5)]。OSMboxAccept() 函式的呼叫函式必須檢查該返回值是否為 NULL。如果該值是 NULL,說明 mailbox 是空的,沒有可用的消息。如果該值是非 NULL 值,說明 mailbox 中有消息可用,而且該呼叫函式已經得到了該消息。ISR 在試圖得到一個消息時,應該使用 OSMboxAccept() 函式,而不能使用 OSMboxPend() 函式。
OSMboxAccept() 函式的另一個用途是,使用者可以用它來清空一個 mailbox 中現有的內容。
程式清單 L6.17 Depositing a message in a mailbox |
void *OSMboxAccept (OS_EVENT *pevent) |
Obtaining the status of a mailbox, OSMboxQuery()
OSMboxQuery() 函式使應用程式可以隨時查詢一個 mailbox 的當前狀態。程式清單 L6.18 是該函式的程式碼。它需要兩個參數:一個是指向 mailbox 的指標 pevent。該指標是在建立該 mailbox 時,由 OSMboxCreate() 函式返回的;另一個是指向用來保存有關 mailbox 的資訊的 OS_MBOX_DATA (見 uCOS_II.H) 資料結構的指標 pdata。在呼叫 OSMboxCreate() 函式之前,必須先定義該結構變數,用來保存有關 mailbox 的資訊。之所以定義一個新的資料結構,是因為這裏關心的只是和特定 mailbox 有關的內容,而非整個 OS_EVENT 資料結構的內容。後者還包含了另外兩個欄位 (.OSEventCnt 和 .OSEventType),而 OS_MBOX_DATA 只包含 mailbox 中的消息指標 (.OSMsg) 和該 mailbox 現有的 wait list (.OSEventTbl[] 和 .OSEventGrp) 。
和前面的所以函式一樣,該函式也是先檢查 ECB 是否是 mailbox [L6.18(1)]。然後,將 mailbox 中的 wait list [L6.18(2)] 和 mailbox 中的消息 [L6.18(3)] 從 OS_EVENT 資料結構複製到 OS_MBOX_DATA 資料結構。
程式清單 L6.18 Obtaining the status of a mailbox |
INT8U OSMboxQuery (OS_EVENT *pevent, OS_MBOX_DATA *pdata) |
Using a mailbox as a binary semaphore
一個 mailbox 可以被用作二值的 semaphore。首先,在初始化時,將 mailbox 設設為一個非零的指標 (如 (void *) 1)。這樣,一個 task 可以呼叫 OSMboxPend() 函式來請求一個 semaphore,然後透過呼叫 OSMboxPost() 函式來釋放一個 semaphore。程式清單 L6.19 說明了這個過程是如何工作的。如果使用者只需要二值 semaphore 和 mailbox,這樣做可以節省程式碼空間。這時可以將 OS_SEM_EN 設為 0,只使用 mailbox 就可以了。
程式清單 L6.19 Using a mailbox as a binary semaphore |
OS_EVENT *MboxSem; |
Using a mailbox instead of OSTimeDly()
mailbox 的等待 time-out 功能可以被用來模仿 OSTimeDly() 函式的延時,如程式清單 L6.20 所示。如果在指定的時間段 TIMEOUT 內,沒有消息到來,Task1() 函式將繼續執行。這和 OSTimeDly(TIMEOUT) 功能很相似。但是,如果 Task2() 在指定的時間結束之前,向該 mailbox 發送了一個 "假" 消息,Task1() 就會提前開始繼續執行。這和呼叫 OSTimeDlyResume() 函式的功能是一樣的。注意,這裏忽略了對返回的消息的檢查,因為此時關心的不是得到了什麼樣的消息。
程式清單 L6.20 Using a mailbox as a time delay |
OS_EVENT *MboxTimeDly; |
Message Queues
message queue 是 uC/OS-II 中另一種溝通機制,它可以使一個 task 或者 ISR 向另一個 task 發送以指標方式定義的變數。因具體的應用有所不同,每個指標指向的資料結構變數也有所不同。為了使用 uC/OS-II 的 message queue 功能,需要在 OS_CFG.H 檔案中,將 OS_Q_EN 常數定義設為 1,並且透過常數定義 OS_MAX_QS 來決定 uC/OS-II 支援的最多 message queue 數目。
在使用一個 message queue 之前,必須先建立該 message queue。這可以透過呼叫 OSQCreate() 函式,並定義 message queue 中的單元數 (消息數) 來完成。
uC/OS-II 提供了 7 個對 message queue 進行操作的函式:OSQCreate(),OSQPend(),OSQPost(),OSQPostFront() ,OSQAccept(),OSQFlush() 和 OSQQuery() 函式。圖 F6.7 是 task、ISR 和 message queue 之間的關係。其中,message queue 的符號很像多個 mailbox。實際上,我們可以將 message queue 看作時多個 mailbox 組成的陣列,只是它們共用一個 wait list。每個指標所指向的資料結構是由具體的應用程式決定的。N 代表了 message queue 中的總單元數。當呼叫 OSQPend() 或者 OSQAccept() 之前,呼叫 N 次 OSQPost() 或者 OSQPostFront() 就會把 message queue 填滿。從圖 F6.7 中可以看出,一個 task 或者 ISR 可以呼叫 OSQPost(),OSQPostFront(),OSQFlush() 或者 OSQAccept() 函式。但是,只有 task 可以呼叫 OSQPend() 和 OSQQuery() 函式。
Figure 6.7 Relationship between tasks, ISRs and a message queue
圖 F6.8 是實現 message queue 所需要的各種資料結構。這裏也需要 ECB 來記錄 wait list [F6.8(1)],而且, ECB 可以使多個 message queue 的操作和 semaphore 操作、mailbox 操作有相同的程式碼。當建立了一個 message queue 時,一個佇列控制塊 (queue control block, QCB) (OS_Q 結構,見 OS_Q.C 檔案) 也同時被建立,並透過 OS_EVENT 中的 .OSEventPtr 欄位鏈結到相對應的 ECB [F6.8(2)]。在建立一個 message queue 之前,必須先定義一個含有與 message queue 最大消息數相同個數的指標陣列 [F6.8(3)]。陣列的起始位址以及陣列中的元素數作為參數傳遞給 OSQCreate() 函式。事實上,如果記憶體佔用了連續的位址空間,也沒有必要非得使用指標陣列結構。
Figure 6.8 Data structures used in a message queue
檔案 OS_CFG.H 中的常數定義 OS_MAX_QS 定義了在 uC/OS-II 中可以使用的最大 message queue 數,這個定義的最小值應為 2。uC/OS-II 在初始化時建立一個未使用的的佇列控制塊 link list,如圖 F6.9 所示。
Figure 6.9 List of free queue control blocks
佇列控制塊 (QCB) 是一個用於維護 message queue 資訊的資料結構,它包含了以下的一些欄位。這裏,仍然在各個變數前加入一個 [.] 來表示它們是資料結構中的一個欄位。
.OSQPtr 在未使用的佇列控制塊中鏈結所有的佇列控制塊。一旦建立了 message queue,該欄位就不再有用了。
.OSQStart 是指向 message queue 的指標陣列的起始位址的指標。使用者應用程式在使用 message queue 之前必須先定義該陣列。
.OSQEnd 是指向 message queue 結束單元的下一個位址的指標。該指標使得 message queue 構成一個 circular buffer。
.OSQIn 是指向 message queue 中插入下一條消息的位置的指標。當 .OSQIn 和 .OSQEnd 相等時,.OSQIn 被調整指向 message queue 的起始單元。
.OSQOut 是指向 message queue 中下一個取出消息的位置的指標。當 .OSQOut 和 .OSQEnd 相等時,.OSQOut 被調整指向 message queue 的起始單元。
.OSQSize 是 message queue 中總的單元數。該值是在建立 message queue 時由使用者應用程式決定的。在 uC/OS-II 中,該最大值是 65,535。
.OSQEntries 是 message queue 中當前的消息數量。當 message queue 是空的時,該值為 0。當 message queue 滿了以後,該值和 .OSQSize 的值一樣。在 message queue 剛剛建立時,該值為 0。
message queue 最根本的部分是一個 circular buffer,如圖 F6.10。其中的每個單元包含一個指標。佇列未滿時,.OSQIn [F6.10(1)] 指向下一個存放消息的位址單元。如果佇列已滿 (ie. .OSQEntries == .OSQSize),.OSQIn [F6.10(3)] 則與 .OSQOut 指向同一單元。如果在 .OSQIn 指向的單元插入新的指向消息的指標,就構成 FIFO (First-In-First-Out) queue。相反,如果在 .OSQOut 指向的單元的下一個單元插入新的指標,就構成 LIFO (Last-In-First-Out) queue [F6.10(2)]。當 .OSQEntries 和 .OSQSize 相等時,說明佇列已滿。消息指標總是從 .OSQOut [F6.10(4)] 指向的單元取出。指標 .OSQStart 和 .OSQEnd [F6.10(5)] 定義了消息指標陣列的頭尾,以便在 .OSQIn 和 .OSQOut 到達佇列的邊緣時,進行邊界檢查和必要的指標調整,實現迴圈的功能。
Figure 6.10 Message queue is a circular buffer of pointers.
Creating a Queue, OSQCreate()
程式清單 L6.21 是 OSQCreate() 函式的程式碼。該函式需要一個指標陣列來容納指向各個消息的指標。該指標陣列必須宣告為 void 類型。
OSQCreate() 首先從未使用的 ECB link list 中取得一個 ECB (見圖 F6.3) [L6.21(1)],並對剩下的未使用的 ECB list 的指標做相對應的調整,使它指向下一個未使用的 ECB [L6.21(2)]。接著,OSQCreate() 函式從未使用的 QCB link list 中取出一個 QCB [L6.21(3)]。如果有未使用的 QCB,就對其進行初始化 [L6.21(4)]。然後該函式將 ECB 的類型設為 OS_EVENT_TYPE_Q [L6.21(5)],並使其 .OSEventPtr 指標指向 QCB [L6.21(6)]。OSQCreate() 還要呼叫 OSEventWaitListInit() 函式對 ECB 的 wait list 初始化 [L6.21(7)]。因為此時 message queue 正在初始化,顯然它的 wait list 是空的。最後,OSQCreate() 向它的呼叫者返回一個指向 ECB 的指標 [L6.21(9)]。該指標將在呼叫 OSQPend(),OSQPost(),OSQPostFront(),OSQFlush(),OSQAccept() 和 OSQQuery() 等 message queue 處理函式時會使用到。因此,該指標可以被看作是相對應 message queue 的控制碼。值得注意的是,如果此時沒有未使用的的 ECB,OSQCreate() 函式將返回一個 NULL 指標。如果沒有 QCB 可以使用,為了不浪費 ECB 資源,OSQCreate() 函式將把剛剛取得的 ECB 重新還給未使用的 ECB link list [L6.21(8)]。
另外, message queue 一旦建立就不能再刪除了。試想,如果有 task 正在等待某個 message queue 中的消息,而此時又刪除該 message queue,將是很危險的。
程式清單 L6.21 Creating a queue |
OS_EVENT *OSQCreate (void **start, INT16U size) |
留言列表