μC/OS-II是一種公開源代碼、結構小巧、具有可剝奪實時內核的嵌入式開發系統,代碼簡短、條理清晰、實時性及安全性能很高,絕大部分代碼用C編寫,現已被移植到多種處理器的構架中。隨著51單片機片內資源的日益豐富,在51單片機上移植μC/OS-II已成為可能,植入系統后,由系統來管理軟件與硬件資源,簡化應用程序的設計,并且使應用系統功能更加完善。因此在51單片機上移植μC/OS-II具有十分重要的意義。
1 μC/OS實時操作系統概述
μC/OS-II實時操作系統是一種可移植、可固化、可裁剪即可剝奪型的多任務實時內核,適用于各種微處理器和微控制器。μC/OS-II主要包括任務調度、時間管理、內存管理、事件管理(信號量、郵箱、消息隊列)4大部分。它的移植與4個文件相關:匯編文件(OS_CPU_A.A SM)、處理器相關C文件(OS_CPU.H、OS_CPU_C.C)和配置文件(OS_CFG.H)。有64個優先級,系統占用8個,用戶可創建56任務,不支持時間片輪轉。
它的基本思路就是“近似地每時每刻總是讓優先級最高的就緒任務處于運行狀態”。為了保證這一點,它在調用系統函數、中斷結束、定時中斷結束時總是執行調度算法。原作者通過事先計算好數據,簡化了運算量,通過精心設計就緒表結構,使得延時可預知。任務的切換是通過模擬一次中斷實現的。
2 任務調度的實現原理
任務調度是μC/OS-II的重要部分,和具體的微處理器關系緊密。必須移植的5個函數有4個都和任務有關。任務調度就是保存當前任務的寄存器和PC指針(即當前任務的斷點),然后把將要執行的任務的寄存器值返回給寄存器并把PC指向將要執行任務的斷點。這些的實現要借助于堆棧和中斷,為了簡便起見,先看函數調用時堆棧的使用情況。在函數調用時,堆棧的一個重要功能就是保存被調函數的斷點地址。若有4個函數,Fun1調用Fun2,Fun2調用Fun3,Fun3調用Fun4,Fun4為葉子程序(無子程序調用)。
假設現在從Fun1一直運行到Fun4,此時堆棧結構如圖1所示,中間的ADD_A到ADD_D為堆棧中的數據,左邊的SP到SP-7為堆棧指針,右邊的Fun1到Fun4為對應的調用函數。運行Fun4時,此時SP與SP-1所存的值為ADD_D,而ADD_D為Fun3中子函數Fun4的下一行的地址,即Fun3中3-2行的地址,以此類推,ADD_C為2-2行地址,ADD_B 圖1函數運行及堆棧結構圖為1-2行地址。
當函數A調用函數B時,進入函數B時就會把函數A的斷點地址壓棧,而當函數B運行結束時則把堆棧中函數A的斷點地址彈出到PC指針,程序接著從函數A的斷點開始運行。如果在函數B中更改SP及SP-1中的數據,則函數B運行結束時就不會再返回函數A中,而返回到SP及SP-1更改后的數據所代表的地址。
以上是函數調用時的基本情況,如果是中斷則堆棧不僅保存斷點地址還會自動保存寄存器的值。任務調度就是靠中斷來實現,中斷中所保存的斷點地址就是任務的斷點地址,當本任務要再次執行時就把斷點地址賦給PC就可以接著任務被中斷時地址順序執行。
3 頭文件移植
與移植相關的4個文件中有2個頭文件,這2個頭文件的移植比較簡單,可以參考其它的移植程序。其中OS_CPU.H中主要是數據類型的定義、堆棧生長方向的定義、開關中斷的定義以及函數級任務切換的宏定義。OS_CFG.H中主要是任務數、優先級數、事件數、每秒中斷節拍數以及各種系統函數的使能定義。
4 匯編與C文件的移植
在要移植的匯編與C的兩個文件中有14個函數,其中9個是接口函數,可根據實際需要來決定,有5個是必須寫的。這5個函數分別是:OS_CPU_C.C文件中的OSTaskStkInit()和OS_CPU_A.ASM文件中的OSStartHighRdy()、OSCtxSw()、OSINTCtxSw()與OSTICkISR()。下面就這5個函數來做具體分析。
4.1 任務堆棧初始化函數OSTaskStkInit()
此函數是在任務創建函數OSTaskCreat()或OSTaskCreatExt()中調用的。因為系統為每個任務申請了一個數組作為棧,當一個任務運行時,就把堆棧指針指向本任務的棧,任務堆棧初始化函數就是在任務創建時將要創建任務的堆棧進行初始化。但C51的堆棧指針SP是8位的,只能在片內RAM的256個字節內尋址。因其尋址空間有限且SP唯一,不能像DSP或ARM那樣為每一段程序或每一種模式定義堆棧,需小心管理堆棧空間。為了適應上述情況,需要換一種思路,不是讓SP去指向各任務堆棧空間,而是把各任務堆棧空間的內容復制到系統棧中。至于堆棧數組空間要有多大以及堆棧數組空間里放些什么內容,可以借鑒keil中中斷函數的壓棧情況,當中斷函數不指定寄存器組時,編譯器一般將PC、ACC、B、DPTR、PSW、R0~R7寄存器入棧,其中PC和DPTR是雙字節的,其它都是單字節的,一共15個字節,所以把堆棧數組設計成至少15個字節的,以保證任務所用的寄存器都在堆棧數組中包含著。因為每個數組里放的是寄存器的值,在此就把這每個任務的堆棧數組叫做寄存器數組,暫且把寄存器數組設計成15個字節,依次存放PC、ACC、B、DPTR、PSW、R0~R7。
函數OSTaskStkInit()傳遞4個參數,第1個參數task是所創建任務的起始地址,這個參數須保存到PC在寄存器數組的對應位置,第2個參數ppdata是所創建任務的參數,C51規則中用R1~R3來傳遞參數指針,這個參數須存放到R1~R3在寄存器數組中的對應位置。第3個參數ptos是棧底指針,從當前地址開始初始化堆棧指針,第4個參數opt是附加參數,一般不用。
4.2 運行等待任務中優先級最高任務函數OSStartHighRdy()
此函數在啟動操作系統函數OSStart()的最后一行調用,且此函數不返回,經過此函數后μC/OS接管系統。OSStartHighRdy()不是去調用用戶任務函數,而是讓PC指針指向任務函數首地址。且任務函數的傳遞參數只有一個,若此參數正確,則可保證任務函數運行正確。在調用OSStartHighRdy()之前OSStart()已經把最高優先級任務的任務表準備好了,只要把最高優先級任務表的數據恢復到堆棧中,再執行返回指令即可,以上最關鍵的是如何讓其返回到最高優先級任務中而不是返回到被調函數中。
當函數OSStart()調用函數OSStartHighRdy()時,斷點地址入棧;當OSStartHighRdy()執行完之后,返回斷點。在OSStartHighRdy()中把SP及SP-1的值改為最高優先級任務的地址,這樣OSStartHighRdy()就會返回到最高優先級任務中去運行。
4.3 任務級的任務切換函數OSCtxSw()
此函數是保存當前任務的狀態,然后運行處于就緒態中的最高優先級任務。前面介紹過不是更改SP去指向寄存器數組,而是把寄存器數組的數復制到堆棧中。先看下一般的情況,在用戶任務MyTask(void*ppdtat)中調用TimeDly(),TimeDly()中調用OSSched(),在OSSched()中有一個宏OS_TASK_SW(),這個宏的目的是讓程序進人函數OSCtxSw()。參看圖1,就如Fun4為OSCtxSw(),Fun3為OSSched(),Fun2為TimeDly(),Fun1為MyTask()。ADD_D存的是OSSched()的斷點,ADD_C為TimeDly()的斷點,ADD_B為MyTask()的斷點。如果進行任務切換,應該把高優先級任務的地址值賦給ADD_B(即SP-4與SP-5)。
以上考慮的是最簡單的情況,當任務比較復雜時,可能更改了ACC、PSW、DPTR或R0~R7的值,在進入高優先任務時,寄存器并不是此任務的寄存器值,運行的結果可能不正確。
在上述情況下如何保證CPU寄存器的值正確,要分兩個階段。第一個階段是把CPU寄存器值保存到要掛起任務的寄存器數組中,當剛進入OSCtxSw()時,CPU寄存器的值是要掛起任務的寄存器值,所以一開始就要鎖定CPU寄存器的值。如果OS_TASK_SW()定義為中斷的話,在進入OSCtxSw()時,CPU寄存器的值被自動壓棧;如果把OS_TASK_SW()定義為函數時,在進入函數時使用內嵌匯編的方法把CPU寄存器入棧。這時堆棧中又壓入了13個字節,就如在圖1的ADD_D上又壓入了13個字節的數據,然后從堆棧中把值取出來放到相應任務的寄存器數組中。第二個階段是把將要執行任務的寄存器數組的值復制到堆棧中。此時PC指針在堆棧中對應的位置是SP-17與SP-18,SP到SP-12的13個字節對應ACC、B、DPTR、PSW、R0~R7。
4.4 中斷級的任務切換函數OSINTCtxSw()
此函數和上一個函數基本思想一致,都要保存當前任務的狀態,運行處于就緒態中的優先級最高的任務。二者的不同在于,上個函數的堆棧中SP-17與SP-18是PC值的位置,SP到SP-12是13個寄存器的位置。當中斷來時,在中斷中調用函數OSIntExit(),函數OSIntExit()調用函數OSIntCtxSw(),在OSIntCtxSw()中實現任務切換。在進入函數OSIntExit()之前寄存器的值已經入棧,所以運行到本函數時堆棧中SP-17與SP-18是PC值的位置。SP-4到SP-16是13個寄存器的位置。在圖1上,上個函數的13個寄存器的值被壓入ADD_D上面的13個字節中,而本函數是在ADD_B于ADD_C之間壓入的這13個寄存器。
4.5周期節拍中斷函數OSTICkISR()
這個函數是給系統提供一個節拍,一般每秒10~100次。如果節拍頻率太高,μC/OS系統會占用大量硬件資源;如果太低,任務間的切換又會很慢。
此函數首先要保證產生一個周期性的中斷,可以使用硬件定時器,也可以從交流電中獲得50/60Hz的時鐘頻率。這個函數至少要做3件事:1)進入中斷時,把中斷嵌套層數計數器加1,說明又進入一次中斷,也可以直接調用OSIntEnter()函數;2)調用時鐘節拍函數OSTimeTick(),告知系統又經過了一個節拍;3)調用OSIntExit()函數,說明要退出中斷了,此函數會自動處理。
5 結束語
文中闡述了在堆棧空間有限的51單片機上運行μC/OS-II系統的移植過程,利用系統棧SP作為數據交換的樞紐。在實際應用中,如果用系統棧來移植,只需根據文中的基本思想進行適當的改寫,即可運行于其他處理器上。如果處理器的堆棧指針尋址空間足夠大,也可以為每個任務開辟一個棧,通過改變堆棧指針指向不同任務的棧空間,來實現任務調度。
通過在51單片機上的運行,可以看出μC/OS-II也能在堆棧空間比較少的CPU上運行。
評論
查看更多