uCOS-II任務間通信之信号量
信号量是什麼?信号量有什麼用?
信号量是可以用來表示一個或多個事件的發生,還可以用來對共享資源的通路。
uCOS-II提供了5個對信号量進行操作的函數。如下所示:
1. 建立一個信号量 -- OSSemCreate()
2. 等待一個信号量 -- OSSemPend()
3. 發送一個信号量 -- OSSemPost()
4. 無等待地請求一個信号量 -- OSSemAccept()
5. 查詢一個信号量的目前狀态 -- OSSemQuery()
OSSemCreate()的實作代碼如下:
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)
}
在MCU看來,建立一個信号量就是申請一個事件控制塊,接着初始化這個事件控制塊。
首先,它從空閑任務控制塊連結清單中得到一個事件控制塊(1),并對空閑事件控制連結清單的指針進行适當的調整,使它指向下一個空閑的事件控制塊(2)。如果這時有任務控制塊可用(3),就将該任務控制塊的事件類型設定成信号量OS_EVENT_TYPE_SEM(4)。其它的信号量操作函數OSSem???()通過檢查該域來保證所操作的任務控制塊類型的正确。例如,這可以防止調用OSSemPost()函數對一個用作郵箱的任務控制塊進行操作。
接着,用信号量的初始值對任務控制塊進行初始化(5)(如果信号量是用來表示一個或者多個事件的發生,那麼該信号量的初始值應設為0,如果信号量是用于對共享資源的通路,那麼該信号量的初始值應設為1,并調用OSEventWaitListInit()函數對事件控制任務控制塊的等待任務清單進行初始化(6)。因為信号量正在被初始化,是以這時沒有任何任務等待該信号量。
最後,OSSemCreate()傳回給調用函數一個指向任務控制塊的指針。以後對信号量的所有操作,如OSSemPend(), OSSemPost(), OSSemAccept()和OSSemQuery()都是通過該指針完成的。是以,這個指針實際上就是該信号量的句柄。如果系統中沒有可用的任務控制塊,OSSemCreate()将傳回一個NULL指針。
建立好一個信号之後,可以調用OSSemQuery()查詢一個信号的狀态。該函數有兩個參數:一個是指向信号量對應事件控制塊的指針pevent,另一個是指向用于記錄信号量資訊的資料結構OS_SEM_DATA。
簡單來說就是把信号量對應的事件控制塊的資訊複制到資料結構OS_SEM_DATA。
OSSemQuery()程式代碼如下:
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);
}
當我們建立好了信号量之後,就可以使用信号量了。使用信号量需要調用OSSemPost()和OSSemPend()。關于具體怎麼使用信号量,就得先看看這兩個系統函數的代碼。
先看等待信号量OSSemPend()的代碼:
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;
}
}
}
在MCU看來,這麼多代碼就是做了一個重要的事情:就是将任務控制塊中的狀态标志.OSTCBStat置1,也就是把任務置于睡眠狀态。這樣任務就處于等待信号量來激活任務的狀态了。可以這樣了解,當任務等待一個信号量的時候,任務是被挂起了,需要等待發送信号量來激活。
下面具體分析下代碼的實作過程。
首先檢查指針pevent所指的任務控制塊是否是由OSSemCreate()建立的(1)。如果信号量目前是可用的(信号量的計數值大于0)(2),将信号量的計數值減1 (3),然後函數将“無錯”錯誤代碼傳回給它的調用函數。顯然,如果正在等待信号量,這時的輸出正是我們所希望的,也是運作OSSemPend()函數最快的路徑。
如果此時信号量無效(計數器的值是0),OSSemPend()函數要進一步檢查它的調用函數是不是中斷服務子程式(4)。在正常情況下,中斷服務子程式是不會調用OSSemPend()函數的。這裡加入這些代碼,隻是為了以防萬一。當然,在信号量有效的情況下,即使是中斷服務子程式調用的OSSemPend(),函數也會成功傳回,不會出任何錯誤。
OSSemPend()函數通過将任務控制塊中的狀态标志.OSTCBStat置1,把任務置于睡眠狀态(5),等待時間也同時置入任務控制塊中(6),該值在OSTimeTick()函數中被逐次遞減。注意,OSTimeTick()函數對每個任務的任務控制塊的.OSTCBDly域做遞減操作(隻要該域不為0)。真正将任務置入睡眠狀态的操作在OSEventTaskWait()函數中執行(7)。
因為目前任務已經不是就緒态了,是以任務排程函數将下一個最高優先級的任務調入,準備運作(8)。當信号量有效或者等待時間到後,調用OSSemPend()函數的任務将再一次成為最高優先級任務。這時OSSched()函數傳回。這之後,OSSemPend()要檢查任務控制塊中的狀态标志,看該任務是否仍處于等待信号量的狀态(9)。如果是,說明該任務還沒有被OSSemPost()函數發出的信号量喚醒。事實上,該任務是因為等待逾時而由TimeTick()函數把它置為就緒狀态的。這種情況下,OSSemPend()函數調用OSEventTO()函數将任務從等待任務清單中删除(10),并傳回給它的調用任務一個“逾時”的錯誤代碼。如果任務的任務控制塊中的OS_STAT_SEM标志位沒有置位,就認為調用OSSemPend()的任務已經得到了該信号量,将指向信号量ECB的指針從該任務的任務控制塊中删除,并傳回給調用函數一個“無錯”的錯誤代碼(11)。
接下來看下發送信号量的代碼:
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);
}
}
}
了解了等待信号量函數的源碼後了解發送信号量函數的源碼也就很容易了。
以上源碼的作用簡單來說就是查找有沒有任務在等待這個信号量,如果有,就把該任務從睡眠态拉回就緒态。
首先檢查參數指針pevent指向的任務控制塊是否是OSSemCreate()函數建立的(1),接着檢查是否有任務在等待該信号量(2)。如果該任務控制塊中的.OSEventGrp域不是0,說明有任務正在等待該信号量。這時,就要調用函數OSEventTaskRdy(),把其中的最高優先級任務從等待任務清單中删除(3)并使它進入就緒狀态。然後,調用OSSched()任務排程函數檢查該任務是否是系統中的最高優先級的就緒任務(4)。如果是,這時就要進行任務切換[當OSSemPost()函數是在任務中調用的],準備執行該就緒任務。如果不是,OSSched()直接傳回,調用OSSemPost()的任務得以繼續執行。如果這時沒有任務在等待該信号量,該信号量的計數值就簡單地加1 (5)。