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 溝通的方法: SemaphoresMessage 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 {
INT8U OSEventType; /* 事件類型 */
INT8U OSEventGrp; /* 等待 task 所在的群組 */
INT16U OSEventCnt; /* 計數器(當事件是semaphore時) */
void *OSEventPtr; /* 指向消息或者 message queue 的指標 */
INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待 task 列表 */
} OS_EVENT;

.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];
pevent->OSEventTbl[prio >> 3] |= OSMapTbl[prio & 0x07];

其中,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) {
pevent->OSEventGrp &= ~OSMapTbl[prio >> 3];
}

該程式碼清除了 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];
x = OSUnMapTbl[pevent->OSEventTbl[y]];
prio = (y << 3) + x;

舉例來說,如果 .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)
{
INT8U i;


pevent->OSEventGrp = 0x00;
for (i = 0; i < OS_EVENT_TBL_SIZE; i++) {
pevent->OSEventTbl[i] = 0x00;
}
}

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)
{
OS_TCB *ptcb;
INT8U x;
INT8U y;
INT8U bitx;
INT8U bity;
INT8U prio;


y = OSUnMapTbl[pevent->OSEventGrp]; (1)
bity = OSMapTbl[y]; (2)
x = OSUnMapTbl[pevent->OSEventTbl[y]]; (3)
bitx = OSMapTbl[x]; (4)
prio = (INT8U) ((y << 3) + x); (5)
if ((pevent->OSEventTbl[y] &= ~bitx) == 0) { (6)
pevent->OSEventGrp &= ~bity;
}

ptcb = OSTCBPrioTbl[prio]; (7)
ptcb->OSTCBDly = 0; (8)
ptcb->OSTCBEventPtr = (OS_EVENT *) 0; (9)

#if (OS_Q_EN && (OS_MAX_QS >= 2)) || OS_MBOX_EN
ptcb->OSTCBMsg = msg; (10)
#else
msg = msg;
#endif

ptcb->OSTCBStat &= ~msk; (11)
if (ptcb->OSTCBStat == OS_STAT_RDY) { (12)
OSRdyGrp |= bity; (13)
OSRdyTbl[y] |= bitx;
}
}


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)
{
OSTCBCur->OSTCBEventPtr = pevent; (1)

if ((OSRdyTbl[OSTCBCur->OSTCBY] &= ~OSTCBCur->OSTCBBitX) == 0) { (2)
OSRdyGrp &= ~OSTCBCur->OSTCBBitY;
}

pevent->OSEventTbl[OSTCBCur->OSTCBY] |= OSTCBCur->OSTCBBitX; (3)
pevent->OSEventGrp |= OSTCBCur->OSTCBBitY;
}

在該函式中,首先將指向 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)
{
if ((pevent->OSEventTbl[OSTCBCur->OSTCBY] &= ~OSTCBCur->OSTCBBitX) == 0){ (1)
pevent->OSEventGrp &= ~OSTCBCur->OSTCBBitY;
}

OSTCBCur->OSTCBStat = OS_STAT_RDY; (2)
OSTCBCur->OSTCBEventPtr = (OS_EVENT *) 0; (3)
}



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)
{
OS_EVENT *pevent;


OS_ENTER_CRITICAL();
pevent = OSEventFreeList; (1)

if (OSEventFreeList != (OS_EVENT *) 0) { (2)
OSEventFreeList = (OS_EVENT *)OSEventFreeList->OSEventPtr;
}

OS_EXIT_CRITICAL();

if (pevent != (OS_EVENT *) 0) { (3)
pevent->OSEventType = OS_EVENT_TYPE_SEM; (4)
pevent->OSEventCnt = cnt; (5)
OSEventWaitListInit(pevent); (6)
}
return (pevent); (7)
}

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)
{
OS_ENTER_CRITICAL();

if (pevent->OSEventType != OS_EVENT_TYPE_SEM) { (1)
OS_EXIT_CRITICAL();
*err = OS_ERR_EVENT_TYPE;
}

if (pevent->OSEventCnt > 0) { (2)
pevent->OSEventCnt--; (3)
OS_EXIT_CRITICAL();
*err = OS_NO_ERR;

} else if (OSIntNesting > 0) { (4)
OS_EXIT_CRITICAL();
*err = OS_ERR_PEND_ISR;

} else {
OSTCBCur->OSTCBStat |= OS_STAT_SEM; (5)
OSTCBCur->OSTCBDly = timeout; (6)
OSEventTaskWait(pevent); (7)
OS_EXIT_CRITICAL();

OSSched(); (8)

OS_ENTER_CRITICAL();
if (OSTCBCur->OSTCBStat & OS_STAT_SEM) { (9)
OSEventTO(pevent); (10)
OS_EXIT_CRITICAL();
*err = OS_TIMEOUT;

} else {
OSTCBCur->OSTCBEventPtr = (OS_EVENT *) 0; (11)
OS_EXIT_CRITICAL();
*err = OS_NO_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)
{
OS_ENTER_CRITICAL();

if (pevent->OSEventType != OS_EVENT_TYPE_SEM) { (1)
OS_EXIT_CRITICAL();
return (OS_ERR_EVENT_TYPE);
}

if (pevent->OSEventGrp) { (2)
OSEventTaskRdy(pevent, (void *) 0, OS_STAT_SEM); (3)
OS_EXIT_CRITICAL();

OSSched(); (4)
return (OS_NO_ERR);

} else {
if (pevent->OSEventCnt < 65535) {
pevent->OSEventCnt++; (5)

OS_EXIT_CRITICAL();
return (OS_NO_ERR);

} else {
OS_EXIT_CRITICAL();
return (OS_SEM_OVF);
}
}
}

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)
{
INT16U cnt;


OS_ENTER_CRITICAL();

if (pevent->OSEventType != OS_EVENT_TYPE_SEM) { (1)
OS_EXIT_CRITICAL();
return (0);
}

cnt = pevent->OSEventCnt; (2)
if (cnt > 0) { (3)
pevent->OSEventCnt--; (4)
}
OS_EXIT_CRITICAL();
return (cnt); (5)
}

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)
{
INT8U i;
INT8U *psrc;
INT8U *pdest;


OS_ENTER_CRITICAL();

if (pevent->OSEventType != OS_EVENT_TYPE_SEM) { (1)
OS_EXIT_CRITICAL();
return (OS_ERR_EVENT_TYPE);
}

pdata->OSEventGrp = pevent->OSEventGrp; (2)
psrc = &pevent->OSEventTbl[0];
pdest = &pdata->OSEventTbl[0];

for (i = 0; i < OS_EVENT_TBL_SIZE; i++) {
*pdest++ = *psrc++;
}

pdata->OSCnt = pevent->OSEventCnt; (3)
OS_EXIT_CRITICAL();
return (OS_NO_ERR);
}



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)
{
OS_EVENT *pevent;


OS_ENTER_CRITICAL();

pevent = OSEventFreeList;
if (OSEventFreeList != (OS_EVENT *) 0) {
OSEventFreeList = (OS_EVENT *)OSEventFreeList->OSEventPtr;
}

OS_EXIT_CRITICAL();

if (pevent != (OS_EVENT *) 0) {
pevent->OSEventType = OS_EVENT_TYPE_MBOX; (1)
pevent->OSEventPtr = msg; (2)
OSEventWaitListInit(pevent);
}
return (pevent); (3)
}

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)
{
void *msg;


OS_ENTER_CRITICAL();

if (pevent->OSEventType != OS_EVENT_TYPE_MBOX) { (1)
OS_EXIT_CRITICAL();
*err = OS_ERR_EVENT_TYPE;
return ((void *) 0);
}

msg = pevent->OSEventPtr;
if (msg != (void *) 0) { (2)
pevent->OSEventPtr = (void *) 0; (3)
OS_EXIT_CRITICAL();
*err = OS_NO_ERR;

} else if (OSIntNesting > 0) { (4)
OS_EXIT_CRITICAL();
*err = OS_ERR_PEND_ISR;

} else {
OSTCBCur->OSTCBStat |= OS_STAT_MBOX; (5)
OSTCBCur->OSTCBDly = timeout;
OSEventTaskWait(pevent);
OS_EXIT_CRITICAL();

OSSched();

OS_ENTER_CRITICAL();

if ((msg = OSTCBCur->OSTCBMsg) != (void *) 0) { (6)
OSTCBCur->OSTCBMsg = (void *) 0;
OSTCBCur->OSTCBStat = OS_STAT_RDY;
OSTCBCur->OSTCBEventPtr = (OS_EVENT *) 0;
OS_EXIT_CRITICAL();
*err = OS_NO_ERR;

} else if (OSTCBCur->OSTCBStat & OS_STAT_MBOX) { (7)
OSEventTO(pevent); (8)
OS_EXIT_CRITICAL();
msg = (void *) 0; (9)
*err = OS_TIMEOUT;

} else {
msg = pevent->OSEventPtr; (10)
pevent->OSEventPtr = (void *) 0; (11)
OSTCBCur->OSTCBEventPtr = (OS_EVENT *) 0; (12)
OS_EXIT_CRITICAL();
*err = OS_NO_ERR;
}
}
return (msg);
}

在 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)
{
OS_ENTER_CRITICAL();

if (pevent->OSEventType != OS_EVENT_TYPE_MBOX) { (1)
OS_EXIT_CRITICAL();
return (OS_ERR_EVENT_TYPE);
}

if (pevent->OSEventGrp) { (2)
OSEventTaskRdy(pevent, msg, OS_STAT_MBOX); (3)
OS_EXIT_CRITICAL();

OSSched(); (4)

return (OS_NO_ERR);
} else {
if (pevent->OSEventPtr != (void *) 0) { (5)
OS_EXIT_CRITICAL();
return (OS_MBOX_FULL);

} else {
pevent->OSEventPtr = msg; (6)
OS_EXIT_CRITICAL();
return (OS_NO_ERR);
}
}
}

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)
{
void *msg;


OS_ENTER_CRITICAL();

if (pevent->OSEventType != OS_EVENT_TYPE_MBOX) { (1)
OS_EXIT_CRITICAL();
return ((void *) 0);
}

msg = pevent->OSEventPtr; (2)

if (msg != (void *) 0) { (3)
pevent->OSEventPtr = (void *) 0; (4)
}
OS_EXIT_CRITICAL();
return (msg); (5)
}

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)
{
INT8U i;
INT8U *psrc;
INT8U *pdest;


OS_ENTER_CRITICAL();

if (pevent->OSEventType != OS_EVENT_TYPE_MBOX) { (1)
OS_EXIT_CRITICAL();
return (OS_ERR_EVENT_TYPE);
}

pdata->OSEventGrp = pevent->OSEventGrp; (2)
psrc = &pevent->OSEventTbl[0];
pdest = &pdata->OSEventTbl[0];

for (i = 0; i < OS_EVENT_TBL_SIZE; i++) {
*pdest++ = *psrc++;
}
pdata->OSMsg = pevent->OSEventPtr; (3)
OS_EXIT_CRITICAL();
return (OS_NO_ERR);
}

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;

void Task1 (void *pdata)
{
INT8U err;


for (;;) {
OSMboxPend(MboxSem, 0, &err); /* 獲得對資源的存取權 */
.
. /* task 獲得 semaphore, 對資源進行存取 */
.
OSMboxPost(MboxSem, (void *) 1); /* 釋放對資源的存取權 */
}
}

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;

void Task1 (void *pdata)
{
INT8U err;


for (;;) {
OSMboxPend(MboxTimeDly, TIMEOUT, &err); /* 延時該 task */
.
. /* 延時結束後執行的程式碼 */
.
}
}


void Task2 (void *pdata)
{
INT8U err;


for (;;) {
OSMboxPost(MboxTimeDly, (void *) 1); /* 取消 task 1 的延時 */
.
.
}
}



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)
{
OS_EVENT *pevent;
OS_Q *pq;


OS_ENTER_CRITICAL();

pevent = OSEventFreeList; (1)
if (OSEventFreeList != (OS_EVENT *) 0) {
OSEventFreeList = (OS_EVENT *) OSEventFreeList->OSEventPtr; (2)
}

OS_EXIT_CRITICAL();

if (pevent != (OS_EVENT *) 0) {
OS_ENTER_CRITICAL();
pq = OSQFreeList; (3)

if (OSQFreeList != (OS_Q *) 0) {
OSQFreeList = OSQFreeList->OSQPtr;
}

OS_EXIT_CRITICAL();

if (pq != (OS_Q *) 0) {
pq->OSQStart = start; (4)
pq->OSQEnd = &start[size];
pq->OSQIn = start;
pq->OSQOut = start;
pq->OSQSize = size;
pq->OSQEntries = 0;
pevent->OSEventType = OS_EVENT_TYPE_Q; (5)
pevent->OSEventPtr = pq; (6)
OSEventWaitListInit(pevent); (7)

} else {
OS_ENTER_CRITICAL();
pevent->OSEventPtr = (void *) OSEventFreeList; (8)
OSEventFreeList = pevent;
OS_EXIT_CRITICAL();
pevent = (OS_EVENT *) 0;
}
}
return (pevent); (9)
}

    全站熱搜

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