第一章:範例 uC/OS-II
在這一章里將提供三個範例來說明如何使用 uC/OS-II。筆者之所以在本書一開始就寫這一章是為了讓讀者儘快開始使用 uC/OS-II。在開始講述這些例子之前,筆者想先說明一些在這本書里的約定。
這些例子曾經用Borland C/C++ 編譯器(V3.1)編譯過,用選擇項產生Intel/AMD80186處理器(大模式下編譯)的代碼。這些代碼實際上是在Intel Pentium II PC (300MHz)上運行和測試過,Intel Pentium II PC可以看成是特別快的80186。筆者選擇PC做為目標系統是由於以下幾個原因:首先也是最為重要的,以PC做為目標系統比起以其他嵌入式環境,如評估板,模擬器等,更容易進行代碼的測試,不用不斷地燒寫EPROM,不斷地向EPROM模擬器中下載程序等等。用戶只需要簡單地編譯、鏈接和執行。其次,使用Borland C/C++產生的80186的目標代碼(實模式,在大模式下編譯)與所有Intel、AMD、Cyrix公司的80x86 CPU兼容。
Advertisements
1.00 安裝 uC/OS-II
本書附帶一張軟盤包括了所有我們討論的源代碼。是假定讀者在80x86,Pentium,或者Pentium-II處理器上運行DOS或Windows95。至少需要5Mb硬碟空間來安裝uC/OS-II。
請按照以下步驟安裝:
1.進入到DOS(或在Windows 95下打開DOS窗口)並且指定C:為默認驅動器。
2.將磁碟插入到A:驅動器。
3.鍵入 A:INSTALL 【drive】
注意『drive』是讀者想要將uC/OS-II安裝的目標磁碟的盤符。
INSTALL.BAT 是一個DOS的批處理文件,位於磁碟的根目錄下。它會自動在讀者指定的目標驅動器中建立\SOFTWARE目錄並且將uCOS-II.EXE文件從A:驅動器複製到\SOFTWARE並且運行。uC/OS-II將在\SOFTWARE目錄下添加所有的目錄和文件。完成之後INSTALL.BAT將刪除uCOS-II.EXE並且將目錄改為\SOFTWARE\uCOS-II\EX1_x86L,第一個例子就存放在這裡。
Advertisements
在安裝之前請一定閱讀一下READ.ME文件。當INSTALL.BAT已經完成時,用戶的目標目錄下應該有一下子目錄:
l \SOFTWARE
這是根目錄,是所有軟體相關的文件都放在這個目錄下。
l \SOFTWARE\BLOCKS
子程序模塊目錄。筆者將例子中uC/OS-II用到的與PC相關的函數模塊編譯以後放在這個目錄下。
l \SOFTWARE\HPLISTC
這個目錄中存放的是與範例HPLIST相關的文件(請看附錄D,HPLISTC和TO)。HPLIST.C存放在\SOFTWARE\HPLISTC\SOURCE目錄下。DOS下的可執行文件(HPLIST.EXE)存放在\SOFTWARE\TO\EXE中。
l \SOFTWARE\TO
這個目錄中存放的是和範例TO相關的文件(請看附錄D,HPLISTC和TO)。源文件TO.C存放在\SOFTWARE\TO\SOURCE中,DOS下的可執行文件(TO.EXE)存放在\SOFTWARE\TO\EXE中。注意TO需要一個TO.TBL文件,它必須放在根目錄下。用戶可以在\SOFTWARE\TO\EXE目錄下找到TO.TBL文件。如果要運行TO.EXE,必須將TO.TBL複製到根目錄下。
l \SOFTWARE\uCOS-II
與uC/OS-II 相關的文件都放在這個目錄下。
l \SOFTWARE\uCOS-II\EX1_x86L
這個目錄里包括例1的源代碼(參見 1.07, 例1),可以在DOS(或Windows 95下的DOS窗口)下運行。
l \SOFTWARE\uCOS-II\EX2_x86L
這個目錄里包括例2的源代碼(參見 1.08, 例2),可以在DOS(或Windows 95下的DOS窗口)下運行。
l \SOFTWARE\uCOS-II\EX3_x86L
這個目錄里包括例3的源代碼(參見 1.09, 例3),可以在DOS(或Windows 95下的DOS窗口)下運行。
l \SOFTWARE\uCOS-II\Ix86L
這個目錄下包括依賴於處理器類型的代碼。此時是為在80x86處理器上運行uC/OS-II而必須的一些代碼,實模式,在大模式下編譯。
l \SOFTWARE\uCOS-II\SOURCE
這個目錄里包括與處理器類型無關的源代碼。這些代碼完全可移植到其它架構的處理器上。
1.01 INCLUDES.H
用戶將注意到本書中所有的 *.C 文件都包括了以下定義:
#include "includes.h" |
INCLUDE.H可以使用戶不必在工程項目中每個*.C文件中都考慮需要什麼樣的頭文件。換句話說,INCLUDE.H是主頭文件。這樣做唯一的缺點是INCLUDES.H中許多頭文件在一些*.C文件的編譯中是不需要的。這意味著逐個編譯這些文件要花費額外的時間。這雖有些不便,但代碼的可移植性卻增加了。本書中所有的例子使用一個共同的頭文件INCLUDES.H,3個副本分別存放在\SOFTWARE\uCOS-II\EX1_x86L,\SOFTWARE\uCOS-II\EX2_x86L,以及\SOFTWARE\uCOS-II\EX3_x86L 中。當然可以重新編輯INCLUDES.H以添加用戶自己的頭文件。
1.02不依賴於編譯的數據類型
因為不同的微處理器有不同的字長,uC/OS-II的移植文件包括很多類型定義以確保可移植性(參見\SOFTWARE\uCOS-II\Ix86L\OS_CPU.H,它是針對80x86的實模式,在大模式下編譯)。uCOS-II不使用C語言中的short,int,long等數據類型的定義,因為它們與處理器類型有關,隱含著不可移植性。筆者代之以移植性強的整數數據類型,這樣,既直觀又可移植,如表L1.1所示。為了方便起見,還定義了浮點數數據類型,雖然uC/OS-II中沒有使用浮點數。
程序清單 L1.1 可移植型數據類型。 |
Typedef unsigned char BOOLEAN; |
Typedef unsigned char INT8U; |
Typedef signed char INT8S; |
Typedef unsigned int INT16U; |
Typedef signed int INT16S; |
Typedef unsigned long INT32U; |
Typedef signed long INT32S; |
Typedef float FP32; |
Typedef double FP64; |
#define BYTE INT8S |
#define UBYTE INT8U |
#define WORD INT16S |
#define UWORD INT16U |
#define LONG INT32S |
#define ULONG INT32U |
以INT16U數據類型為例,它代表16位無符號整數數據類型。uC/OS-II和用戶的應用代碼可以定義這種類型的數據,範圍從0到65,535。如果將uCO/S-II移植到32位處理器中,那就意味著INT16U不再不是一個無符號整型數據,而是一個無符號短整型數據。然而將無論uC/OS-II用到哪裡,都會當作INT16U處理。 表1.1是以Borland C/C++編譯器為例,為80x86提供的定義語句。為了和uC/OS兼容,還定義了BYTE,WORD,LONG以及相應的無符號變數。這使得用戶可以不作任何修改就能將uC/OS的代碼移植到uC/OS-II中。之所以這樣做是因為筆者覺得這種新的數據類型定義有更多的靈活性,也更加易讀易懂。對一些人來說,WORD意味著32位數,而此處卻意味著16位數。這些新的數據類型應該能夠消除此類含混不請
1.03全局變數
以下是如何定義全局變數。眾所周知,全局變數應該是得到內存分配且可以被其他模塊通過C語言中extern關鍵字調用的變數。因此,必須在 .C 和 .H 文件中定義。這種重複的定義很容易導致錯誤。以下討論的方法只需用在頭文件中定義一次。雖然有點不易懂,但用戶一旦掌握,使用起來卻很靈活。表1.2中的定義出現在定義所有全局變數的.H頭文件中。
程序清單 L 1.2 定義全局宏。 |
#ifdef xxx_GLOBALS |
#define xxx_EXT |
#else |
#define xxx_EXT extern |
#endif |
.H 文件中每個全局變數都加上了xxx_EXT的前綴。xxx代表模塊的名字。該模塊的.C文件中有以下定義:
#define xxx_GLOBALS |
#include "includes.h" |
當編譯器處理.C文件時,它強制xxx_EXT(在相應.H文件中可以找到)為空,(因為xxx_GLOBALS已經定義)。所以編譯器給每個全局變數分配內存空間,而當編譯器處理其他.C文件時,xxx_GLOBAL沒有定義,xxx_EXT被定義為extern,這樣用戶就可以調用外部全局變數。為了說明這個概念,可以參見uC/OS_II.H,其中包括以下定義:
#ifdef OS_GLOBALS |
#define OS_EXT |
#else |
#define OS_EXT extern |
#endif |
OS_EXT INT32U OSIdleCtr; |
OS_EXT INT32U OSIdleCtrRun; |
OS_EXT INT32U OSIdleCtrMax; |
同時,uCOS_II.H有中以下定義:
#define OS_GLOBALS |
#include 「includes.h」 |
當編譯器處理uCOS_II.C時,它使得頭文件變成如下所示,因為OS_EXT被設置為空。
INT32U OSIdleCtr; |
INT32U OSIdleCtrRun; |
INT32U OSIdleCtrMax; |
這樣編譯器就會將這些全局變數分配在內存中。當編譯器處理其他.C文件時,頭文件變成了如下的樣子,因為OS_GLOBAL沒有定義,所以OS_EXT被定義為extern。
extern INT32U OSIdleCtr; |
extern INT32U OSIdleCtrRun; |
extern INT32U OSIdleCtrMax; |
在這種情況下,不產生內存分配,而任何 .C文件都可以使用這些變數。這樣的就只需在 .H 文件中定義一次就可以了。
1.04 OS_ENTER_CRITICAL() 和
OS_EXIT_CRITICAL()
用戶會看到,調用OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()兩個宏,貫穿本書的所有源代碼。OS_ENTER_CRITICAL() 關中斷;而OS_EXIT_CRITICAL()開中斷。關中斷和開中斷是為了保護臨界段代碼。這些代碼很顯然與處理器有關。關於宏的定義可以在OS_CPU.H中找到。9.03.02節詳細討論定義這些宏的兩種方法。
程序清單 L 1.3 進入正確部分的宏。 |
#define OS_CRITICAL_METHOD 2 |
#if OS_CRITICAL_METHOD == 1 |
#define OS_ENTER_CRITICAL() asm CLI |
#define OS_EXIT_CRITICAL() asm STI |
#endif |
#if OS_CRITICAL_METHOD == 2 |
#define OS_ENTER_CRITICAL() asm {PUSHF; CLI} |
#define OS_EXIT_CRITICAL() asm POPF |
#endif |
用戶的應用代碼可以使用這兩個宏來開中斷和關中斷。很明顯,關中斷會影響中斷延遲,所以要特別小心。用戶還可以用信號量來保護林階段代碼。
1.05基於PC的服務
PC.C 文件和 PC.H 文件(在\SOFTWARE\BLOCKS\PC\SOURCE目錄下)是筆者在範例中使用到的一些基於PC的服務程序。與 uC/OS-II 以前的版本(即 uC/OS)不同,筆者希望集中這些函數以避免在各個例子中都重複定義,也更容易適應不同的編譯器。PC.C包括字元顯示,時間度量和其他各種服務。所有的函數都以PC_為前綴。
1.05.01字元顯示
為了性能更好,顯示函數直接向顯示內存區中寫數據。在VGA顯示器中,顯示內存從絕對地址0x000B8000開始(或用段、偏移量表示則為B800:0000)。在單色顯示器中,用戶可以把#define constant DISP_BASE從0xB800改為0xB000。
PC.C中的顯示函數用x和y坐標來直接向顯示內存中寫ASCII字元。PC的顯示可以達到25行80列一共2,000個字元。每個字元需要兩個位元組來顯示。第一個位元組是用戶想要顯示的字元,第二個位元組用來確定前景色和背景色。前景色用低四位來表示,背景色用第4位到6位來表示。最高位表示這個字元是否閃爍,(1)表示閃爍,(0)表示不閃爍。 用PC.H中 #defien constants定義前景和背景色,PC.C包括以下四個函數:
PC_DispClrScr() Clear the screen
PC_DispClrLine() Clear a single row (or line)
PC_DispChar() Display a single ASCII character anywhere on the screen
PC_DispStr() Display an ASCII string anywhere on the screen
1.05.02花費時間的測量
時間測量函數主要用於測試一個函數的運行花了多少時間。測量時間是用PC的82C54定時器2。 被測的程序代碼是放在函數PC_ElapsedStart()和PC_ElapsedStop()之間來測量的。在用這兩個函數之前,應該調用PC_ElapsedInit()來初始化,它主要是計算運行這兩個函數本身所附加的的時間。這樣,PC_ElapsedStop()函數中返回的數值就是準確的測量結果了。注意,這兩個函數都不具備可重入性,所以,必須小心,不要有多個任務同時調用這兩個函數。表1.4說明了如何測量PC_DisplayChar()的執行時間。注意,時間是以uS為單位的。
程序清單 L 1.4 測量代碼執行時間。 |
INT16U time; |
PC_ElapsedInit(); |
. |
. |
PC_ElapsedStart(); |
PC_DispChar(40, 24, 『A』, DISP_FGND_WHITE); |
time = PC_ElapsedStop(); |
1.05.03其他函數
uC/OS-II的應用程序和其他DOS應用程序是一樣的,換句話說,用戶可以像在DOS下編譯其他單線程的程序一樣編譯和鏈接用戶程序。所生成的.EXE程序可以在DOS下裝載和運行,當然應用程序應該從main()函數開始。因為uC/OS-II 是多任務,而且為每個任務開闢一個堆棧,所以單線程的DOS環境應該保存,在退出uC/OS-II 程序時返回到DOS。調用PC_DOSSaveReturn()可以保存當前DOS環境,而調用PC_DOSReturn()可以返回到DOS。 PC.C中使用ANSI C的setjmp(),longjmp()函數來分別保存和恢復DOS環境。Borland C/C++編譯庫提供這些函數,多數其它的編譯程序也應有這類函數。
應該注意到無論是應用程序的錯誤還是只調用exit(0)而沒有調用PC_DOSReturn()函數都會使DOS環境被破壞,從而導致DOS或WINDOWS95下的DOS窗口崩潰。
調用PC_GetDateTime()函數可得到PC中的日期和時間,並且以SACII字元串形式返回。格式是MM-DD-YY HH:MM:SS,用戶需要19個字元來存放這些數據。該函數使用了Borland C/C++的gettime()和getdate()函數,其它DOS環境下的C編譯應該也有類似函數。
PC_GetKey() 函數檢查是否有按鍵被按下。如果有按鍵被按下,函數返回其值。這個函數使用了Borland C/C++的kbhit()和getch()函數,其它DOS環境下的C編譯應該也有類似函數。
函數PC_SetTickRate()允許用戶為 uC /OS-II定義頻率,以改變鍾節拍的速率。在DOS下,每秒產生18.20648次時鐘節拍,或每隔54.925ms一次。這是因為82C54定時器晶元沒有初始化,而使用默認值65,535的結果。如果初始化為58,659,那麼時鐘節拍的速率就會精確地為20.000Hz。筆者決定將時鐘節拍設得更快一些,用的是200Hz(實際是上是 199.9966Hz)。注意OS_CPU_A.ASM中的OSTickISR()函數將會每11個時鐘節拍調用一次DOS中的時鐘節拍處理,這是為了保證在DOS下時鐘的準確性。如果用戶希望將時鐘節拍的速度設置為20HZ,就必須這樣做。在返回DOS以前,要調用PC_SetTickRate(),並設置18為目標頻率,PC_SetTickRate()就會知道用戶要設置為18.2Hz,並且會正確設置82C54。
PC.C中最後兩個函數是得到和設置中斷向量,筆者是用Borland C/C++中的庫函數來完成的,但是PC_VectGet()和PC_VectSet()很容易改寫,以適用於其它編譯器。
1.06 應用 uC/OS-II 的範例
本章中的例子都用Borland C/C++編譯器編譯通過,是在Windows95 的DOS窗口下編譯的。可執行代碼可以在每個範例的OBJ子目錄下找到。實際上這些代碼是在Borland IDE (Integrated Development Environment)下編譯的,編譯時的選項如表1.1所示:
表T1.1 IDE中編譯選項。 | |
Code generation | |
Model | : Large |
Options | : Treat enums as ints |
Assume SSEquals DS | : Default for memory model |
Advanced code generation | |
Floating point | : Emulation |
Instruction set | : 80186 |
Options | : Generate underbars |
Debug info in OBJs | |
Fast floating point | |
Optimizations | |
Optimizations | Global register allocation |
Invariant code motion | |
Induction variables | |
Loop optimization | |
Suppress redundant loads | |
Copy propagation | |
Dead code elimination | |
Jump optimization | |
In-line intrinsic functions | |
Register variables | Automatic |
Common subexpressions | Optimize globally |
Optimize for | Speed |
筆者的Borland C/C++編譯器安裝在C:\CPP目錄下,如果用戶的編譯器是在不同的目錄下,可以在Options/Directories的提示下改變IDE的路徑。
uC/OS-II是一個可裁剪的操作系統,這意味著用戶可以去掉不需要的服務。代碼的削減可以通過設置OS_CFG.H中的#defines OS_???_EN 為0來實現。用戶不需要的服務代碼就不生成。本章的範例就用這種功能,所以每個例子都定義了不同的OS_???_EN。
1.07例1
第一個範例可以在\SOFTWARE\uCOS_II\EX1_x86L目錄下找到,它有13個任務(包括 uC/OS-II 的空閑任務)。uC/OS-II 增加了兩個內部任務:空閑任務和一個計算CPU利用率的任務。例1建立了11個其它任務。TaskStart()任務是在函數main()中建立的;它的功能是建立其它任務並且在屏幕上顯示如下統計信息:
l 每秒鐘任務切換次數;
l CPU利用百分率;
l 寄存器切換次數;
l 目前日期和時間;
l uC/OS-II的版本號;
TaskStart()還檢查是否按下ESC鍵,以決定是否返回到DOS。
其餘10個任務基於相同的代碼——Task();每個任務在屏幕上隨機的位置顯示一個0到9的數字。
1.07.01 main()
例1基本上和最初uC/OS中的第一個例子做一樣的事,但是筆者整理了其中的代碼,並且在屏幕上加了彩色顯示。同時筆者使用原來的數據類型(UBYTE, UWORD等)來說明uC/OS-II向下兼容。
main()程序從清整個屏幕開始,為的是保證屏幕上不留有以前的DOS下的顯示[L1.5(1)]。注意,筆者定義了白色的字元和黑色的背景色。既然要請屏幕,所以可以只定義背景色而不定義前景色,但是這樣在退回DOS之後,用戶就什麼也看不見了。這也是為什麼總要定義一個可見的前景色。
uC/OS-II要用戶在使用任何服務之前先調用OSInit() [L1.5(2)]。它會建立兩個任務:空閑任務和統計任務,前者在沒有其它任務處於就緒態時運行;後者計算CPU的利用率。
程序清單 L 1.5 main(). |
void main (void) |
{ |
PC_DispClrScr(DISP_FGND_WHITE + DISP_BGND_BLACK); (1) |
OSInit(); (2) |
PC_DOSSaveReturn(); (3) |
PC_VectSet(uCOS, OSCtxSw); (4) |
RandomSem = OSSemCreate(1); (5) |
OSTaskCreate(TaskStart, (6) |
(void *)0, |
(void *)&TaskStartStk[TASK_STK_SIZE-1], |
0); |
OSStart(); (7) |
} |
當前DOS環境是通過調用PC_DOSSaveReturn()[L1.5(3)]來保存的。這使得用戶可以返回到沒有運行uC/OS-II以前的DOS環境。跟隨清單L1.6中的程序可以看到PC_DOSSaveReturn()做了很多事情。PC_DOSSaveReturn()首先設置PC_ExitFlag為FALSE[L1.6(1)],說明用戶不是要返回DOS,然後初始化OSTickDOSCtr為1[L1.6(2)],因為這個變數將在OSTickISR()中遞減,而0將使得這個變數在OSTickISR()中減1后變為255。然後,PC_DOSSaveReturn()將DOS 的時鐘節拍處理(tick handler)存入一個自由向量表入口中[L1.6(3)-(4)],以便為uC/OS-II的時鐘節拍處理所調用。接著PC_DOSSaveReturn()調用jmp()[L1.6(5)],它將處理器狀態(即所有寄存器的值)存入被稱為PC_JumpBuf的結構之中。保存處理器的全部寄存器使得程序返回到PC_DOSSaveReturn()並且在調用setjmp()之後立即執行。因為PC_ExitFlag被初始化為FALSE[L1.6(1)]。PC_DOSSaveReturn()跳過if狀態語句 [L1.6(6)–(9)] 回到main()函數。如果用戶想要返回到DOS,可以調用 PC_DOSReturn()(程序清單 L 1.7),它設置PC_ExitFlag為TRUE,並且執行longjmp()語句[L1.7(2)],這時處理器將跳回 PC_DOSSaveReturn()[在調用 setjmp()之後] [L1.6(5)],此時PC_ExitFlag為TRUE,故if語句以後的代碼將得以執行。 PC_DOSSaveReturn()將時鐘節拍改為 18.2Hz[L1.6(6)],恢復PC 時鐘節拍中斷服務[L1.6(7)],清屏幕[L1.6(8)],通過exit(0)返回DOS [L1.6(9)]。
程序清單 L 1.6 保存DOS環境。. |
void PC_DOSSaveReturn (void) |
{ |
PC_ExitFlag = FALSE; (1) |
OSTickDOSCtr = 8; (2) |
PC_TickISR = PC_VectGet(VECT_TICK); (3) |
OS_ENTER_CRITICAL(); |
PC_VectSet(VECT_DOS_CHAIN, PC_TickISR); (4) |
OS_EXIT_CRITICAL(); |
Setjmp(PC_JumpBuf); (5) |
if (PC_ExitFlag == TRUE) { |
OS_ENTER_CRITICAL(); |
PC_SetTickRate(18); (6) |
PC_VectSet(VECT_TICK, PC_TickISR); (7) |
OS_EXIT_CRITICAL(); |
PC_DispClrScr(DISP_FGND_WHITE + DISP_BGND_BLACK); (8) |
exit(0); (9) |
} |
} |
程序清單 L 1.7 設置返回DOS。 |
void PC_DOSReturn (void) |
{ |
PC_ExitFlag = TRUE; (1) |
longjmp(PC_JumpBuf, 1); (2) |
} |
現在回到main()這個函數,在程序清單 L 1.5中,main()調用PC_VectSet()來設置uCOS-II中的 CPU寄存器切換。任務級的CPU寄存器切換由80x86 INT指令來分配向量地址。筆者使用向量0x80(即128),因為它未被DOS和BIOS使用。
這裡用了一個信號量來保護Borland C/C++庫中的產生隨機數的函數[L1.5(5)],之所以使用信號量保護一下,是因為筆者不知道這個函數是否具備可重入性,筆者假設其不具備,初始化將信號量設置為1,意思是在某一時刻只有一個任務可以調用隨機數產生函數。
在開始多任務之前,筆者建立了一個叫做TaskStart()的任務[L1.5(6)],在啟動多任務OSStart()之前用戶至少要先建立一個任務,這一點非常重要[L1.5(7)]。不這樣做用戶的應用程序將會崩潰。實際上,如果用戶要計算CPU的利用率時,也需要先 建立一個任務。uCOS-II的統計任務要求在整個一秒鐘內沒有任何其它任務運行。如果用戶在啟動多任務之前要建立其它任務,必須保證用戶的任務代碼監控全局變數OSStatRdy和延時程序 [即調用 OSTimeDly()]的執行,直到這個變數變成TRUE。這表明uC/OS-II的CPU利用率統計函數已經採集到了數據。
1.07.02 TaskStart()
例1中的主要工作由TaskStart()來完成。TaskStart()函數的示意代碼如程序清單 L 1.8所示。TaskStart()首先在屏幕頂端顯示一個標識,說明這是例1 [L1.8(1)]。然後關中斷,以改變中斷向量,讓其指向uC/OS-II的時鐘節拍處理,而後,改變時鐘節拍率,從DOS的 18.2Hz 變為 200Hz [L1.8(3)]。在處理器改變中斷向量時以及系統沒有完全初始化前,當然不希望有中斷打入!注意main()這個函數(見程序清單 L 1.5)在系統初始化的時候並沒有將中斷向量設置成uC/OS-II的時鐘節拍處理程序,做嵌入式應用時,用戶必須在第一個任務中打開時鐘節拍中斷。
程序清單 L 1.8 建立其它任務的任務。 |
void TaskStart (void *data) |
{ |
Prevent compiler warning by assigning 『data』 to itself; |
Display banner identifying this as EXAMPLE #1; (1) |
OS_ENTER_CRITICAL(); |
PC_VectSet(0x08, OSTickISR); (2) |
PC_SetTickRate(200); (3) |
OS_EXIT_CRITICAL(); |
Initialize the statistic task by calling 『OSStatInit()』; (4) |
Create 10 identical tasks; (5) |
for (;;) { |
Display the number of tasks created; |
Display the % of CPU used; |
Display the number of task switches in 1 second; |
Display uC/OS-II』s version number |
If (key was pressed) { |
if (key pressed was the ESCAPE key) { |
PC_DOSReturn(); |
} |
} |
Delay for 1 Second; |
} |
} |
在建立其他任務之前,必須調用OSStatInit()[L1.8(4)]來確定用戶的PC有多快,如程序清單L1.9所示。在一開始,OSStatInit()就將自身延時了兩個時鐘節拍,這樣它就可以與時鐘節拍中斷同步[L1.9(1)]。因此,OSStatInit()必須在時鐘節拍啟動之後調用;否則,用戶的應用程序就會崩潰。當uC/OS-II調用OSStatInit()時,一個32位的計數器OSIdleCtr被清為0 [L1.9(2)],併產生另一個延時,這個延時使OSStatInit()掛起。此時,uCOS-II沒有別的任務可以執行,它只能執行空閑任務(uC/OS-II的內部任務)。空閑任務是一個無線的循環,它不斷的遞增OSIdleCtr[L1.9(3)]。1秒以後,uCOS-II重新開始OSStatInit(),並且將OSIdleCtr保存在OSIdleMax中[L1.9(4)。所以OSIdleMax是OSIdleCtr所能達到的最大值。而當用戶再增加其他應用代碼時,空閑任務就不會佔用那樣多的CPU時間。OSIdleCtr不可能達到那樣多的記數,(如果擁護程序每秒複位一次OSIdleCtr)CPU利用率的計算由uC/OS-II 中的OSStatTask()函數來完成,這個任務每秒執行一次。而當OSStatRdy置為TRUE[L1.9(5)],表示uC/OS-II將統計CPU的利用率。
程序清單 L 1.9 測試CPU速度。 |
void OSStatInit (void) |
{ |
OSTimeDly(2); (1) |
OS_ENTER_CRITICAL(); |
OSIdleCtr = 0L; (2) |
OS_EXIT_CRITICAL(); |
OSTimeDly(OS_TICKS_PER_SEC); (3) |
OS_ENTER_CRITICAL(); |
OSIdleCtrMax = OSIdleCtr; (4) |
OSStatRdy = TRUE; (5) |
OS_EXIT_CRITICAL(); |
} |
1.07.03 TaskN()
OSStatInit()將返回到TaskStart()。現在,用戶可以建立10個同樣的任務(所有任務共享同一段代碼)。所有任務都由TaskStart()中建立,由於TaskStart()的優先順序為0(最高),新任務建立后不進行任務調度。當所有任務都建立完成後,TaskStart()將進入無限循環之中,在屏幕上顯示統計信息,並檢測是否有ESC鍵按下,如果沒有按鍵輸入,則延時一秒開始下一次循環;如果在這期間用戶按下了ESC鍵,TaskStart()將調用PC_DOSReturn()返回DOS系統。
程序清單L1.10給出了任務的代碼。任務一開始,調用OSSemPend()獲取信號量RandomSem [程序清單L1.10(1)](也就是禁止其他任務運行這段代碼—譯者注),然後調用Borland C/C++的庫函數random()來獲得一個隨機數[程序清單L1.10(2)],此處設random()函數是不可重入的,所以10個任務將輪流獲得信號量,並調用該函數。當計算出x和y坐標后[程序清單L1.10(3)],任務釋放信號量。隨後任務在計算的坐標處顯示其任務號(0-9,任務建立時的標識)[程序清單L1.10(4)]。最後,任務延時一個時鐘節拍[程序清單L1.10(5)],等待進入下一次循環。系統中每個任務每秒執行200次,10個任務每秒鐘將切換2000次。
程序清單 L 1.10 在屏幕上顯示隨機位置顯示數字的任務。 |
void Task (void *data) |
{ |
UBYTE x; |
UBYTE y; |
UBYTE err; |
for (;;) { |
OSSemPend(RandomSem, 0, &err); (1) |
x = random(80); (2) |
y = random(16); |
OSSemPost(RandomSem); (3) |
PC_DispChar(x, y + 5, *(char *)data, DISP_FGND_LIGHT_GRAY); (4) |
OSTimeDly(1); (5) |
} |
} |
1.08 例2
例2使用了帶擴展功能的任務建立函數OSTaskCreateExt()和uCOS-II的堆棧檢查操作(要使用堆棧檢查操作必須用OSTaskCreateExt()建立任務—譯者注)。當用戶不知道應該給任務分配多少堆棧空間時,堆棧檢查功能是很有用的。在這個例子里,先分配足夠的堆棧空間給任務,然後用堆棧檢查操作看看任務到底需要多少堆棧空間。顯然,任務要運行足夠長時間,並要考慮各種情況才能得到正確數據。最後決定的堆棧大小還要考慮系統今後的擴展,一般多分配10%,25%或者更多。如果系統對穩定性要求高,則應該多一倍以上。
uCOS-II的堆棧檢查功能要求任務建立時堆棧清零。OSTaskCreateExt()可以執行此項操作(設置選項OS_TASK_OPT_STK_CHK和OS_TASK_OPT_STK_CLR打開此項操作)。如果任務運行過程中要進行建立、刪除任務的操作,應該設置好上述的選項,確保任務建立后堆棧是清空的。同時要意識到OSTaskCreateExt()進行堆棧清零操作是一項很費時的工作,而且取決於堆棧的大小。執行堆棧檢查操作的時候,uCOS-II從棧底向棧頂搜索非0元素(參看圖F 1.1),同時用一個計數器記錄0元素的個數。
例2的磁碟文件為\SOFTWARE\uCOS-II\EX2_x86L,它包含9個任務。加上uCOS-II本身的兩個任務:空閑任務(idle task)和統計任務。與例1一樣TaskStart()由main()函數建立,其功能是建立其他任務並在屏幕上顯示如下的統計數據:
l 每秒種任務切換的次數;
l CPU利用率的百分比;
l 當前日期和時間;
l uCOS_II的版本號;
圖F 1.1 uC/OS-II stack checking.
1.08.01 main()
例2的main()函數和例1的看起來差不多(參看程序清單L1.11),但是有兩處不同。第一,main()函數調用PC_ElapsedInit()[程序清單L1.11(1)]來初始化定時器記錄OSTaskStkChk()的執行時間。第二,所有的任務都使用OSTaskCreateExt()函數來建立任務[程序清單L1.11(2)](替代老版本的OSTaskCreate()),這使得每一個任務都可進行堆棧檢查。
程序清單 L 1.11 例2中的Main()函數. |
void main (void) |
{ |
PC_DispClrScr(DISP_FGND_WHITE + DISP_BGND_BLACK); |
OSInit(); |
PC_DOSSaveReturn(); |
PC_VectSet(uCOS, OSCtxSw); |
PC_ElapsedInit(); (1) |
OSTaskCreateExt(TaskStart, (2) |
(void *)0, |
&TaskStartStk[TASK_STK_SIZE-1], |
TASK_START_PRIO, |
TASK_START_ID, |
&TaskStartStk[0], |
TASK_STK_SIZE, |
(void *)0, |
OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR); |
OSStart(); |
} |
除了OSTaskCreate()函數的四個參數外,OSTaskCreateExt()還需要五個參數(一共9個):任務的ID,一個指向任務堆棧棧底的指針,堆棧的大小(以堆棧單元為單位,80X86中為字),一個指向用戶定義的TCB擴展數據結構的指針,和一個用於指定對任務操作的變數。該變數的一個選項就是用來設定uCOS-II堆棧檢查是否允許。例2中並沒有用到TCB擴展數據結構指針。
1.08.02TaskStart()
程序清單L1.12列出了TaskStart()的偽碼。前五項操作和例1中相同。TaskStart()建立了兩個郵箱,分別提供給任務4和任務5[程序清單L1.12(1)]。除此之外,還建立了一個專門顯示時間和日期的任務。
程序清單 L 1.12 TaskStart()的偽碼。. |
void TaskStart (void *data) |
{ |
Prevent compiler warning by assigning 『data』 to itself; |
Display a banner and non-changing text; |
Install uC/OS-II』s tick handler; |
Change the tick rate to 200 Hz; |
Initialize the statistics task; |
Create 2 mailboxes which are used by Task #4 and #5; (1) |
Create a task that will display the date and time on the screen; (2) |
Create 5 application tasks; |
for (;;) { |
Display #tasks running; |
Display CPU usage in %; |
Display #context switches per seconds; |
Clear the context switch counter; |
Display uC/OS-II』s version; |
If (Key was pressed) { |
if (Key pressed was the ESCAPE key) { |
Return to DOS; |
} |
} |
Delay for 1 second; |
} |
} |
1.08.03 TaskN()
任務1將檢查其他七個任務堆棧的大小,同時記錄OSTackStkChk()函數的執行時間[程序清單L1.13(1)–(2)],並與堆棧大小一起顯示出來。注意所有堆棧的大小都是以位元組為單位的。任務1每秒執行10次[程序清單L1.13(3)](間隔100ms)。
程序清單 L 1.13 例2, 任務1 |
void Task1 (void *pdata) |
{ |
INT8U err; |
OS_STK_DATA data; |
INT16U time; |
INT8U i; |
char s[80]; |
pdata = pdata; |
for (;;) { |
for (i = 0; i < 7; i++) { |
PC_ElapsedStart(); (1) |
err = OSTaskStkChk(TASK_START_PRIO+i, &data) |
time = PC_ElapsedStop(); (2) |
if (err == OS_NO_ERR) { |
sprintf(s, "%3ld %3ld %3ld %5d", |
data.OSFree + data.OSUsed, |
data.OSFree, |
data.OSUsed, |
time); |
PC_DispStr(19, 12+i, s, DISP_FGND_YELLOW); |
} |
} |
OSTimeDlyHMSM(0, 0, 0, 100); (3) |
} |
} |
程序清單L1.14所示的任務2在屏幕上顯示一個順時針旋轉的指針(用橫線,斜線等字元表示—譯者注),每200ms旋轉一格。
程序清單 L 1.14 任務2 |
void Task2 (void *data) |
{ |
data = data; |
for (;;) { |
PC_DispChar(70, 15, '|', DISP_FGND_WHITE + DISP_BGND_RED); |
OSTimeDly(10); |
PC_DispChar(70, 15, '/', DISP_FGND_WHITE + DISP_BGND_RED); |
OSTimeDly(10); |
PC_DispChar(70, 15, '-', DISP_FGND_WHITE + DISP_BGND_RED); |
OSTimeDly(10); |
PC_DispChar(70, 15, '\\', DISP_FGND_WHITE + DISP_BGND_RED); |
OSTimeDly(10); |
} |
} |
任務3(程序清單 L1.15)也顯示了與任務2相同的一個旋轉指針,但是旋轉的方向不同。任務3在堆棧中分配了一個很大的數組,將堆棧填充掉,使得OSTaskStkChk()只需花費很少的時間來確定堆棧的利用率,尤其是當堆棧已經快滿的時候。
程序清單 L 1.15 任務3 |
void Task3 (void *data) |
{ |
char dummy[500]; |
INT16U i; |
data = data; |
for (I = 0; i < 499; i++) { |
dummy[i] = '?'; |
} |
for (;;) { |
PC_DispChar(70, 16, '|', DISP_FGND_WHITE + DISP_BGND_BLUE); |
OSTimeDly(20); |
PC_DispChar(70, 16, '\\', DISP_FGND_WHITE + DISP_BGND_BLUE); |
OSTimeDly(20); |
PC_DispChar(70, 16, '-', DISP_FGND_WHITE + DISP_BGND_BLUE); |
OSTimeDly(20); |
PC_DispChar(70, 16, '/', DISP_FGND_WHITE + DISP_BGND_BLUE); |
OSTimeDly(20); |
} |
} |
任務4(程序清單L1.16)向任務5發送消息並等待確認[程序清單L1.16(1)]。發送的消息是一個指向字元的指針。每當任務4從任務5收到確認[程序清單L1.16(2)],就將傳遞的ASCII碼加1再發送[程序清單L1.16(3)],結果是不斷的傳送「ABCDEFG....」。
程序清單 L 1.16 任務4 |
void Task4 (void *data) |
{ |
char txmsg; |
INT8U err; |
data = data; |
txmsg = 'A'; |
for (;;) { |
while (txmsg <= 'Z') { |
OSMboxPost(TxMbox, (void *)&txmsg); (1) |
OSMboxPend(AckMbox, 0, &err); (2) |
txmsg++; (3) |
} |
txmsg = 'A'; |
} |
} |
當任務5 [程序清單L1.17]接收消息后[程序清單L1.17(1)](發送的字元),就將消息顯示到屏幕上[程序清單L1.17(2)],然後延時1秒[程序清單L1.17(3)],再向任務4發送確認信息。
程序清單 L 1.17 任務5 |
void Task5 (void *data) |
{ |
char *rxmsg; |
INT8U err; |
data = data; |
for (;;) { |
rxmsg = (char *)OSMboxPend(TxMbox, 0, &err); (1) |
PC_DispChar(70, 18, *rxmsg, DISP_FGND_YELLOW+DISP_BGND_RED); (2) |
OSTimeDlyHMSM(0, 0, 1, 0); (3) |
OSMboxPost(AckMbox, (void *)1); (4) |
} |
} |
TaskClk()函數[程序清單L1.18]顯示當前日期和時間,每秒更新一次。
程序清單 L 1.18 時鐘顯示任務 |
void TaskClk (void *data) |
{ |
Struct time now; |
Struct date today; |
char s[40]; |
data = data; |
for (;;) { |
PC_GetDateTime(s); |
PC_DispStr(0, 24, s, DISP_FGND_BLUE + DISP_BGND_CYAN); |
OSTimeDly(OS_TICKS_PER_SEC); |
} |
} |
1.09例3
例3中使用了許多uCOS-II提供的附加功能。任務3使用了OSTaskCreateExt()中TCB的擴展數據結構,用戶定義的任務切換對外介面函數(OSTaskSwHook()),用戶定義的統計任務(statistic task )的對外介面函數(OSTaskStatHook())以及消息隊列。例3的磁碟文件是\SOFTWARE\uCOS-II\EX3_x86L,它包括9個任務。除了空閑任務(idle task)和統計任務(statistic task ),還有7個任務。與例1,例2一樣,TaskStart()由main()函數建立,其功能是建立其他任務,並顯示統計信息。
1.09.01 main()
main()函數[程序清單L1.19]和例2中的相不多,不同的是在用戶定義的TCB擴展數據結構中可以保存每個任務的名稱[程序清單L1.19(1)](擴展結構的聲明在INCLUDES.H中定義,也可參看程序清單L1.20)。筆者定義了30個位元組來存放任務名(包括空格)[程序清單L1.20(1)]。本例中沒有用到堆棧檢查操作,TaskStart()中禁止該操作[程序清單L1.19(2)]。
程序清單 L 1.19 例3的main()函數 |
void main (void) |
{ |
PC_DispClrScr(DISP_FGND_WHITE + DISP_BGND_BLACK); |
OSInit(); |
PC_DOSSaveReturn(); |
PC_VectSet(uCOS, OSCtxSw); |
PC_ElapsedInit(); |
Strcpy(TaskUserData[TASK_START_ID].TaskName, "StartTask"); (1) |
OSTaskCreateExt(TaskStart, |
(void *)0, |
&TaskStartStk[TASK_STK_SIZE-1], |
TASK_START_PRIO, |
TASK_START_ID, |
&TaskStartStk[0], |
TASK_STK_SIZE, |
&TaskUserData[TASK_START_ID], |
0); (2) |
OSStart(); |
} |
程序清單 L 1.20 TCB擴展數據結構。 |
typedef struct { |
char TaskName[30]; (1) |
INT16U TaskCtr; |
INT16U TaskExecTime; |
INT32U TaskTotExecTime; |
} TASK_USER_DATA; |
1.09.02任務
TaskStart()的偽碼如程序清單L1.21所示,與例2有3處不同:
l 為任務1,2,3建立了一個消息隊列[程序清單L1.21(1)];
l 每個任務都有一個名字,保存在任務的TCB擴展數據結構中[程序清單L1.21(2)];
l 禁止堆棧檢查。
程序清單 L 1.21 TaskStart()的偽碼。 |
void TaskStart (void *data) |
{ |
Prevent compiler warning by assigning 『data』 to itself; |
Display a banner and non-changing text; |
Install uC/OS-II』s tick handler; |
Change the tick rate to 200 Hz; |
Initialize the statistics task; |
Create a message queue; (1) |
Create a task that will display the date and time on the screen; |
Create 5 application tasks with a name stored in the TCB ext.; (2) |
for (;;) { |
Display #tasks running; |
Display CPU usage in %; |
Display #context switches per seconds; |
Clear the context switch counter; |
Display uC/OS-II』s version; |
If (Key was pressed) { |
if (Key pressed was the ESCAPE key) { |
Return to DOS; |
} |
} |
Delay for 1 second; |
} |
} |
任務1向消息隊列發送一個消息[程序清單L1.22(1)],然後延時等待消息發送完成[程序清單L1.22(2)]。這段時間可以讓接收消息的任務顯示收到的消息。發送的消息有三種。
程序清單 L 1.22 任務1。 |
void Task1 (void *data) |
{ |
char one = '1'; |
char two = '2'; |
char three = '3'; |
data = data; |
for (;;) { |
OSQPost(MsgQueue, (void *)&one); (1) |
OSTimeDlyHMSM(0, 0, 1, 0); (2) |
OSQPost(MsgQueue, (void *)&two); |
OSTimeDlyHMSM(0, 0, 0, 500); |
OSQPost(MsgQueue, (void *)&three); |
OSTimeDlyHMSM(0, 0, 1, 0); |
} |
} |
任務2處於等待消息的掛起狀態,且不設定最大等待時間[程序清單L1.23(1)]。所以任務2將一直等待直到收到消息。當收到消息后,任務2顯示消息並且延時500mS[程序清單L1.23(2)],延時的時間可以使任務3檢查消息隊列。
程序清單 L 1.23 任務2。 |
void Task2 (void *data) |
{ |
INT8U *msg; |
INT8U err; |
data = data; |
for (;;) { |
msg = (INT8U *)OSQPend(MsgQueue, 0, &err); (1) |
PC_DispChar(70, 14, *msg, DISP_FGND_YELLOW+DISP_BGND_BLUE); (2) |
OSTimeDlyHMSM(0, 0, 0, 500); (3) |
} |
} |
任務3同樣處於等待消息的掛起狀態,但是它設定了等待結束時間250mS[程序清單L1.24(1)]。如果有消息來到,任務3將顯示消息號[程序清單L1.24(3)],如果超過了等待時間,任務3就顯示「T」(意為timeout)[程序清單L1.24(2)]。
程序清單 L 1.24任務3 |
void Task3 (void *data) |
{ |
INT8U *msg; |
INT8U err; |
data = data; |
for (;;) { |
msg = (INT8U *)OSQPend(MsgQueue, OS_TICKS_PER_SEC/4, &err); (1) |
If (err == OS_TIMEOUT) { |
PC_DispChar(70,15,'T',DISP_FGND_YELLOW+DISP_BGND_RED); (2) |
} else { |
PC_DispChar(70,15,*msg,DISP_FGND_YELLOW+DISP_BGND_BLUE); (3) |
} |
} |
} |
任務4的操作只是從郵箱發送[程序清單L1.25(1)]和接收[程序清單L1.25(2)],這使得用戶可以測量任務在自己PC上執行的時間。任務4每10mS執行一次[程序清單L1.25(3)]。
程序清單 L 1.25 任務4。 |
void Task4 (void *data) |
{ |
OS_EVENT *mbox; |
INT8U err; |
data = data; |
mbox = OSMboxCreate((void *)0); |
for (;;) { |
OSMboxPost(mbox, (void *)1); (1) |
OSMboxPend(mbox, 0, &err); (2) |
OSTimeDlyHMSM(0, 0, 0, 10); (3) |
} |
} |
任務5除了延時一個時鐘節拍以外什麼也不做[程序清單L1.26(1)]。注意所有的任務都應該調用uCOS-II的函數,等待延時結束或者事件的發生而讓出CPU。如果始終佔用CPU,這將使低優先順序的任務無法得到CPU。
程序清單 L 1.26 任務5。 |
void Task5 (void *data) |
{ |
data = data; |
for (;;) { |
OSTimeDly(1); (1) |
} |
} |
同樣, TaskClk()函數[程序清單L1.18]顯示當前日期和時間。
1.09.03注意
有些程序的細節只有請您仔細讀一讀EX3L.C才能理解。EX3L.C中有OSTaskSwHook()函數的代碼,該函數用來測量每個任務的執行時間,可以用來統計每一個任務的調度頻率,也可以統計每個任務運行時間的總和。這些信息將存儲在每個任務的TCB擴展數據結構中。每次任務切換的時候OSTaskSwHook()都將被調用。
每次任務切換髮生的時候,OSTaskSwHook()先調用PC_ElapsedStop()函數[程序清單L1.27(1)] 來獲取任務的運行時間[程序清單L1.27(1)],PC_ElapsedStop()要和PC_ElapsedStart()一起使用,上述兩個函數用到了PC的定時器2(timer 2)。其中PC_ElapsedStart()功能為啟動定時器開始記數;而PC_ElapsedStop()功能為獲取定時器的值,然後清零,為下一次計數做準備。從定時器取得的計數將拷貝到time變數[程序清單L1.27(1)]。然後OSTaskSwHook()調用PC_ElapsedStart()重新啟動定時器做下一次計數[程序清單L1.27(2)]。需要注意的是,系統啟動后,第一次調用PC_ElapsedStart()是在初始化代碼中,所以第一次任務切換調用PC_ElapsedStop()所得到的計數值沒有實際意義,但這沒有什麼影響。如果任務分配了TCB擴展數據結構[程序清單L1.27(4)],其中的計數器TaskCtr進行累加[程序清單L1.27(5)]。TaskCtr可以統計任務被切換的頻繁程度,也可以檢查某個任務是否在運行。TaskExecTime [程序清單L1.27(6)]用來記錄函數從切入到切出的運行時間,TaskTotExecTime[程序清單L1.27(7)]記錄任務總的運行時間。統計每個任務的上述兩個變數,可以計算出一段時間內各個任務佔用CPU的百分比。OSTaskStatHook()函數會顯示這些統計信息。
程序清單 L 1.27 用戶定義的OSTaskSwHook() |
void OSTaskSwHook (void) |
{ |
INT16U time; |
TASK_USER_DATA *puser; |
time = PC_ElapsedStop(); (1) |
PC_ElapsedStart(); (2) |
puser = OSTCBCur->OSTCBExtPtr; (3) |
if (puser != (void *)0) { (4) |
puser->TaskCtr++; (5) |
puser->TaskExecTime = time; (6) |
puser->TaskTotExecTime += time; (7) |
} |
} |
本例中的統計任務(statistic task)將調用對外介面函數OSTaskStatHook()(設置OS_CFG.H文件中的OS_TASK_STAT_EN為1允許對外介面函數)。統計任務每秒運行一次,本例中OSTaskStatHook()用來計算並顯示各任務佔用CPU的情況。
OSTaskStatHook()函數中首先計算所有任務的運行時間[程序清單L1.28(1)],DispTaskStat()用來將數字顯示為ASCII字元[程序清單L1.28(2)]。然後是計算每個任務運行時間的百分比[程序清單L1.28(3)],顯示在合適的位置上 [程序清單L1.28(4)]。
程序清單 L 1.28 用戶定義的OSTaskStatHook(). |
void OSTaskStatHook (void) |
{ |
char s[80]; |
INT8U i; |
INT32U total; |
INT8U pct; |
total = 0L; |
for (I = 0; i < 7; i++) { |
total += TaskUserData[i].TaskTotExecTime; (1) |
DispTaskStat(i); (2) |
} |
if (total > 0) { |
for (i = 0; i < 7; i++) { |
pct = 100 * TaskUserData[i].TaskTotExecTime / total; (3) |
sprintf(s, "%3d %%", pct); |
PC_DispStr(62, i + 11, s, DISP_FGND_YELLOW); (4) |
} |
} |
if (total > 1000000000L) { |
for (i = 0; i < 7; i++) { |
TaskUserData[i].TaskTotExecTime = 0L; |
} |
} |
} |