引言 市面上有很多優秀的嵌入式實時操作系統(RTOS),但在中低端微控制器(MCU)上運行性能良好的RTOS內核并不多。在高檔機下,功能強大、運行極好的嵌入式實時操作系統,移植到中低端機上時性能很可能大幅度下降。一個很重要的原因就是它的大部分功能對中低檔系統來說是不需要的,反而成為制約性能的累贅。中低檔微控制器與高檔機相比,一方面,尋址能力有限,處理速度慢,在相同的實時性能要求下,對內核的代碼效率的要求更為嚴格;另一方面,中低檔機完成的任務相對簡單,減少了對內核的功能需求,比如可以不需要內存管理。從嵌入式系統的共性來說,大多數情況下用戶程序和系統內核是緊密結合在一起的,運行時存儲器容量消耗、任務的數量、執行時間和結果都是可以預計的,這可進一步縮小對內核的功能需求。 事件驅動的觀點認為,任務應該是被動地響應外界發生的各種事件,而不是主動地去“查詢”,浪費處理器時間。采用事件驅動編程的方法,不僅提高了運行效率,而且降低了事件處理之間的耦合,使程序流程非常清晰,從而可大大提高開發效率。 充分考慮中低端微控制器的硬件特點和嵌入式系統軟件的需求,引入“事件驅動”的觀念,筆者開發了一個微型的搶占式多任務RTOS內核—— MicroStar。支持任務的動態創建、刪除、睡眠、掛起和恢復,提供消息(message)和信號(signal)兩種任務間的通信方案、完善的定時器服務和功能齊全的任務同步函數庫。限于篇幅,著重論述幾個與眾不同的設計思路和實現難點。 1 調度策略 1.1 基于事件的優先級 對內核的實時性能來說,調度策略是關鍵。好的調度策略,既要體現各任務因所處理的事件對實時性的不同要求而帶來的優先級差異,又要保證一定的公平性,避免出現低優先級任務長時間得不到執行的極端情形。常用的調度策略有兩種:一種是按時間片輪轉(round robin)調度,如RTX51;另一種是嚴格按優先級的占先式調度,如μC/OS。 按時間片輪轉調度能很好地保證公平,但優先級的差異是通過對處理器的占用時間的多少來體現的。如果各個任務都不主動放棄執行,高優先級的任務能夠比低優先級任務獲得更多的處理器時間;但在嵌入式系統中,某個事件要求實時處理,并不意味著該處理需要較長的時間,而往往是要求盡快響應。因此,采用按時間片輪轉調度,實時性不會太好。 如果嚴格按任務的優先級來調度,可極大地提升系統的實時性,但卻欠缺公平。如果高優先級任務是個無等待的死循環,低優先級任務就無法獲得執行機會。 一個好的辦法是兩者的結合,即可由任務的優先級產生調度,也可以由時間片到產生新的任務調度,如VxWorks;但是實現起來較為復雜,不一定適合中低檔MCU。為此,基于以下事實,提出“基于事件的優先級(events based priority)”這一新觀念。 ① 一個任務往往處理多個事件,各個事件對實時性的要求不盡相同。一般的RTOS下,任務的優先級是根據這些事件中對實時性要求最高的一個來確定的。因此,高優先級任務在處理對實時性要求不高的事件時,完全可能會妨礙低優先級任務處理具有一定實時性要求的事件。 ② 有些情況下,對同一事件的處理可分為前臺處理和后臺處理:前臺處理所需時間短,對實時性有較高的要求;后臺處理花費時間長,對實時性則無多大要求。 如果根據正在處理和等待處理的事件對實時性的不同要求,更細致地按事件處理的前后臺階段,動態地調整任務的優先級,采用優先級調度策略,既可發揮實時性好的優點,又可在一定限度內保證公平。這種情況下,任務的優先級不再是一成不變的,而是動態地取決于所處理的事件和處理階段,這就是所謂的 “基于事件的優先級”。 1.2 在MicroStar中的實現 MicroStar中任務的優先級是由靜態優先級和動態優先級共同決定的。靜態優先級等同于其它RTOS中的優先級;動態優先級為基于事件的優先級——由內核根據任務正在處理和等待處理的事件動態調整。靜態優先等級限定為0~15級,不允許創建靜態優先級相同的任務。動態優先等級目前只有 0(亦稱緊急級)、1(亦稱普通級)兩級。任務的實際優先等級可由下式來計算: 優先等級=動態優先等級×16 + 靜態優先等級。 優先等級值越大,優先級越低。可以看出,動態優先級起決定作用。 怎樣實現優先級動態可調呢?首先簡要介紹MacroStar中任務的四個狀態: 休眠(dormant)——任務因調用睡眠函數、掛起函數或者等待內核同步對象而進入休眠態; 等待(waiting)——任務因等待消息或者信號(勿與“信標”、“信號量”相混淆)而進入等待態; 就緒(ready)——任務運行的條件都已俱備,只等被調度,稱為就緒態,亦稱可調度態; 運行(running)——任務正在使用處理器的資源,稱為運行態。 這些狀態都是用標志位來實現的。16個靜態優先級對應的任務的某一狀態剛好可用一個16位的二進制數來標識。休眠態用 os_slpState來表示,從高位算起,第N位為0表示靜態優先級為N的任務處于休眠態。等待態是依據“事件驅動”觀念而專為消息和信號而設計的,用 os_rdyhState和os_rdyState兩個16位的變量來記錄。只有當os_rdyhState和os_rdyState的第N位均為0時,才表示靜態優先級為N的任務處于等待態。如果任務處于非等待狀態,意味著任務已在處理事件或者有事件要處理(可以認為任務一開始就處理“啟動”這個“虛擬事件”),這時,才有動態優先級的概念。如果os_rdyhState中的第N位為1,表示靜態優先級為N的任務的動態優先級為緊急級;如果 os_rdyhState第N位為0,則表示靜態優先級為N的任務的動態優先級為普通級。要求實時處理的事件發生后,內核簡單將os_rdyhState 相應位置1,提升任務的動態優先級;當前事件處理完畢后,如果已無實時性要求較高的事件等待處理,簡單地將os_rdyhState相應位清0,降低任務的動態優先級。由此,即可實現優先級的動態可調。只有當任務既不處在休眠態也不處在等待態時,任務才是可以調度的。 2 任務管理 2.1 任務控制塊 多任務系統中用任務控制塊(TCB)來記錄任務的各種屬性。在這些屬性中,最重要的是任務堆棧棧頂地址。進行上下文切換(context switch)時,被停止執行的任務的所有寄存器狀態、下一條代碼的地址都要入棧保護,因而這個屬性是必需的。如果允許修改任務的優先級,優先級屬性也是必需的。所以,將任務控制塊簡化如下: typedef struct{ uint_16 msg[2]; /*消息接收區*/ int * sp; /*堆棧棧頂指針*/ uchar priority; /*靜態優先級*/ uchar reserved; /*保留 */ }TCB,*PTCB; TCB os_tcbs[ USER_TASK_NUM +1 ]; /*用戶任務數最多為15個*/ msg用來存儲發送給任務的消息,兩個16位的二進制可按位存放32個消息。sp指向任務堆棧棧頂。priority記錄任務的靜態優先級。數組os_tcbs用來記錄系統所有任務的信息,其下標與任務的ID號相對應,即ID號為N的任務的控制塊為os_tcbs[N]。 2.2 任務的創建 os_CreateTask函數用來創建一個任務: void os_CreateTask( TASKPROC task, //任務函數的指針 uchar taskId, //任務的ID號 uchar priority, //優先級 int * pStack, //任務堆棧棧底地址 void * param //任務函數的入口參數 ); typedef void (*TASKPROC)( void * param); 創建任務時,內核要做以下幾方面的工作:① 初始化任務控制塊;② 初始化任務堆棧,使其如同被其它任務搶斷時的情形;③ 將任務狀態置為就緒態。該函數是依賴于處理器的,圖1是較為通用的描述。 中斷程序中,在高優先級任務剝奪低優先級任務之前,內核將斷點時的各寄存器狀態入棧保護,這部分區域即為寄存器映像區。將任務退出函數 os_Exit的地址先于任務函數MyTask入棧,以使MyTask函數退出后返回到os_Exit中去,由此來實現任務的自動刪除。 2.3 任務切換 與任務創建一樣,任務切換代碼與硬件相關。在PC機上,代碼和步驟如下: void interrupt os_Schedule( ) …………(1) { if( os_nLayers )return; os_nLayers++; …………(2) _DX = (int)os_pCurTCB; /*os_pCurTCB指向當前任務的控制塊*/ *(int*)(_DX+4) = _SP; *(int*)(_DX+6) = _SS; …………(3) os_GetReadyTask( ); …………(4) _DX = (int)os_pCurTCB; _SP = *(int*)(_DX+4); _DX = *(int*)(_DX+6); _SS = _DX; …………(5) os_nLayers--;? …………(6) UNLOCK_INT( ); } …………(7) (1)利用C語言interrupt關鍵字使各寄存器入棧保護。(2)鎖定調度器,不允許重調度。(3)將當前任務的棧頂地址(由堆棧段寄存器SS和棧指針寄存器SP組成)保存在os_pCurTCB->sp中(PC機下,TCB中的sp定義為遠指針類型)。(4)選出優先級最高的就緒任務(方法類似于μC/OS),并將os_pCurTCB指向新任務的控制塊。(5)棧寄存器指向新任務的棧頂地址。(6)解鎖調度器。(7)各寄存器出棧,恢復到上次被中斷時的情形。 3 消息與信號 為很好地支持事件驅動編程,MicroStar借鑒了Windows的“基于消息,事件驅動”觀念,并加以擴展。在MicroStar中,事件不僅可以觸發消息、信號,而且由事件觸發的消息或信號是有優先級的,這是因為不同事件對處理的實時性要求是不同的。內核正是根據消息、信號的優先級來動態調整任務的動態優先級的。 3.1 消 息 消息是一種很友好的通信方式。考慮中低檔單片機的內存容量和需求,將消息簡化為一個0~31的值。采用固定位圖存儲格式,將這32個值映射到任務控制塊的msg域,這大大減小了存儲空間。可將msg域看作一個32位的二進制變量,第i位為1,表示有值為i的消息,因此消息的存取只需通過簡單的“與”、“或”運算。消息的優先級依值而定,值越大,優先級越低。在系統范圍內,消息優先級又分為兩級:緊急級(值0~15)與普通級(值 16~31)。當有緊急消息發送給任務時,內核會提升任務的動態優先級,從而提高消息處理的實時性。當任務無緊急消息要處理時,內核就降低它的動態優先級。發送消息的核心代碼如下: /*const uint_16 os_maskTable[16] ={ 0x8000,0x4000, .....,0x0008,0x0004,0x0002,0x0001 */ if( msg&0xF0 ) { /*普通級消息*/ pTCB->msg[1] |= os_maskTable[msg&0x0F]; /*普通級消息存在msg[1]中*/ os_rdyState |= os_maskTable[pTCB->priority]; } else { /*緊急級消息*/ pTCB->msg[0] |= os_maskTable[msg]; ; /*緊急級消息存在msg[0]中*/ os_rdyhState |= os_maskTable[pTCB->priority]; /*提升動態優先級*/ } 與先進先出(FIFO)方式的消息隊列不同,內核總是取出優先級最高的消息來交給任務處理。消息接收函數os_GetMessage設計思路如下:如果消息接收區中無緊急消息,則降低任務的動態優先級;如果消息接收區中有消息,則取出優先級最高的消息;如果沒有消息,則將任務轉為等待態。考慮有時候不希望任務進入等待態,MicroStar還提供了非阻塞的os_PeekMessage消息接收函數。 3.2 信 號 在嵌入式系統編程中,常利用標志位來實現前后臺程序或不同的任務間的通信。MicroStar也提供了類似的任務間的通信方式——信號(signal)。它避免了用戶程序因不斷查詢標志位而帶來的時間浪費,而且支持信號間的“與”、“或”運算。通俗來說,信號就是標志位,用來標識某個事件的發生。同消息一樣,信號也有緊急級與普通級之分。與消息不同的是,信號完全由用戶程序創建和維護,內核只是幫助用戶程序等待信號,以避免低效率的標志位查詢。使用起來不如消息直觀,但執行效率較高。實現起來非常簡單,請參見源碼。 4 定時器 定時器在嵌入式系統有著大量的用途,如LED的定時刷新、串口通信中的超時檢查。對定時器的需求分為兩類,一種是周期性重復定時,比如每隔 10ms去刷新LED;另一種是僅需定時一次的一次性定時。定時時長以系統時鐘節拍(tick,又譯作滴達)作為單位。兩次系統定時中斷之間的時間間隔為一個節拍。定時器結構體如下: typedef struct{ uint_16 elapse; /*定時時長的余值*/ uint_16 backTime; /*定時時長的備份值*/ MSG timerId; /*定時器ID號*/ uchar taskId; /*擁有該定時器的任務的ID*/ TIMERPROC lpTimerFunc; /*定時調用的函數指針*/ }TIMER,*PTIMER; TIMER os_timers[USER_TIMER_NUM]; /*最多為16個*/ 周期性定時和一次性定時是通過timerId來區分的。如果timeId為64,為一次性定時;如果timerId不大于32,則為周期性定時。用os_timers數組記錄定時器信息,用16位的os_timerState表示定時器的狀態。如果os_timerState的二進制數的第 N位為1,則表示os_timers[N]空閑可用。 對周期性定時器,每隔定時時長的時間,內核就調用的lpTimerFunc指向的函數,并且將timerId以消息的方式發送給任務,對任務的動態優先級的影響與普通消息一樣。因此,要想取得實時性較好的定時器,只需將timerId設在0~15之間。與一次性定時相關的是睡眠函數和限時等待同步對象的函數。任務使用這兩個函數而進入休眠態后,在定時時間到時,內核將其恢復為就緒態,并自動釋放定時器資源。系統定時處理的核心代碼如下: if( !(--pTimer->elapse) ){ /*elapse減為零表示時間到*/ if( pTimer->lpTimerFunc)(*pTimer->lpTimerFunc)(pTimer-> taskId,pTimer->timerId); switch( pTimer->timerId&0xF0 ){ case SLEEP_ID: /*一次性定時*/ os_slpState |= taskMask; /*結束休眠態*/ os_timerState |= timerMask; /*釋放定時器*/ break; case 0x00: /*發送緊急級定時器消息*/ pTCB->msg[0] |= os_maskTable[pTimer->timerId]; os_rdyhState |= os_maskTable[pTCB->priority ];; break; case 0x10: : /*發送普通級定時器消息*/ pTCB->msg[1] |= os_maskTable[pTimer->timerId&0x0f]; os_rdyState |= os_maskTable[pTCB->priority ];; } } 5 同 步 搶占式多任務下,低優先級的任務可以被高優先級任務打斷執行。以常規方式訪問共享變量或資源時,會出現奇怪的結果。比如,一個任務調用 printf(“12345”)試圖在輸出設備上輸出“12345”,但執行中被高優先級任務打斷;而高優先級任務也調用printf(“67890”) 試圖輸出“67890”,最終的輸出結果可能是“1267890345”之類。這就是多任務環境下的任務同步問題。 同步方式有兩種,一種為用戶同步方式,不需要與內核打交道,具有速度快的優點,但只適合保護執行時間短的代碼;另一種是內核同步方式,需要通過內核來實現,速度相對較慢,但可保護執行時間長的代碼。 5.1 用戶同步方式 用戶方式下的同步是通過關鍵代碼段(critical section)保護來實現。關鍵代碼段是指這樣一小段代碼,它執行時必須獨占對某些共享資源的訪問權,不允行被其它試圖訪問該資源的代碼打斷。最簡單的是得用關/開中斷來實現,優點是速度極快,缺點是帶來中斷延遲,只適合執行時間極短的代碼段。另一簡單的方案是通過加鎖/解鎖調度器來實現,即在關鍵代碼段執行期間禁止內核進行任務切換。采用這種方法,不會帶來中斷延遲,但帶來了調度延遲。在MicroStar中,對os_nLayers加1即可鎖定調度器,減1即可解鎖。但直接利用解鎖調度器來離開關鍵代碼段并不合適。如果在關鍵代碼段執行中,發生了中斷,使更高優先級任務就緒。但由于調度器被鎖定,中斷程序退出時不能進行任務切換以使高優先級任務執行。因此我們希望,最好一旦調度器解鎖,馬上就切換到高優先級任務。為此,專門用變量os_flag的最低位作為標志位,中斷程序中調用任何可以使任務就緒的系統函數都會影響到該標志位,如os_PostMessage、 os_SetEvent,os_Notity。退出關鍵代碼段時以此來判斷是否需要進行任務調度。離開臨界代碼段時的代碼如下: if( (os_flag&0x01) && (!(--s_nLayers ) ) ) {--os_Schedule( ); } 5.2 內核同步對象 如果要保護執行時間較長的代碼,就要使用內核同步對象來同步。常用的內核同步對象有事件(event)、信標(semaphore,亦稱信號量)和互斥量(mutex)。 事件對象用來通知事件或者操作已經完成,它用一個布爾值來表示該事件處于通知還是未通知狀態。信標對象用于對資源進行計數。它記錄了當前可用的資源數目。當用1來初始化信標對象的可用資源數目時,信標對象實際上成為了互斥對象。MicroStar提供事件和信標兩種同步對象,支持查詢、限時等待或無限時等待操作。內核同步對象的結構如下: typedef struct{ uint_16 waiter; /*等待列表*/ uchar num; /*可用資源數目或者事件狀態*/ uchar type; /*同步對象類型*/ }OBJECT,*POBJECT,*HOBJECT,*HEVENT,*HSEMAPHORE; 當一個任務因等待同步對象而進入休眠態時,它的靜態優先級按位存放在waiter域中。如果靜態優先級為N的任務在等待某個同步對象,則 waiter二進制數中第N位置1,以示等待。當type為EVENT_OBJECT時,表示事件對象,此時num為事件狀態,1表示通知態,0表示未通知態;為SEMAPHORE_OBJECT時,表示信標對象,對應的num為可用資源數。 內核同步對象不是嵌入式多任務系統特有的,通用的多任務操作系統如Windows都提供齊全的同步函數,在此不作介紹。 6 運用和使用示例 在MicroStar中,各個功能模塊是分開的,因而可裁減度高。移植MicroStar也比較容易,只需改寫與硬件相關的任務創建和調度函數。MicroStar1.0的PC機完全版本的代碼約為10KB,針對96單片機用匯編語言寫成的版本為1.4KB。本文附帶的演示示例,都在 TC2.0下編譯通過,可直接在PC機上運行。第一個示例啟動了三個用戶任務:① WatchTask任務在屏幕中央顯示一個以10ms為計時單位的跑表。② KeyTask 任務每隔200ms讀一次鍵盤,按“Q”鍵系統退出執行。③ MicroStar 任務顯示MicroStar相關信息,每隔1.5s更新一幀。 演示程序及內核源碼見本刊網站(www.dpj.com.cn)。 結 語 本文提出了基于事件的優先級這一觀念,使任務優先級的安排更為合理。介紹了微型多任務實時內核——MicroStar的設計與實現。消息和信號兩種通信方式的提供,使其對事件驅動編程有很好的支持。較為完善的定時器服務和齊全的任務同步函數庫,給用戶提供了更多、更靈活的選擇。有限的功能,使其與其它實時操作系統相比,減小了從技術掌握上所花費的時間。加上較低的存儲器消耗,總體上說,MicroStar是比較適合在中低端MCU平臺上運行的。 參考文獻 1. Jeffrey Ricter.王建華 Windows 核心編程 2000 2. Tanenbaum A S.陳向群 現代操作系統 1999 3. Labrosse Jean.邵貝貝 J.μ C/OS-Ⅱ-源碼公開的實時嵌入式操作系統 2001 作 者:北京航空航天大學 鄭玉全 來 源:單片機與嵌入式系統應用2004(1) |