天天看點

uCOS-II學習筆記(一)

                                                              第一章:範例

  在這一章裡将提供三個範例來說明如何使用 µC/OS-II。這一章是為了讓讀者盡快開始使用 µC/OS-II。

1.00 安裝µC/OS-II

1.01 INCLUDES.H

#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不依賴于編譯的資料類型

因為不同的微處理器有不同的字長,µC/OS-II的移植檔案包括很多類型定義以確定可移植性(參見\SOFTWARE\uCOS-II\Ix86L\OS_CPU.H,它是針對80x86的實模式,在大模式下編譯)。µCOS-II不使用C語言中的short,int,long等資料類型的定義,因為它們與處理器類型有關,隐含着不可移植性。筆者代之以移植性強的整數資料類型,這樣,既直覺又可移植,如表L1.1所示。為了友善起見,還定義了浮點數資料類型,雖然µC/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位無符号整數資料類型。µC/OS-II和使用者的應用代碼可以定義這種類型的資料,範圍從0到65,535。如果将µCO/S-II移植到32位處理器中,那就意味着INT16U不再不是一個無符号整型資料,而是一個無符号短整型資料。然而将無論µC/OS-II用到哪裡,都會當作INT16U處理。 表1.1是以Borland C/C++編譯器為例,為80x86提供的定義語句。為了和µC/OS相容,還定義了BYTE,WORD,LONG以及相應的無符号變量。這使得使用者可以不作任何修改就能将µC/OS的代碼移植到µC/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.04OS_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的服務程式。與µC/OS-II 以前的版本(即µC/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其他函數

µC/OS-II的應用程式和其他DOS應用程式是一樣的,換句話說,使用者可以像在DOS下編譯其他單線程的程式一樣編譯和連結使用者程式。所生成的.EXE程式可以在DOS下裝載和運作,當然應用程式應該從main()函數開始。因為µC/OS-II 是多任務,而且為每個任務開辟一個堆棧,是以單線程的DOS環境應該儲存,在退出µC/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()允許使用者為µC /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 應用µC/OS-II 的範例

本章中的例子都用Borland C/C++編譯器編譯通過,是在Windows95 的DOS視窗下編譯的。可執行代碼可以在每個範例的OBJ子目錄下找到。實際上這些代碼是在Borland IDE (Integrated Development Environment)下編譯的,編譯時的選項如表1.1所示:

表T1.1 IDE中編譯選項。
Code generation
Model : Large
Options : Treatenums asints
AssumeSSEqualsDS : 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的路徑。

µC/OS-II是一個可裁剪的作業系統,這意味着使用者可以去掉不需要的服務。代碼的削減可以通過設定OS_CFG.H中的#defines OS_???_EN 為0來實作。使用者不需要的服務代碼就不生成。本章的範例就用這種功能,是以每個例子都定義了不同的OS_???_EN。

1.07例1

第一個範例可以在\SOFTWARE\uCOS_II\EX1_x86L目錄下找到,它有13個任務(包括µC/OS-II 的空閑任務)。µC/OS-II 增加了兩個内部任務:空閑任務和一個計算CPU使用率的任務。例1建立了11個其它任務。TaskStart()任務是在函數main()中建立的;它的功能是建立其它任務并且在螢幕上顯示如下統計資訊:

l每秒鐘任務切換次數;

lCPU利用百分率;

l寄存器切換次數;

l目前日期和時間;

lµC/OS-II的版本号;

TaskStart()還檢查是否按下ESC鍵,以決定是否傳回到DOS。

其餘10個任務基于相同的代碼——Task();每個任務在螢幕上随機的位置顯示一個0到9的數字。

1.07.01 main()

例1基本上和最初µC/OS中的第一個例子做一樣的事,但是筆者整理了其中的代碼,并且在螢幕上加了彩色顯示。同時筆者使用原來的資料類型(UBYTE,UWORD等)來說明µC/OS-II向下相容。

main()程式從清整個螢幕開始,為的是保證螢幕上不留有以前的DOS下的顯示[L1.5(1)]。注意,筆者定義了白色的字元和黑色的背景色。既然要請螢幕,是以可以隻定義背景色而不定義前景色,但是這樣在退回DOS之後,使用者就什麼也看不見了。這也是為什麼總要定義一個可見的前景色。

µC/OS-II要使用者在使用任何服務之前先調用OSInit() [L1.5(2)]。它會建立兩個任務:空閑任務和統計任務,前者在沒有其它任務處于就緒态時運作;後者計算CPU的使用率。

程式清單 L 1.5main().
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)]來儲存的。這使得使用者可以傳回到沒有運作µC/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)],以便為µC/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()來設定µCOS-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的使用率時,也需要先建立一個任務。µCOS-II的統計任務要求在整個一秒鐘内沒有任何其它任務運作。如果使用者在啟動多任務之前要建立其它任務,必須保證使用者的任務代碼監控全局變量OSStatRdy和延時程式 [即調用 OSTimeDly()]的執行,直到這個變量變成TRUE。這表明µC/OS-II的CPU使用率統計函數已經采集到了資料。

1.07.02 TaskStart()

例1中的主要工作由TaskStart()來完成。TaskStart()函數的示意代碼如程式清單 L 1.8所示。TaskStart()首先在螢幕頂端顯示一個辨別,說明這是例1 [L1.8(1)]。然後關中斷,以改變中斷向量,讓其指向µC/OS-II的時鐘節拍處理,而後,改變時鐘節拍率,從DOS的 18.2Hz 變為 200Hz [L1.8(3)]。在處理器改變中斷向量時以及系統沒有完全初始化前,當然不希望有中斷打入!注意main()這個函數(見程式清單 L 1.5)在系統初始化的時候并沒有将中斷向量設定成µC/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()必須在時鐘節拍啟動之後調用;否則,使用者的應用程式就會崩潰。當µC/OS-II調用OSStatInit()時,一個32位的計數器OSIdleCtr被清為0 [L1.9(2)],并産生另一個延時,這個延時使OSStatInit()挂起。此時,uCOS-II沒有别的任務可以執行,它隻能執行空閑任務(µC/OS-II的内部任務)。空閑任務是一個無線的循環,它不斷的遞增OSIdleCtr[L1.9(3)]。1秒以後,uCOS-II重新開始OSStatInit(),并且将OSIdleCtr儲存在OSIdleMax中[L1.9(4)。是以OSIdleMax是OSIdleCtr所能達到的最大值。而當使用者再增加其他應用代碼時,空閑任務就不會占用那樣多的CPU時間。OSIdleCtr不可能達到那樣多的記數,(如果擁護程式每秒複位一次OSIdleCtr)CPU使用率的計算由µC/OS-II 中的OSStatTask()函數來完成,這個任務每秒執行一次。而當OSStatRdy置為TRUE[L1.9(5)],表示µC/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每秒種任務切換的次數;

lCPU使用率的百分比;

l目前日期和時間;

luCOS_II的版本号;

圖F 1.1 µC/OS-II stack checking.

uCOS-II學習筆記(一)

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.12TaskStart()的僞碼。.
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.20TCB擴充資料結構。
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.21TaskStart()的僞碼。
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;
}
}
}

繼續閱讀