淺析μC/OS-ⅡAPI的設計思想及實現機制

作者:上海交通大學計算機科學與工程系 孫高鑫

任何一個操作系統都會提供大量的API供程序員使用,μC/OS-也不例外。由於μC/OS-面向的是嵌入式開發,並不要求大而全,所以內核提供的API也就大多和多任務息息相關。本文通過分析μC/OS-Ⅱ中提供的API來引出μC/OS-Ⅱ中API的設計思路和實現機制。

 

       API全稱Application Programming Interface,中文是應用程序編程接口的意思。API是操作系統提供給用戶的一組函數,供用戶在編寫應用程序時調用,可以完成應用程序對操作系統的各種調用,包括進程調度、存儲管理、圖形設備接口及文件管理等。μC/OS-Ⅱ作爲一個嵌入式操作系統,相對於其他操作系統,有很多自己的特色,在設計思路和實現機制上都和一般操作系統有很大的不同。

 

    1. 簡介

 

 任何一個操作系統都會提供大量的API供程序員使用,μC/OS-也不例外。由於μC/OS-面向的是嵌入式開發,並不要求大而全,所以內核提供的API也就大多和多任務息息相關。μC/OS-Ⅱ的API主要分以下幾類:(1)任務類、(2)消息類、(3)同步類、(4)時間類、(5)臨界區與事件類等。下面分別從這幾類API分析各自的設計思路和實現機制。

 

    2. 任務類API的設計思路和實現機制

 

μC/OS-Ⅱ可以管理多達64個任務,並從中保留了四個最高優先級和四個最低優先級的任務供自己使用,所以用戶可以使用的只有56個任務。任務類API包括如何在用戶的應用程序中建立任務、刪除任務、改變任務的優先級、掛起和恢復任務,以及獲得有關任務的信息等。

 

     2.1 建立任務API

   

    想讓μC/OS-Ⅱ管理用戶的任務,用戶必須要先建立任務。用戶可以通過傳遞任務地址和其它參數到以下兩個函數之一來建立任務:OSTaskCreate() OSTaskCreateExt()

 

    OSTaskCreate()μC/OS是向下兼容的,OSTaskCreateExt()OSTaskCreate()的擴展版本,提供了一些附加的功能。用兩個函數中的任何一個都可以建立任務。任務可以在多任務調度開始前建立,也可以在其它任務的執行過程中被建立。在開始多任務調度(即調用OSStart())前,用戶必須建立至少一個任務。任務不能由中斷服務程序(ISR)來建立。

   

    OSTaskCreate()的函數定義如下。從中可以知道,OSTaskCreate()需要四個參數:task是任務代碼的指針,pdata是當任務開始執行時傳遞給任務的參數的指針,ptos是分配給任務的堆棧的棧頂指針,prio是分配給任務的優先級。

INT8U OSTaskCreate (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio)

 

OSTaskCreateExt()函數來建立任務會更加靈活,但會增加一些額外的開銷。

 

OSTaskCreateExt()需要九個參數!前四個參數(task,pdata,ptosprio)OSTaskCreate()的四個參數完全相同,連先後順序都一樣。這樣做的目的是爲了使用戶能夠更容易地將用戶的程序從OSTaskCreate()移植到OSTaskCreateExt()上去。函數的定義如下:

INT8U OSTaskCreateExt (void   (*task)(void *pd),

                       void    *pdata,

                       OS_STK  *ptos,

                       INT8U    prio,

                       INT16U   id,

                       OS_STK  *pbos,

                       INT32U   stk_size,

                       void    *pext,

                       INT16U   opt)

 

    2.2 刪除任務API

 

有時候刪除任務是很有必要的。刪除任務,是說任務將返回並處於休眠狀態,並不是說任務的代碼被刪除了,只是任務的代碼不再被μC/OS-Ⅱ調用。通過調用OSTaskDel()就可以完成刪除任務的功能。OSTaskDel()一開始應確保用戶所要刪除的任務並非是空閒任務,因爲刪除空閒任務是不允許的。不過,用戶可以刪除statistic任務。接着,OSTaskDel()還應確保用戶不是在ISR例程中去試圖刪除一個任務,因爲這也是不被允許的。調用此函數的任務可以通過指定OS_PRIO_SELF參數來刪除自己。接下來OSTaskDel()會保證被刪除的任務是確實存在的。該函數的入口參數很簡單,只需要知道要刪除任務的優先級即可。

INT8U OSTaskDel (INT8U prio)

 

    2.3 改變任務優先級API

 

在用戶建立任務的時候會分配給任務一個優先級。在程序運行期間,用戶可以通過調用OSTaskChangePrio()來改變任務的優先級。換句話說,就是μC/OS-Ⅱ允許用戶動態的改變任務的優先級。函數定義如下:

INT8U OSTaskChangePrio (INT8U oldprio, INT8U newprio)

 

用戶不能改變空閒任務的優先級,但用戶可以改變調用本函數的任務或者其它任務的優先級。爲了改變調用本函數的任務的優先級,用戶可以指定該任務當前的優先級或OS_PRIO_SELFOSTaskChangePrio()會決定該任務的優先級。用戶還必須指定任務的新(即想要的)優先級。因爲μC/OS-Ⅱ不允許多個任務具有相同的優先級,所以OSTaskChangePrio()需要檢驗新優先級是否是合法的(即不存在具有新優先級的任務)。如果新優先級是合法的,μC/OS-Ⅱ通過將某些東西儲存到OSTCBPrioTbl[newprio]中保留這個優先級。如此就使得OSTaskChangePrio()可以重新允許中斷,因爲此時其它任務已經不可能建立擁有該優先級的任務,也不能通過指定相同的新優先級來調用OSTaskChangePrio()。接下來OSTaskChangePrio()可以預先計算新優先級任務的OS_TCB中的某些值。而這些值用來將任務放入就緒表或從該表中移除。

 

    2.4 掛起任務和恢復任務API

 

 有時候將任務掛起是很有用的。掛起任務可通過調用OSTaskSuspend()函數來完成。被掛起的任務只能通過調用OSTaskResume()函數來恢復。任務掛起是一個附加功能。也就是說,如果任務在被掛起的同時也在等待延時的期滿,那麼,掛起操作需要被取消,而任務繼續等待延時期滿,並轉入就緒狀態。任務可以掛起自己或者其它任務。

 

 OSTaskSuspend()函數的函數定義如下:

INT8U OSTaskSuspend (INT8U prio)

 

 恢復任務OSTaskResume()函數定義爲:

INT8U OSTaskResume (INT8U prio)

 

被掛起的任務只有通過調用OSTaskResume()才能恢復。因爲OSTaskSuspend()不能掛起空閒任務,所以必須得確認用戶的應用程序不是在恢復空閒任務。注意,這個測試也可以確保用戶不是在恢復優先級爲OS_PRIO_SELF的任務(OS_PRIO_SELF被定義爲0xFF,它總是比OS_LOWEST_PRIO)

 

  2.5 獲得任務信息API

 

用戶的應用程序可以通過調用OSTaskQuery()來獲得自身或其它應用任務的信息。實際上,OSTaskQuery()獲得的是對應任務的OS_TCB中內容的拷貝。用戶能訪問的OS_TCB的數據域的多少決定於用戶的應用程序的配置(參看OS_CFG.H)。由於μC/OS-Ⅱ是可裁剪的,它只包括那些用戶的應用程序所要求的屬性和功能。

void MyTask (void *pdata)

 

函數參數爲一指針變量,指向對應任務的OS_TCB結構地址。本函數是有用的調試工具。

 

  3. 消息和同步類API的設計思路和實現機制

 

 μC/OS-Ⅱ中有三種方法實現消息通信和同步:信號量、郵箱和消息隊列。一個任務或者中斷服務子程序可以通過事件控制塊ECBEvent Control Blocks)來向另外的任務發信號。這裏,所有的信號都被看成是事件(Event)。這也說明爲什麼上面把用於通訊的數據結構叫做事件控制塊。一個任務還可以等待另一個任務或中斷服務子程序給它發送信號。這裏要注意的是,只有任務可以等待事件發生,中斷服務子程序是不能這樣做的。對於處於等待狀態的任務,還可以給它指定一個最長等待時間,以此來防止因爲等待的事件沒有發生而無限期地等下去。多個任務可以同時等待同一個事件的發生。在這種情況下,當該事件發生後,所有等待該事件的任務中,優先級最高的任務得到了該事件並進入就緒狀態,準備執行。上面講到的事件,可以是信號量、郵箱或者消息隊列等。當事件控制塊是一個信號量時,任務可以等待它,也可以給它發送消息。

 

μC/OS-II中的信號量由兩部分組成:一個是信號量的計數值,它是一個16位的無符號整數(0 65,535之間);另一個是由等待該信號量的任務組成的等待任務表。用戶要在OS_CFG.H中將OS_SEM_EN開關量常數置成1,這樣μC/OS-II才能支持信號量。

 

郵箱是μC/OS-II中另一種通訊機制,它可以使一個任務或者中斷服務子程序向另一個任務發送一個指針型的變量。該指針指向一個包含了特定“消息”的數據結構。爲了在μC/OS-II中使用郵箱,必須將OS_CFG.H中的OS_MBOX_EN常數置爲1

 

消息隊列是μC/OS-II中另一種通訊機制,它可以使一個任務或者中斷服務子程序向另一個任務發送以指針方式定義的變量。因具體的應用有所不同,每個指針指向的數據結構變量也有所不同。爲了使用μC/OS-II的消息隊列功能,需要在OS_CFG.H 文件中,將OS_Q_EN常數設置爲1,並且通過常數OS_MAX_QS來決定μC/OS-II支持的最多消息隊列數。

 

μC/OS-Ⅱ提供一系列API函數供用戶調用,實現各個任務之間的通信和同步功能。下面以信號量爲例說明各個API的實現。

 

μC/OS-II提供了5個對信號量進行操作的函數。它們是:OSSemCreate()OSSemPend()OSSemPost()OSSemAccept()OSSemQuery()函數。圖 F6.5說明了任務、中斷服務子程序和信號量之間的關係。圖中用鑰匙或者旗幟的符號來表示信號量:如果信號量用於對共享資源的訪問,那麼信號量就用鑰匙符號。符號旁邊的數字N代表可用資源數。對於二值信號量,該值就是1;如果信號量用於表示某事件的發生,那麼就用旗幟符號。這時的數字N代表事件已經發生的次數。從圖 F6.5中可以看出OSSemPost()函數可以由任務或者中斷服務子程序調用,而OSSemPend()OSSemQuery()函數只能有任務程序調用。

 

 

3.1建立一個信號量, OSSemCreate()

OS_EVENT *OSSemCreate (INT16U cnt)

 

函數參數傳遞的是要創建的信號量的初始值,在函數內部對任務控制塊進行初始化。OSSemCreate()返回給調用函數一個指向任務控制塊的指針。以後對信號量的所有操作,如OSSemPend(), OSSemPost(), OSSemAccept()OSSemQuery()都是通過該指針完成的。因此,這個指針實際上就是該信號量的句柄。如果系統中沒有可用的任務控制塊,OSSemCreate()將返回一個NULL指針。

 

值得注意的是,在μC/OS-II中,信號量一旦建立就不能刪除了,因此也就不可能將一個已分配的任務控制塊再放回到空閒ECB鏈表中。如果有任務正在等待某個信號量,或者某任務的運行依賴於某信號量的出現時,刪除該任務是很危險的。

 

  3.2等待一個信號量, OSSemPend()

void OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *err)

 

它首先檢查指針pevent所指的任務控制塊是否是由OSSemCreate()建立的。如果信號量當前是可用的(信號量的計數值大於0),將信號量的計數值減1,然後函數將“無錯”錯誤代碼返回給它的調用函數。顯然,如果正在等待信號量,這時的輸出正是我們所希望的,也是運行OSSemPend()函數最快的路徑。

 

  3.3發送一個信號量, OSSemPost()

INT8U OSSemPost (OS_EVENT *pevent)

 

它首先檢查參數指針pevent指向的任務控制塊是否是OSSemCreate()函數建立的,接着檢查是否有任務在等待該信號量。如果該任務控制塊中的.OSEventGrp域不是0,說明有任務正在等待該信號量。這時,就要調用函數OSEventTaskRdy(),使一個任務進入就緒狀態,把其中的最高優先級任務從等待任務列表中刪除並使它進入就緒狀態。然後,調用OSSched()任務調度函數檢查該任務是否是系統中的最高優先級的就緒任務。如果是,這時就要進行任務切換[OSSemPost()函數是在任務中調用的],準備執行該就緒任務。如果不是,OSSched()直接返回,調用OSSemPost()的任務得以繼續執行。如果這時沒有任務在等待該信號量,該信號量的計數值就簡單地加1

 

上面是由任務調用OSSemPost()時的情況。當中斷服務子程序調用該函數時,不會發生上面的任務切換。如果需要,任務切換要等到中斷嵌套的最外層中斷服務子程序調用OSIntExit()函數後才能進行。

 

  3.4無等待地請求一個信號量, OSSemAccept()

INT16U OSSemAccept (OS_EVENT *pevent)

 

當一個任務請求一個信號量時,如果該信號量暫時無效,也可以讓該任務簡單地返回,而不是進入睡眠等待狀態。這種情況下的操作是由OSSemAccept()函數完成的。該函數在最開始也是檢查參數指針pevent指向的事件控制塊是否是由OSSemCreate()函數建立的,接着從該信號量的事件控制塊中取出當前計數值,並檢查該信號量是否有效(計數值是否爲非0值)。如果有效,則將信號量的計數值減1,然後將信號量的原有計數值返回給調用函數。調用函數需要對該返回值進行檢查。如果該值是0,說明該信號量無效。如果該值大於0,說明該信號量有效,同時該值也暗示着該信號量當前可用的資源數。應該注意的是,這些可用資源中,已經被該調用函數自身佔用了一個(該計數值已經被減1)。中斷服務子程序要請求信號量時,只能用OSSemAccept()而不能用OSSemPend(),因爲中斷服務子程序是不允許等待的。

 

  3.5查詢一個信號量的當前狀態, OSSemQuery()

INT8U OSSemQuery (OS_EVENT *pevent, OS_SEM_DATA *pdata)

 

在應用程序中,用戶隨時可以調用函數OSSemQuery()來查詢一個信號量的當前狀態。該函數有兩個參數:一個是指向信號量對應事件控制塊的指針pevent。該指針是在生產信號量時,由OSSemCreate()函數返回的;另一個是指向用於記錄信號量信息的數據結構OS_SEM_DATA(見uCOS_II.H)的指針pdata。因此,調用該函數前,用戶必須先定義該結構變量,用於存儲信號量的有關信息。在這裏,之所以使用一個新的數據結構的原因在於,調用函數應該只關心那些和特定信號量有關的信息,而不是象OS_EVENT數據結構包含的很全面的信息。該數據結構只包含信號量計數值.OSCnt和等待任務列表.OSEventTbl[].OSEventGrp,而OS_EVENT中還包含了另外的兩個域.OSEventType.OSEventPtr

 

  4. 時間類API的設計思路和實現機制

 

μC/OS-(其它內核也一樣)要求用戶提供定時中斷來實現延時與超時控制等功能。這個定時中斷叫做時鐘節拍,它應該每秒發生10100次。時鐘節拍的實際頻率是由用戶的應用程序決定的。時鐘節拍的頻率越高,系統的負荷就越重。

 

下面主要講述五個與時鐘節拍有關的API函數。

 

  4.1 任務延時函數OSTimeDly()

void OSTimeDly (INT16U ticks)

 

這應該程序員們調用最多的一個函數了,這個函數完成功能很簡單,就是先掛起當起當前任務,然後進行任務切換,在指定的時間到來之後,將當前任務恢復爲就緒狀態,但是並不一定運行,如果恢復後是優先級最高就緒任務的話,那麼運行之。簡單點說,就是可以任務延時一定時間後再次執行它,或者說,暫時放棄CPU的使用權。一個任務可以不顯式的調用這些可以導致放棄CPU使用權的API,但那樣多任務性能會大大降低,因爲此時僅僅依靠時鐘機制在進行任務切換。一個好的任務應該在完成一些操作主動放棄使用權。

 

  4.2 按時分秒延時函數 OSTimeDlyHMSM()

INT8U OSTimeDlyHMSM (INT8U hours, INT8U minutes, INT8U seconds, INT16U milli)

 

 OSTimeDly()雖然是一個非常有用的函數,但用戶的應用程序需要知道延時時間對應的時鐘節拍的數目。增加了OSTimeDlyHMSM()函數後,用戶就可以按小時(H)、分(M)、秒(S)和毫秒(m)來定義時間了,這樣會顯得更自然些。與OSTimeDly()一樣,調用OSTimeDlyHMSM()函數也會使μC/OS-Ⅱ進行一次任務調度,並且執行下一個優先級最高的就緒態任務。任務調用OSTimeDlyHMSM()後,一旦規定的時間期滿或者有其它的任務通過調用OSTimeDlyResume()取消了延時,它就會馬上處於就緒態。同樣,只有當該任務在所有就緒態任務中具有最高的優先級時,它纔會立即運行。

 

  4.3 讓處在延時期的任務結束延時OSTimeDlyResume()

INT8U OSTimeDlyResume (INT8U prio)

 

μC/OS-Ⅱ允許用戶結束延時正處於延時期的任務。延時的任務可以不等待延時期滿,而是通過其它任務取消延時來使自己處於就緒態。這可以通過調用OSTimeDlyResume()和指定要恢復的任務的優先級來完成。實際上,OSTimeDlyResume()也可以喚醒正在等待事件的任務,雖然這一點並沒有提到過。在這種情況下,等待事件發生的任務會考慮是否終止等待事件。

 

  4.4 系統時間,OSTimeGet()OSTimeSet()

INT32U OSTimeGet (void)

void OSTimeSet (INT32U ticks)

 

用戶可以通過調用OSTimeGet()來獲得該計數器的當前值。也可以通過調用OSTimeSet()來改變該計數器的值。注意,在訪問OSTime的時候中斷是關掉的。這是因爲在大多數8位處理器上增加和拷貝一個32位的數都需要數條指令,這些指令一般都需要一次執行完畢,而不能被中斷等因素打斷。

 

  5. 臨界區API的設計思路和實現機制

 

和其它內核一樣,μC/OS-Ⅱ爲了處理臨界段代碼需要關中斷,處理完畢後再開中斷。這使得μC/OS-Ⅱ能夠避免同時有其它任務或中斷服務進入臨界段代碼。

 

μC/OS-Ⅱ定義兩個宏(macros)來關中斷和開中斷,以便避開不同C編譯器廠商選擇不同的方法來處理關中斷和開中斷。μC/OS-Ⅱ中的這兩個宏調用分別是:OS_ENTER_CRITICAL()OS_EXIT_CRITICAL()。因爲這兩個宏的定義取決於所用的微處理器,故在文件OS_CPU.H中可以找到相應宏定義。每種微處理器都有自己的OS_CPU.H文件。

 

  5.1 OS_ENTER_CRITICAL

 

很多人都以爲它是個函數,其實不然,仔細分析一下OS_CPU.H文件,它和下面馬上要談到的OS_EXIT_CRITICAL都是宏。他們都是涉及特定CPU的實現。一般都被替換爲一條或者幾條嵌入式彙編代碼。由於系統希望向上層程序員隱藏內部實現,故而一般都宣稱執行此條指令後系統進入臨界區。其實,它就是關個中斷而已。這樣,只要任務不主動放棄CPU使用權,別的任務就沒有佔用CPU的機會了,相對這個任務而言,它就是獨佔了。所以說進入臨界區了。這個宏能少用還是少用,因爲它會破壞系統的一些服務,尤其是時間服務。並使系統對外界響應性能降低。

 

  5.2 OS_EXIT_CRITICAL

 

這個是和上面介紹的宏配套使用另一個宏,它在系統手冊裏的說明是退出臨界區。其實它就是重新開中斷。需要注意的是,它必須和上面的宏成對出現,否則會帶來意想不到的後果。最壞的情況下,系統會崩潰。我們推薦程序員們儘量少使用這兩個宏調用,因爲他們的確會破壞系統的多任務性能。

 

  6. 結束語

 

通過對μC/OS-II中這幾類API的簡單分析,我們可以看出μC/OS-II作爲一個多任務、搶佔式嵌入式操作系統,其API都是爲多任務及其多任務之間的調度和通信的實現來設計的。μC/OS-II提供的API簡潔而靈活,使用戶在設計實際應用時能夠很方便的利用系統提供的各種調用,充分發揮μC/OS-II實時、高效的優點。

 

  參考文獻:

 

  [1]  Jean J.Labroase 著,邵貝貝 等譯,嵌入式實時操作系統μC/OS-[M].北京航空航天大學出版社,20035

  [2]  王田苗,嵌入式系統設計與實例開發基於ARM微處理器與μC/OS-Ⅱ實時操作系統[M].清華大學出版社,2002.9

  [3]  姚傳安,宋寅卯.μC/OS-II 實時內核下的A/D驅動程序設計. 鄭州輕工業學院學報.2003.10

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章