uC/OS-ll

μC/OS 和μC/OS-II 是專門為計算機的嵌入式套用設計的, 絕大部分代碼是用C語言編寫的。CPU 硬體相關部分是用彙編語言編寫的、總量約200行的彙編語言部分被壓縮到最低限度,為的是便於移植到任何一種其它的CPU 上。

uC/OS-II簡介

u C / O S 是一種公開原始碼、結構小巧、具有可剝奪實時核心的實時作業系統,商業套用需要付費。

μC/OS-II 的前身是μC/OS,最早出自於1992 年美國嵌入式系統專家Jean J.Labrosse 在《嵌入式系統編程》雜誌的5 月和6 月刊上刊登的文章連載,並把μC/OS 的源碼發布在該雜誌的B B S 上。

用戶只要有標準的ANSI 的C交叉編譯器,有彙編器、連線器等軟體工具,就可以將μC/OS-II嵌人到開發的產品中。μC/OS-II 具有執行效率高、占用空間小、實時性能優良和可擴展性強等特點, 最小核心可編譯至 2KB。μC/OS-II 已經移植到了幾乎所有知名的CPU 上。

嚴格地說uC/OS-II只是一個實時作業系統核心,它僅僅包含了任務調度,任務管理,時間管理,記憶體管理和任務間的通信和同步等基本功能。沒有提供輸入輸出管理,檔案系統,網路等額外的服務。但由於uC/OS-II良好的可擴展性和源碼開放,這些非必須的功能完全可以由用戶自己根據需要分別實現。

uC/OS-II目標是實現一個基於優先權調度的搶占式的實時核心,並在這個核心之上提供最基本的系統服務,如信號量,信箱,訊息佇列,記憶體管理,中斷管理等。

uC/OS-II工作原理

uC/OS-II是一種基於優先權的可搶先的硬實時核心。

要實現多任務機制,那么目標CPU必須具備一種在運行期更改PC的途徑,否則無法做到切換。不幸的是,直接設定PC指針,目前還沒有哪個CPU支持這樣的指令。但是一般CPU都允許通過類似JMP,CALL這樣的指令來間接的修改PC。我們的多任務機制的實現也正是基於這個出發點。事實上,我們使用CALL指令或者軟中斷指令來修改PC,主要是軟中斷。但在一些CPU上,並不存在軟中斷這樣的概念,所以,我們在那些CPU上,使用幾條PUSH指令加上一條CALL指令來模擬一次軟中斷的發生。

在uC/OS-II里,每個任務都有一個任務控制塊(Task Control Block),這是一個比較複雜的數據結構。在任務控制塊的偏移為0的地方,存儲著一個指針,它記錄了所屬任務的專用堆疊地址。事實上,在uC/OS-II內,每個任務都有自己的專用堆疊,彼此之間不能侵犯。這點要求程式設計師再他們的程式中保證。一般的做法是把他們申明成靜態數組。而且要申明成OS_STK類型。當任務有了自己的堆疊,那么就可以將每一個任務堆疊在那裡記錄到前面談到的任務控制快偏移為0的地方。以後每當發生任務切換,系統必然會先進入一個中斷,這一般是通過軟中斷或者時鐘中斷實現。然後系統會先把當前任務的堆疊地址保存起來,僅接著恢復要切換的任務的堆疊地址。由於哪個任務的堆疊里一定也存的是地址(還記得我們前面說過的,每當發生任務切換,系統必然會先進入一個中斷,而一旦中斷CPU就會把地址壓入堆疊),這樣,就達到了修改PC為下一個任務的地址的目的。

任務管理

uC/OS-II 中最多可以支持64 個任務,分別對應優先權0~63,其中0 為最高優先權。63為最低級,系統保留了4個最高優先權的任務和4個最低優先權的任務,所有用戶可以使用的任務數有56個。

uC/OS-II提供了任務管理的各種函式調用,包括創建任務,刪除任務,改變任務的優先權,任務掛起和恢復等。

系統初始化時會自動產生兩個任務:一個是空閒任務,它的優先權最低,該任務僅給一個整型變數做累加運算;另一個是系統任務,它的優先權為次低,該任務負責統計當前cpu的利用率。

時間管理

uC/OS-II的時間管理是通過定時中斷來實現的,該定時中斷一般為10毫秒或100毫秒發生一次,時間頻率取決於用戶對硬體系統的定時器編程來實現。中斷髮生的時間間隔是固定不變的,該中斷也成為一個時鐘節拍。

uC/OS-II要求用戶在定時中斷的服務程式中,調用系統提供的與時鐘節拍相關的系統函式,例如中斷級的任務切換函式,系統時間函式。

記憶體管理

在ANSI C中是使用malloc和free兩個函式來動態分配和釋放記憶體。但在嵌入式實時系統中,多次這樣的操作會導致記憶體碎片,且由於記憶體管理算法的原因,malloc和free的執行時間也是不確定。

uC/OS-II中把連續的大快記憶體按分區管理。每個分區中包含整數個大小相同的記憶體塊,但不同分區之間的記憶體塊大小可以不同。用戶需要動態分配記憶體時,系統選擇一個適當的分區,按塊來分配記憶體。釋放記憶體時將該塊放回它以前所屬的分區,這樣能有效解決碎片問題,同時執行時間也是固定的。

任務間通信與同步

對一個多任務的作業系統來說,任務間的通信和同步是必不可少的。uC/OS-II中提供了4中同步對象,分別是信號量,信箱,訊息佇列和事件。所有這些同步對象都有創建,等待,傳送,查詢的接口用於實現進程間的通信和同步。

任務調度

uC/OS-II 採用的是可剝奪型實時多任務核心。可剝奪型的實時核心在任何時候都運行就緒了的最高優先權的任務。

uC/os-II的任務調度是完全基於任務優先權的搶占式調度,也就是最高優先權的任務一旦處於就緒狀態,則立即搶占正在運行的低優先權任務的處理器資源。為了簡化系統設計,uC/OS-II規定所有任務的優先權不同,因為任務的優先權也同時唯一標誌了該任務本身。

任務調度的時機

1) 高優先權的任務因為需要某種臨界資源,主動請求掛起,讓出處理器,此時將調度就緒狀態的低優先權任務獲得執行,這種調度也稱為任務級的上下文切換。

2) 高優先權的任務因為時鐘節拍到來,在時鐘中斷的處理程式中,核心發現高優先權任務獲得了執行條件(如休眠的時鐘到時),則在中斷態直接切換到高優先權任務執行。這種調度也稱為中斷級的上下文切換。

這兩種調度方式在uC/OS-II的執行過程中非常普遍,一般來說前者發生在系統服務中,後者發生在時鐘中斷的服務程式中。

調度工作的內容可以分為兩部分:最高優先權任務的尋找和任務切換。其最高優先權任務的尋找是通過建立就緒任務表來實現的。u C / O S 中的每一個任務都有獨立的堆疊空間,並有一個稱為任務控制塊TCB(Task Control Block)的數據結構,其中第一個成員變數就是保存的任務堆疊指針。任務調度模組首先用變數OSTCBHighRdy 記錄當前最高級就緒任務的TCB 地址,然後調用OS_TASK_SW()函式來進行任務切換。

μC/OS-II的組成部分

組成部分

μC/OS-II可以大致分成核心、任務處理、時間處理、任務同步與通信,CPU的移植等5個部分。

1) 核心部分(OSCore.c)

是作業系統的處理核心,包括作業系統初始化、作業系統運行、中斷退出的前導、時鐘節拍、任務調度、事件處理等多部分。能夠維持系統基本工作的部分都在這裡。

2) 任務處理部分(OSTask.c)

任務處理部分中的內容都是與任務的操作密切相關的。包括任務的建立、刪除、掛起、恢復等等。因為μC/OS-II是以任務為基本單位調度的,所以這部分內容也相當重要。

3) 時鐘部分(OSTime.c)

μC/OS-II中的最小時鐘單位是timetick(時鐘節拍)。任務延時等操作是在這裡完成的。

4) 任務同步和通信部分

為事件處理部分,包括信號量、信箱、信箱佇列、事件標誌等部分;主要用於任務間的互相聯繫和對臨界資源的訪問。

5) 與CPU的接口部分

是指μC/OS-II針對所使用的CPU的移植部分。由於μC/OS-II是一個通用性的作業系統,所以對於關鍵問題上的實現,還是需要根據具體CPU的具體內容和要求作相應的移植。這部分內容由於牽涉到SP等系統指針,所以通常用彙編語言編寫。主要包括中斷級任務切換的底層實現、任務級任務切換的底層實現、時鐘節拍的產生和處理、中斷的相關處理部分等內容。

uC/OS-II的任務切換機理及中斷調度最佳化

引 言

在嵌入式作業系統領域,由Jean J. Labrosse開發的μC/OS,由於開放原始碼和強大而穩定的功能,曾經一度在嵌入式系統領域引起強烈反響。而其本人也早已成為了嵌入式系統會議(美國)的顧問委員會的成員。

不管是對於初學者,還是有經驗的工程師,μC/OS開放原始碼的方式使其不但知其然,還知其所以然。通過對於系統內部結構的深入了解,能更加方便地進行開發和調試;並且在這種條件下,完全可以按照設計要求進行合理的裁減、擴充、配置和移植。通常,購買RTOS往往需要一大筆資金,使得一般的學習者望而卻步;而μC/OS對於學校研究完全免費,只有在套用於盈利項目時才需要支付少量的著作權費,特別適合一般使用者的學習、研究和開發。自1992第1版問世以來,已有成千上萬的開發者把它成功地套用於各種系統,安全性和穩定性已經得到認證,現已經通過美國FAA認證。

μC/OS-II的幾大組成部分

μC/OS-II可以大致分成核心、任務處理、時間處理、任務同步與通信,CPU的移植等5個部分。

核心部分(OSCore.c) 是作業系統的處理核心,包括作業系統初始化、作業系統運行、中斷進出的前導、時鐘節拍、任務調度、事件處理等多部分。能夠維持系統基本工作的部分都在這裡。

任務處理部分(OSTask.c) 任務處理部分中的內容都是與任務的操作密切相關的。包括任務的建立、刪除、掛起、恢復等等。因為μC/OS-II是以任務為基本單位調度的,所以這部分內容也相當重要。

時鐘部分(OSTime.c) μC/OS-II中的最小時鐘單位是timetick(時鐘節拍)。任務延時等操作是在這裡完成的。

任務同步和通信部分 為事件處理部分,包括信號量、信箱、信箱佇列、事件標誌等部分;主要用於任務間的互相聯繫和對臨界資源的訪問。

與CPU的接口部分 是指μC/OS-II針對所使用的CPU的移植部分。由於μC/OS-II是一個通用性的作業系統,所以對於關鍵問題上的實現,還是需要根據具體CPU的具體內容和要求作相應的移植。這部分內容由於牽涉到SP等系統指針,所以通常用彙編語言編寫。主要包括中斷級任務切換的底層實現、任務級任務切換的底層實現、時鐘節拍的產生和處理、中斷的相關處理部分等內容。

對於MSP430的中斷處理

2.1 函式調用和中斷調用的操作

MSP430最常使用的C編譯器應該就是IAR Embedd-ed WorkBench。對於這一編譯器來說,通過分析和研究,發現它有以下規律。

(1)函式調用

如果是函式級調用,編譯器會在函式調用時先把當前函式PC壓棧,然後調用函式,PC值改變。

如果被調用的函式帶有參數,那么,編譯器按照以下的規則進行。

最左邊的兩個參數如果不是struct(結構體)或者union(聯合體),將被賦值到暫存器,否則將被壓棧。函式剩下的參數都將被壓棧。根據最左邊的那兩個參數的類型,分別賦值給R12(對於32位類型賦值給R12:R13)和R14(對於32位類型賦值給R14:R15)。

(2)中斷調用

如果是在中斷中調用中斷服務子程式的話,編譯器將把當前執行語句的PC壓棧,同時再把SR壓棧。接著,根據中斷服務子程式的複雜程度,選擇把R12~R15中的暫存器壓棧。然後,執行中斷服務子程式。中斷處理結束後再把Rx暫存器出棧,SR出棧,PC出棧。把系統恢復到中斷前的狀態,使程式接著被中斷的部分繼續運行。

圖3 中斷髮生時的任務棧壓棧操作

2.2 任務級和中斷級的任務切換步驟和原理

(1)任務級的任務切換原理

μC/OS-II是一個多任務的作業系統,在沒有用戶自己定義的中斷情況下,任務間的切換步驟是這樣的:任務間的切換一般會調用OSSched()函式。函式的結構如下:

void OSSched(void){

關中斷

如果(不是中斷嵌套並且系統可以被調度){

確定優先權最高的任務

如果(最高級的任務不是當前的任務){

調用OSCtxSw();

}

}

開中斷

}

我們把這個函式稱作任務調度的前導函式。它先判斷要進行任務切換的條件,如果條件允許進行任務調度,則調用OSCtxSw()。這個函式是真正實現任務調度的函式。由於期間要對堆疊進行操作,所以OSCtxSw()一般用彙編語言寫成。它將正在運行的任務的CPU的SR暫存器推入堆疊,然後把R4~R15壓棧。接著把當前的SP保存在TCB->OSTCBStkPtr中,然後把最高優先權的TCB->OSTCBStkPtr的值賦值給SP。這時候,SP就已經指到最高優先權任務的任務堆疊了。然後進行出棧工作,把R15~R4出棧。接著使用RETI返回,這樣就把SR和PC出棧了。簡單地說,μC/OS-II切換到最高優先權的任務,只是恢復最高優先權任務所有的暫存器並運行中斷返回指令(RETI),實際上,所作的只是人為地模仿了一次中斷。

(2)中斷級的任務切換原理

μC/OS-II的中斷服務子程式和一般前後台的操作有少許不同,往往需要這樣操作:

保存全部CPU暫存器

調用OSIntEnter()或OSIntNesting++

開放中斷

執行用戶代碼

關閉中斷

調用OSIntExit();

恢復所有CPU暫存器

RETI

OSIntEnter()就是將全局變數OSIntNesting加1。OSIntNesting是中斷嵌套層數的變數。μC/OS-II通過它確保在中斷嵌套的時候,不進行任務調度。執行完用戶的代碼後,μC/OS-II調用OSIntExit(),一個與OSSched()很像的函式。在這個函式中,系統首先把OSIntNesting減1,然後判斷是否中斷嵌套。如果不是的話,並且當前任務不是最高優先權的任務,那么找到優先權最高的任務,執行OSIntCtxSw()這一出中斷任務切換函式。因為,在這之前已經做好了壓棧工作;在這個函式中,要進行R15~R4的出棧工作。而且,由於在之前調用函式的時候,可能已經有一些暫存器被壓入了堆疊。所以要進行堆疊指針的調整,使得能夠從正確的位置出棧。

使用μC/OS-II存在的問題和解決方法

由於μC/OS-II在套用的時候會占用單片機上的一些資源,如系統時鐘、RAM、Flash或者ROM,從而減少了用戶程式對資源的利用。對於MSP430來說,RAM的占用是特別突出的問題。對於8、16位的單片機來說,片內的RAM容量都很小,MSP430也是如此(最大的片內RAM也只有2KB,例如MSP430F149)。如果使用擴展記憶體,會大大增加設計難度。

通過對μC/OS-II的分析可以得知,μC/OS-II占用的RAM主要是用在每個任務的TCB、每個任務的堆疊等方面。通過進一步分析,發現任務堆疊大的原因是因為MSP430的硬體設計中沒有把中斷堆疊和任務堆疊分開。這樣就造成了在套用μC/OS-II的時候,考慮每個任務的任務堆疊大小時,不單單需要計算任務中局部變數和函式嵌套層數,還需要考慮中斷的最大嵌套層數。因為,對於μC/OS-II原始的中斷處理的設計、中斷處理過程中的中斷嵌套中所需要壓棧的暫存器大小和局部變數的記憶體大小,都需要算在每個任務的任務堆疊中,則對於每一個任務都需要預留這一部分記憶體,所以大量的RAM被浪費。從這裡可以看出,解決這一問題的直接方法就是把中斷堆疊和每個任務自己的堆疊分開。這樣,在計算每個任務堆疊的時候,就不需要把中斷處理中(包括中斷嵌套過程中)的記憶體的占用計算到每個任務的任務堆疊中,只需要計算每個任務本身需要的記憶體大小,從而提高了RAM的利用率,可以緩解記憶體緊張的問題。

在這種設計方案中,中斷堆疊區也就是利用原有的MSP430中的系統堆疊區。在前後台的設計形式中,中斷中的壓棧和出棧的操作都是在系統的堆疊區完成的。基於μC/OS-II的任務切換的原理,我們對於任務堆疊的功能和系統堆疊的功能做了以下劃分:任務在運行過程中產生中斷和任務切換的時候,PC和SR以及暫存器Rx都保存在各個任務自己的任務堆疊中;而中斷嵌套產生的壓棧和出棧的操作都是放在系統堆疊中進行的。這種劃分方式是基於儘量將中斷任務與普通任務分開的思想設計的。

從前面對於IAR EW的默認操作分析來看,堆疊的結構可以有兩種。一種是把μC/OS-II的任務堆疊設計成圖1所示的形式。這種方法是把編譯器默認的壓棧操作放在前面,然後再把剩下的暫存器進棧。但是,由於編譯器在處理複雜程度不同的中斷服務程式的時候,壓入棧的暫存器的數量不定,所以會對以後其餘暫存器的壓棧和出棧操作增加複雜度。這裡,我們採用了圖2所示的方式生成堆疊。在這種堆疊中,PC和SR壓棧後,通過調整SP指針,使得R4~R15暫存器覆蓋編譯器默認壓棧的暫存器。這樣,處理的難度會小一點。

對於這樣的設計方式,CPU必須能夠:

◆ 有相應的CPU暫存器能夠模仿SP的一些功能,能使用相應的指令來完成類似SP的一些操作;

◆ 作為SP使用的暫存器在編譯過程中最好不被編譯器默認使用。在IAR的編譯器中,有一個選項可以避免在編譯過程中使用到R4、R5。

這兩點MSP430都可以做到。

下面對一個正在運行的優先權為6的任務中斷後,會發生的幾種情況進行分析。

1)在中斷的處理過程中沒有更高優先權的中斷產生,即不會產生中斷嵌套。

圖3所示為中斷髮生後對於任務優先權為6的任務堆疊所進行的操作。中斷髮生後,PC和SR被系統壓棧②,對於IAR C編譯器來說,會按照複雜度不同的中斷服務程式的要求,默認地進行一些暫存器的壓棧操作③。因為我們要求的堆疊格式是如圖2所示的,我們要把SP調整到SR後面④,然後進行R4~R15的壓棧操作,形成我們所要求的堆疊格式⑤。

進行任務堆疊的壓棧工作以後,就可以調整SP的指針到系統堆疊了,如圖4所示。壓棧後的SP指向最後一個壓棧內容①。我們把SP的值賦值給優先權6任務的TCB->OSTCBStkPtr,以便進行任務調度的時候出棧使用②。接著,就把SP調整到系統堆疊處③。在中斷處理過程中,可能會出現壓棧的操作,那么這種情況下SP的指針會隨之移動。由於現在是中斷堆疊中,所以不會破壞任務堆疊的格式。

由於沒有中斷嵌套,在中斷處理中沒有別的中斷髮生,那么返回的步驟和上述的進棧操作正好相反。在中斷處理完了以後,SP會自動回到圖4中③的SP位置。接著,系統會查詢到優先權最高的任務,然後把SP的指針移到優先權最高的任務的任務堆疊,進行R15~R4的出棧工作,最後用RETI中斷返回指令返回到新的任務。因為我們把所有的任務堆疊都規定成相同的格式,所以它們之間不會產生問題。這裡需要注意的是,因為系統在C編譯器的中斷處理中會對中斷進入時默認壓棧的暫存器出棧,所以在設計出棧的程式時,要先把這些內容壓棧,這樣才能正確出棧。

2)在中斷的處理過程中,有別的中斷產生,產生中斷嵌套。

如圖5所示,由於在處理中斷的時候,SP已經被移到系統堆疊去了,只有當中斷退出的時候才可能把SP移到別的任務的任務堆疊中。所以在中斷的時候進行中斷嵌套,那么對於中斷的處理和第一次是一樣的,所不同的是,這次保存在堆疊中的不是任務運行中的暫存器,而是中斷處理中的暫存器,而且是保存在系統堆疊中而不是任務堆疊中。從這裡就可以看出最佳化記憶體的效果。所有的中斷嵌套中的暫存器壓棧都壓在系統堆疊中,這樣對於任務堆疊記憶體大小的要求大大降低。

因為μC/OS-II在進入中斷中,會把全局變數OSIntNesting++;在退出中斷的時候,又會把OSIntNesting--。在退出中斷進行任務切換之前,μC/OS-II會先判斷OSIntNesting是否為0,是0才會進行任務調度。當第二中斷運行結束以後,退出中斷嵌套的時候,OSIntNesting不為0,也就不會進行任務調度。因此,仍舊在系統堆疊出棧,那么系統會繼續前面沒有完成的中斷服務程式。

接著退出中斷的順序和非中斷嵌套的順序是一樣的。在中斷處理完以後,SP會自動回到圖4中③的SP位置。接著,系統會查詢到優先權最高的任務,然後把SP的指針移到優先權最高的任務的任務堆疊。進行R15~R4的出棧工作,最後用RETI中斷返回指令返回到新的任務。

中斷的情況基本上就是上述兩種。對於有些文獻中提到的在中斷中會調度到更高優先權的任務的情況,筆者覺得是不應該發生的。因為從上面的分析可以看出,默認的(μC/OS-II的設計思路)中斷處理會同時對全局變數OSIntNesting進行增減處理,以給出是否需要任務調度的條件。那么即使在中斷服務程式中把更高優先權的任務就緒,也會等到中斷退出以後再進行調度,除非是在中斷中直接調用更高優先權的任務函式。但這種方法應該是和μC/OS-II的原則相違背的,沿用的是以前前後台設計的思路。

對於這樣的設計方式,時鐘節拍的處理方式必須和一般的中斷處理方式是一樣的。一般來說,MSP430使用WATCHDOG時鐘中斷作為時鐘節拍的產生源。從本質上來說,時鐘節拍本身也是中斷處理過程,所以對於時鐘節拍的處理應該和其它的中斷處理過程相同。實際上,在時鐘節拍的處理過程中也可能會存在中斷嵌套的問題。

中斷堆疊和任務堆疊分離設計的程式流程如圖6所示。

幾點建議

① 編寫中斷程式的時候,有條件儘量使用彙編語言。因為這樣可以避免一些編譯器自己進行的操作,減少指針調整的次數。

② 在用C編寫中斷服務的時候,因為有些功能必須調用彙編的函式才能實現。調用函式時,有些時候壓棧的PC會破壞堆疊的結構。這個時候需要把堆疊進行適當的調整,保證堆疊格式的正確。

③ 中斷處理過程中調用OSIntExit()的時候,由於 μC/OS-II的原始設計中SP指針有時是不調整的,所以在OSIntExit()返回了以後,還要判斷一下是否中斷嵌套。因為有的時候是需要切換任務的。

(綜合電子論壇)

熱門詞條

聯絡我們