Understanding uC/OS-II (7) 強大版



    -- Porting uC/OS-II



        






這一篇要介紹如何將 uC/OS-II 移植到不同的處理器上。所謂移植,就是使一個即時 kernel 能在某個微處理器或微控制器上執行。為了方便移植,大部分的 uC/OS-II 程式碼是用 C 語言寫的;但仍需要組合語言寫一些與處理器相關的程式碼,這是因為 uC/OS-II 在讀寫處理器暫存器時只能透過組合語言來實現。由於 uC/OS-II 在設計時就已經充分考慮了可攜性,所以 uC/OS-II 的移植相對來說是比較容易的。如果已經有人在您使用的處理器上成功地移植了 uC/OS-II,您也得到了相關程式碼,就不必看本章了。當然,本章介紹的內容將有助於使用者瞭解 uC/OS-II 中與處理器相關的程式碼。


要使 uC/OS-II 正常執行,處理器必須滿足以下要求:




  1. 處理器的 C 編譯器能產生可重入程式碼。



  2. 用 C 語言就可以打開和關閉中斷。



  3. 處理器支援中斷,並且能產生定時中斷 (通常在 10 至 100Hz 之間)。



  4. 處理器支援能夠容納一定數量的資料的硬體堆疊 (可能是幾千位元組)。



  5. 處理器有將堆疊指標和其他 CPU 暫存器讀出和存儲到堆疊或記憶體中的指令。



像 Motorola 6805 系列的處理器不能滿足上面的第 4 條和第 5 條要求,所以 uC/OS-II 不能在這類處理器上執行。



Figure 8.1 uC/OS-II Hardware/Software Architecture


如果使用者能理解處理器和 C 編譯器的技術細節,移植 uC/OS-II 的工作實際上是非常簡單的。前提是您的處理器和編譯器滿足了 uC/OS-II 的要求,並且已經有了必要工具。移植工作包括以下幾個內容:




  • 將用一些 #define 的常數定義設為 1 (OS_CPU.H)



  • 宣告 10 個資料型別 (OS_CPU.H)



  • 用 #define 宣告三個巨集 (OS_CPU.H)



  • 用 C 語言編寫六個簡單的函式 (OS_CPU_C.C)



  • 編寫四個組合語言函式 (OS_CPU_A.ASM)



根據處理器的不同,一個移植實例可能需要編寫或改寫 50 至 300 行的程式碼,需要的時間從幾個小時到一星期不等。


一旦程式碼移植結束,下一步工作就是測試。測試一個像 uC/OS-II 一樣的 multitasking read-time kernel 並不複雜。甚至可以在沒有應用程式的情況下測試。換句話說,就是讓 kernel 自己測試自己。這樣做有兩個好處:第一,避免使本來就複雜的事情更加複雜;第二,如果出現問題,可以知道問題出在 kernel 程式碼上而不是應用程式。剛開始的時候可以執行一些簡單的 task 和 tick ISR。一旦多工排程成功地執行了,再添加應用程式的 task 就是非常簡單的工作了。




Development Tools


如前所述,移植 uC/OS-II 需要一個 C 編譯器,並且是針對使用者用的 CPU。因為 uC/OS-II 是一個 preemptive kernel,使用者只有透過 C 編譯器來產生可重入程式碼; C 編譯器還要支援組合語言程式。絕大部分的 C 編譯器都是為嵌入式系統設計的,它包括 assembler,linker,locator。linker 用來將不同的模組 (編譯過和組譯過的檔案) 連接成目標檔案。locator 則允許使用者將程式碼和資料放設在目標處理器的指定記憶體映射空間中。所用的 C 編譯器還必須提供一個機制來從 C 中打開和關閉中斷。一些編譯器允許使用者在 C 的程式碼中插入組合語言。這就使得插入合適的處理器指令來允許和禁止中斷變得非常容易了。還有一些編譯器實際上包括了語言擴展功能,可以直接從 C 中允許和禁止中斷。




Directories and Files


本書所付的磁片中提供了uC/OS-II的安裝程式,可在硬碟上安裝uC/OS-II和移植實例程式碼 (Intel 80x86實模式,大模式編譯)。我設計了一個連續的目錄結構,使得使用者更容易找到目標處理器的檔案。如果想增加一個其他處理器的移植實例,您可以考慮採取同樣的方法 (包括目錄的建立和檔案的命名等等) 。


各個微處理器或微控制器的移植程式碼會在以下兩個或三個檔案中找到:OS_CPU.H,OS_CPU_C.C,OS_CPU_A.ASM。組合語言檔案 OS_CPU_A.ASM 是可要可不要的,因為某些 C 編譯器允許使用者在 C 語言中插入組合語言,所以使用者可以將所需的組合語言程式碼直接放到 OS_CPU_C.C 中。




INCLUDES.H


INCLUDES.H 是一個表頭檔,它在所有 .C 檔案的第一行被 include 進來。





#include    "includes.h"

INCLUDES.H 使得使用者專案中的每個 .C 檔案不用分別去考慮它實際上需要哪些表頭檔。使用 INCLUDES.H 的唯一缺點是它可能會包含一些實際不相關的表頭檔。這意味著每個檔案的編譯時間可能會增加。但由於它增強了程式碼的可攜性,所以 uC/OS-II 還是決定使用這一方法。使用者可以透過編輯 INCLUDES.H 來增加自己的表頭檔,但是使用者的表頭檔必須添加在表頭檔列表的最後。




OS_CPU.H


OS_CPU.H 包括了用 #define 定義了與處理器相關的常數定義,巨集和型別定義。OS_CPU.H 的大體結構如程式清單 L8.1 所示。








程式清單 L8.1 Contents of OS_CPU.H



#ifdef OS_CPU_GLOBALS
#define OS_CPU_EXT
#else
#define OS_CPU_EXT extern
#endif

/*
************************************************************************
* 資料型別
* (與編譯器相關)
************************************************************************
*/
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U; /* 無號數 8 位元整數 */ (1)
typedef signed char INT8S; /* 有號數 8 位元整數 */
typedef unsigned int INT16U; /* 無號數 16 位元整數 */
typedef signed int INT16S; /* 有號數 16 位元整數 */
typedef unsigned long INT32U; /* 無號數 32 位元整數 */
typedef signed long INT32S; /* 有號數 32 位元整數 */
typedef float FP32; /* 單精確度浮點數 */ (2)
typedef double FP64; /* 雙精度浮點數 */

typedef unsigned int OS_STK; /* 堆疊入口寬度為 16 位元 */

/*
*************************************************************************
* 與處理器相關的程式碼
*************************************************************************
*/
#define OS_ENTER_CRITICAL() ??? /* 禁止中斷 */ (3)
#define OS_EXIT_CRITICAL() ??? /* 允許中斷 */

#define OS_STK_GROWTH 1 /* 定義堆疊的增長方向:1=向下, 0=向上 */ (4)

#define OS_TASK_SW() ??? (5)



OS_CPU.H, Compiler Specific Data Types


因為不同的微處理器有不同的 word 長度,所以 uC/OS-II 的移植包括了一系列的型別定義以確保其可攜性。尤其是,uC/OS-II 程式碼從不使用 C 的 short,int 和 long 等資料型別,因為它們是與編譯器相關的,不可移植。相反的,uC/OS-II 所定義的整型資料結構既是可移植的又是直觀的 [L8.1(2)]。為了方便,雖然 uC/OS-II 不使用浮點數據,但還是定義了浮點資料型別 [L8.1(2)]。


例如,INT16U 資料型別總是代表 16 位元的無號數整數。現在,uC/OS-II 和使用者的應用程式就可以估計出宣告為該資料型別的變數的數值範圍是 0 ~ 65535。若將 uC/OS-II 移植到 32 位元的處理器上也就意味著 INT16U 實際被宣告為 unsigned short 而不是 unsigned int。但是,uC/OS-II 所處理的仍然是 INT16U。


使用者必須將 task 堆疊的資料型別告訴給 uC/OS-II。這個過程是透過為 OS_STK 宣告正確的 C 資料型別來完成的。如果使用者的處理器上的堆疊成員是 32 位元的,並且使用者的編譯檔案指定 int 為 32 位元數,那麼就應該將 OS_STK 宣告為 unsigned int。所有的 task 堆疊都必須用 OS_STK 來宣告資料型別。


使用者所必須要做的就是查看編譯器手冊,並找到對應於 uC/OS-II 的標準 C 資料型別。


OS_CPU.H, OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL()


與所有的 real-time kernel 一樣,uC/OS-II 需要先禁止中斷再存取程式碼的 critical section,並且在存取完畢後重新允許中斷。這就使得 uC/OS-II 能夠保護 critical section 程式碼免受多工或 ISR 的破壞。中斷禁止時間是商業即時 kernel 公司提供的重要指標之一,因為它將影響到使用者的系統對即時事件的回應能力。雖然 uC/OS-II 儘量使中斷禁止時間達到最短,但是 uC/OS-II 的中斷禁止時間還主要依賴於處理器結構和編譯器產生的程式碼的品質。通常每個處理器都會提供一定的指令來禁止/允許中斷,因此使用者的 C 編譯器必須要有一定的機制來直接從 C 中執行這些操作。有些編譯器能夠允許使用者在 C 程式碼中插入組合語言宣告。這樣就使得插入處理器指令來允許和禁止中斷變得很容易了。其他一些編譯器實際上包括了語言擴展功能,可以直接從 C 中允許和禁止中斷。為了隱藏編譯器廠商提供的具體實現方法, uC/OS-II 定義了兩個巨集來禁止和允許中斷:OS_ENTER_CRITICAL()OS_EXIT_CRITICAL() [L8.1(3)]。





{
OS_ENTER_CRITICAL();

/* uC/OS-II critical section 程式碼 */

OS_EXIT_CRITICAL();
}

Method 1


執行這兩個巨集的第一個也是最簡單的方法是在 OS_ENTER_CRITICAL() 中呼叫處理器指令來禁止中斷,以及在 OS_EXIT_CRITICAL() 中呼叫允許中斷指令。但是,在這個過程中還存在著小小的問題。如果使用者在禁止中斷的情況下呼叫 uC/OS-II 函式,在從 uC/OS-II 返回的時候,中斷可能會變成是允許的了!如果使用者禁止中斷就表明使用者想在從 uC/OS-II 函式返回的時候中斷還是禁止的。在這種情況下,光靠這種執行方法可能是不夠的。


Method 2


執行 OS_ENTER_CRITICAL() 的第二個方法是先將中斷禁止狀態保存到堆疊中,然後禁止中斷。而執行 OS_EXIT_CRITICAL() 的時候只是從堆疊中恢復中斷狀態。如果用這個方法的話,不管使用者是在中斷禁止還是允許的情況下呼叫 uC/OS-II 服務,在整個呼叫過程中都不會改變中斷狀態。如果使用者在中斷禁止的時候呼叫 uC/OS-II 服務,其實使用者是在延長應用程式的中斷回應時間。使用者的應用程式還可以用 OS_ENTER_CRITICAL() 和 OS_EXIT_CRITICAL() 來保護程式碼的 critical section 。但是,使用者在使用這種方法的時候還得十分小心,因為如果使用者在呼叫像 OSTimeDly() 之類的服務之前就禁止中斷,很有可能使用者的應用程式會崩潰。發生這種情況的原因是 task 被休眠直到時間期滿,而中斷是禁止的,因而使用者不可能獲得 tick interrupt!很明顯,所有的 PEND 呼叫都會涉及到這個問題,使用者得十分小心。一個通用的辦法是使用者應該在中斷允許的情況下呼叫 uC/OS-II 的系統服務!


問題是:哪種方法更好一點?這就得看使用者想犧牲些什麼。如果使用者並不關心在呼叫 uC/OS-II 服務後使用者的應用程式中中斷是否是允許的,那麼使用者應該選擇第一種方法執行。如果使用者想在呼叫 uC/OS-II 服務過程中保持中斷禁止狀態,那麼很明顯使用者應該選擇第二種方法。


給使用者舉個例子吧,透過執行 STI 命令在 Intel 80186上禁止中斷,並用 CLI 命令來允許中斷。使用者可以用下面的方法來執行這兩個巨集:








Method 1



#define OS_ENTER_CRITICAL()     asm CLI

#define OS_EXIT_CRITICAL() asm STI

CLI 和 STI 指令都會在兩個 clock cycle 期內被馬上執行 (總共為四個 cycle)。為了保持中斷狀態,使用者需要用下面的方法來執行巨集:








Method 2



#define OS_ENTER_CRITICAL()     asm PUSHF; CLI

#define OS_EXIT_CRITICAL() asm POPF

在這種情況下,OS_ENTER_CRITICAL() 需要 12 個 clock cycle,而 OS_EXIT_CRITICAL() 需要另外的 8 個 clock cycle (總共有20個 cycle)。這樣,保持中斷禁止狀態要比簡單的禁止/允許中斷多花 16 個 clock cycle 的時間 (至少在 80186 上是這樣的)。當然,如果使用者有一個速度比較快的處理器 (如 Intel Pentium II),那麼這兩種方法的時間差別會很小。


OS_CPU.H, OS_STK_GROWTH


絕大多數的微處理器和微控制器的堆疊是從上往下長的。但是某些處理器是用另外一種方式工作的。uC/OS-II 被設計成兩種情況都可以處理,只要在結構常數定義 OS_STK_GROWTH [L8.1(4)] 中指定堆疊的生長方式 (如下所示) 就可以了。


OS_STK_GROWTH 為 0 表示堆疊從下往上長。


OS_STK_GROWTH 為 1 表示堆疊從上往下長。


OS_CPU.H, OS_TASK_SW()


OS_TASK_SW() [L8.1(5)] 是一個巨集,它是在 uC/OS-II 從低優先順序 task 切換到最高優先順序 task 時被呼叫的。OS_TASK_SW() 總是在 task 級程式碼中被呼叫的。另一個函式 OSIntExit() 被用來在 ISR 使得更高優先順序 task 處於 READY 狀態時,執行 task switch 功能。 task switch 只是簡單的將處理器暫存器保存到將被休眠的 task 的堆疊中,並且將更高優先順序的 task 從堆疊中恢復出來。


在 uC/OS-II 中,處於 READY 狀態的 task 的堆疊結構看起來就像剛發生過中斷並將所有的暫存器保存到堆疊中的情形一樣。換句話說,uC/OS-II 要執行處於 READY 狀態的 task 必須要做的事就是將所有處理器暫存器從 task 堆疊中恢復出來,並且執行中斷的返回。為了切換 task 可以透過執行 OS_TASK_SW() 來產生中斷。大部分的處理器會提供軟體中斷或是陷阱指令來完成這個功能。ISR 或是陷阱處理函式 (也叫做異常處理函式) 的向量位址必須指向組合語言函式 OSCtxSw()。


例如,在 Intel 或者 AMD 80x86 處理器上可以使用 INT 指令。但是中斷處理向量需要指向 OSCtxSw()。Motorola 68HC11處理器使用的是 SWI 指令,同樣,SWI 的向量位址仍是 OSCtxSw()。還有,Motorola 680x0/CPU32 可能會使用 16 個陷阱指令中的一個。當然,選中的陷阱向量地址還是 OSCtxSw()。


一些處理器如 Zilog Z80 並不提供軟中斷機制。在這種情況下,使用者需要盡自己的所能將堆疊結構設設成與中斷堆疊結構一樣。OS_TASK_SW() 只會簡單的呼叫 OSCtxSw() 而不是將某個向量指向 OSCtxSw()。uC/OS 已經被移植到了 Z80 處理器上,uC/OS-II 也同樣可以。




OS_CPU_A.ASM


uC/OS-II 的移植要求使用者編寫四個簡單的組合語言函式:




  • OSStartHighRdy()



  • OSCtxSw()



  • OSIntCtxSw()



  • OSTickISR()



如果使用者的編譯器支援插入組合語言程式碼的話,使用者就可以將所有與處理器相關的程式碼放到 OS_CPU_C.C 檔案中,而不必再擁有一些分散的組合語言檔案。


OS_CPU_A.ASM, OSStartHighRdy()


使 READY 狀態的 task 開始執行的函式叫做 OSStart(),如下所示。在使用者呼叫 OSStart() 之前,使用者必須至少已經建立了自己的一個 task (參看 OSTaskCreate() 和 OSTaskCteateExt())。OSStartHighRdy() 假設 OSTCBHighRdy 指向的是優先順序最高的 task 的 TCB。前面曾提到過,在 uC/OS-II 中處於 READY 狀態的 task 的堆疊結構看起來就像剛發生過中斷並將所有的暫存器保存到堆疊中的情形一樣。要想執行最高優先順序 task,使用者所要做的是將所有處理器暫存器按順序從 task 堆疊中恢復出來,並且執行中斷的返回。為了簡單一點,堆疊指標總是儲存在 TCB (即它的 OS_TCB) 的開頭。換句話說,也就是要想恢復的 task 堆疊指標總是儲存在 OS_TCB 的 0 偏移位址記憶體單元中。





void OSStartHighRdy (void)
{
Call user definable OSTaskSwHook();

Get the stack pointer of the task to resume:

Stack pointer = OSTCBHighRdy->OSTCBStkPtr;

OSRunning = TRUE;

Restore all processor registers from the new task's stack;

Execute a return from interrupt instruction;
}

注意,OSStartHighRdy() 必須呼叫 OSTaskSwHook(),因為使用者正在進行 task switch 的部分工作 -- 使用者在恢復最高優先順序 task 的暫存器。而 OSTaskSwHook() 可以透過檢查 OSRunning 來知道是 OSStartHighRdy() 在呼叫它 (OSRunning 為 FALSE) 還是正常的 task switch 在呼叫它 (OSRunning 為 TRUE).


OSStartHighRdy() 還必須在最高優先順序 task 恢復之前和呼叫 OSTaskSwHook() 之後設 OSRunning 為 TRUE。


OS_CPU_A.ASM, OSCtxSw()


如前面所述, task 級的切換問題是透過發軟中斷命令或依靠處理器執行陷阱指令來完成的。中斷服務常式,陷阱或異常處理常式的向量位址必須指向 OSCtxSw()。


如果當前 task 呼叫 uC/OS-II 提供的系統服務,並使得更高優先順序 task 處於 READY 狀態,uC/OS-II 就會借助上面提到的向量位址找到 OSCtxSw()。在系統服務呼叫的最後,uC/OS-II 會呼叫 OSSched(),並由此來推斷當前 task 不再是要執行的最重要的 task 了。OSSched() 先將最高優先順序 task 的地址裝載到 OSTCBHighRdy 中,再透過呼叫 OS_TASK_SW() 來執行軟中斷或陷阱指令。注意,變數 OSTCBCur 早就包含了指向當前 task 的 TCB (OS_TCB) 的指標。軟中斷 (或陷阱) 指令會強制一些處理器暫存器 (比如返回位址和處理器狀態字組) 到當前 task 的堆疊中,並使處理器執行 OSCtxSw()。OSCtxSw() 的原型如程式清單 L8.2 所示。這些程式碼必須寫在組合語言中,因為使用者不能直接從 C 中存取 CPU 暫存器。注意在 OSCtxSw() 和使用者定義的函式 OSTaskSwHook() 的執行過程中,中斷是禁止的。








程式清單 L8.2 Pseudo code for OSCtxSw()



void OSCtxSw(void)
{
保存處理器暫存器;

將當前 task 的堆疊指標保存到當前 task 的 OS_TCB 中:

OSTCBCur->OSTCBStkPtr = Stack pointer;

呼叫使用者定義的 OSTaskSwHook();

OSTCBCur = OSTCBHighRdy;

OSPrioCur = OSPrioHighRdy;

得到需要恢復的 task 的堆疊指標:

Stack pointer = OSTCBHighRdy->OSTCBStkPtr;

將所有處理器暫存器從新 task 的堆疊中恢復出來;

執行中斷返回指令;
}

OS_CPU_A.ASM, OSIntCtxSw()


OSIntExit() 透過呼叫 OSIntCtxSw() 來從 ISR 中執行切換的功能。因為 OSIntCtxSw() 是在 ISR 中被呼叫的,所以可以斷定所有的處理器暫存器都被正確地保存到了被中斷的 task 的堆疊之中。實際上除了我們需要的東西外,堆疊結構中還有其他的一些東西。OSIntCtxSw() 必須要清理堆疊,這樣被中斷的 task 的堆疊結構內容才能滿足我們的需要。


要想瞭解 OSIntCtxSw(),使用者可以看看 uC/OS-II 呼叫該函式的過程。使用者可以參看圖 F8.2 來幫助理解下面的描述。假設中斷不能巢狀 (即ISR不會被中斷),中斷是允許的,並且處理器正在執行 task 級的程式碼。當中斷來臨的時候,處理器會結束當前的指令,識別中斷並且初始化中斷處理過程,包括將處理器的狀態暫存器和返回被中斷的 task 的位址保存到堆疊中 [F8.2(1)]。至於究竟哪些暫存器保存到了堆疊上,以及保存的順序是怎樣的,並不重要。



Figure 8.2 Stack contents during an ISR


接著,CPU 會呼叫正確的 ISR。uC/OS-II 要求使用者的 ISR 在開始時要保存剩下的處理器暫存器 [F8.2(2)]。一旦暫存器保存好了,uC/OS-II 就要求使用者或者呼叫 OSIntEnter(),或者將變數 OSIntNesting 加 1。在這個時候,被中斷 task 的堆疊中只包含了被中斷 task 的暫存器內容。現在,ISR 可以執行中斷服務了。並且如果 ISR 發消息給 task (透過呼叫 OSMboxPost() 或 OSQPost()),恢復 task (透過呼叫 OSTaskResume()),或者呼叫 OSTimeTick() 或 OSTimeDlyResume() 的話,有可能使更高優先順序的 task 處於 READY 狀態。


假設有一個更高優先順序的 task 處於 READY 狀態。uC/OS-II 要求使用者的 ISR 在完成中斷服務的時候呼叫 OSIntExit()。OSIntExit() 會告訴 uC/OS-II 到了返回 task 級程式碼的時間了。呼叫 OSIntExit() 會導致呼叫者的返回位址被保存到被中斷的 task 的堆疊中 [F8.2(3)]。


OSIntExit() 剛開始時會禁止中斷,因為它需要執行 critical section 的程式碼。根據 OS_ENTER_CRITICAL() 的不同執行過程,處理器的狀態暫存器會被保存到被中斷的 task 的堆疊中 [F8.2(4)]。OSIntExit() 注意到由於有更高優先順序的 task 處於 READY 狀態,被中斷的 task 已經不再是要繼續執行的 task 了。在這種情況下,指標 OSTCBHighRdy 會被指向新 task 的 OS_TCB,並且 OSIntExit() 會呼叫 OSIntCtxSw() 來執行 task switch。呼叫 OSIntCtxSw() 也同樣使返回位址被保存到被中斷的 task 的堆疊中 [F8.2(5)]。


在使用者切換 task 的時候,使用者只想將某些項 ([F8.2(1)] 和 [F8.2(2)]) 保留在堆疊中,並忽略其他項 (F8.2(3),(4)和(5))。這是透過調整堆疊指標 (加一個數在堆疊指標上) 來完成的 [F8.2(6)]。加在堆疊指標上的數必須是明確的,而這個數主要依賴於移植的目標處理器 (位址空間可能是 16,32 或 64位元),所用的編譯器,編譯器選項,記憶體模式等等。另外,處理器狀態字組可能是 8,16,32 甚至 64 位元寬,並且 OSIntExit() 可能會分配局部變數。有些處理器允許使用者直接增加常數定義到堆疊指標中,而有些則不允許。在後一種情況下,可以透過簡單的執行一定數量的 pop 指令來實現相同的功能。一旦堆疊指標完成調整,新的堆疊指標會被保存到被切換出去的 task 的 OS_TCB 中 [F8.2(7)]。


OSIntCtxSw() 是 uC/OS-II (和 uC/OS) 中唯一的與組譯器相關的函式;在我收到的 e-mail 中,關於該函式的 e-mail 明顯多於關於 uC/OS 其它方面的。如果在多次 task switch 後用戶的系統崩潰了,用戶應該懷疑堆疊指標在 OSIntCtxSw() 中是否被正確地調整了。


OSIntCtxSw() 的原型如程式清單 L8.3 所示。這些程式碼必須寫在組合語言中,因為使用者不能直接從 C 語言中存取 CPU 暫存器。如果使用者的編譯器支援插入組合語言程式碼的話,使用者就可以將 OSIntCtxSw() 程式碼放到 OS_CPU_C.C 檔案中,而不放到 OS_CPU_A.ASM 檔案中。正如使用者所看到的那樣,除了第一行以外,OSIntCtxSw() 的程式碼與 OSCtxSw() 是一樣的。這樣在移植實例中,使用者可以透過將程式碼轉移到 OSCtxSw() 中來減少 OSIntCtxSw() 的程式碼數量。








程式清單 L8.3 Pseudo code for OSIntCtxSw()



void OSIntCtxSw(void)
{
調整堆疊指標來去掉在呼叫:
OSIntExit(),
OSIntCtxSw()過程中壓入堆疊的多餘內容;

將當前 task 堆疊指標保存到當前 task 的OS_TCB中:
OSTCBCur->OSTCBStkPtr = 堆疊指標;

呼叫使用者定義的OSTaskSwHook();

OSTCBCur = OSTCBHighRdy;

OSPrioCur = OSPrioHighRdy;

得到需要恢復的 task 的堆疊指標:
堆疊指標 = OSTCBHighRdy->OSTCBStkPtr;

將所有處理器暫存器從新 task 的堆疊中恢復出來;

執行中斷返回指令;
}

OS_CPU_A.ASM, OSTickISR()


uC/OS-II 要求使用者提供一個時鐘資源來實現時間的延時和期滿功能。時鐘節拍應該每秒鐘發生 10 ~ 100 次。為了完成該 task,可以使用硬體時鐘,也可以從交流電中獲得 50/60Hz 的時鐘頻率。


使用者必須在開始多工排程後 (即呼叫 OSStart() 後) 允許時鐘節拍中斷。換句話說,就是使用者應該在 OSStart() 執行後,uC/OS-II 啟動執行的第一個 task 中初始化節拍中斷。通常所犯的錯誤是在呼叫 OSInit() 和 OSStart() 之間允許時鐘節拍中斷(如程式清單 L8.4 所示)。








程式清單 L8.4 Incorrect place to start the tick interrupt



void main(void)
{
.
.
OSInit(); /* 初始化 uC/OS-II */
.
.
/* 應用程式初始化程式碼 ... */
/* ... 呼叫 OSTaskCreate() 建立至少一個 task */
.
.
允許 tick interrupt; /* 千萬不要在這裏允許! */
.
.
OSStart(); /* 開始多工排程 */
}

有可能在 uC/OS-II 開始執行第一個 task 前時鐘節拍中斷就發生了。在這種情況下,uC/OS-II 的執行狀態不確定,使用者的應用程式也可能會崩潰。


時鐘節拍 ISR 的原型如程式清單 L8.5 所示。這些程式碼必須寫在組合語言中,因為使用者不能直接從 C 語言中存取 CPU 暫存器。如果使用者的處理器可以透過單條指令來增加 OSIntNesting,那麼使用者就沒必要呼叫 OSIntEnter()了。增加 OSIntNesting 要比透過函式呼叫和返回快得多。OSIntEnter() 只增加 OSIntNesting,並且作為 critical section 程式碼中受到保護。








程式清單 L8.5 Pseudo code for Tick ISR



void OSTickISR(void)
{
保存處理器暫存器;
呼叫 OSIntEnter() 或者直接將 OSIntNesting 加 1;
呼叫 OSTimeTick();
呼叫 OSIntExit();
恢復處理器暫存器;
執行中斷返回指令;
}



OS_CPU_C.C


uC/OS-II 的移植實例要求使用者編寫六個簡單的 C 函式:




  • OSTaskStkInit()



  • OSTaskCreateHook()



  • OSTaskDelHook()



  • OSTaskSwHook()



  • OSTaskStatHook()



  • OSTimeTickHook()



唯一必要的函式是 OSTaskStkInit(),其他五個函式必須得宣告但沒必要包含程式碼。


OS_CPU_C.C, OSTaskStkInit()


OSTaskCreate() 和 OSTaskCreateExt() 透過呼叫 OSTaskStkInt() 來初始化 task 的堆疊結構,因此,堆疊看起來就像剛發生過中斷並將所有的暫存器保存到堆疊中的情形一樣。圖 F8.3 顯示了 OSTaskStkInt() 放到正被建立的 task 堆疊中的東西。注意,在這裏我假設了堆疊是從上往下長的。下面的討論同樣適用於從下往上長的堆疊。


在使用者建立 task 的時候,使用者會傳遞 task 的地址,pdata 指標, task 的堆疊頂端和 task 的優先順序給 OSTaskCreate() 和 OSTaskCreateExt()。雖然 OSTaskCreateExt() 還要求有其他的參數,但這些參數在討論 OSTaskStkInt() 的時候是無關緊要的。為了正確初始化堆疊結構,OSTaskStkInt() 只要求剛才提到的前三個參數和一個附加的選項,這個選項只能在 OSTaskCreateExt() 中得到。



Figure 8.3 Stack frame initialization with 'pdata' passed on the stack


回顧一下,在 uC/OS-II 中,無限迴圈的 task 看起來就像其他的 C 函式一樣。當 task 開始被 uC/OS-II 執行時,task 就會收到一個參數,好像它被其他的 task 呼叫一樣。





void MyTask (void *pdata)
{
/* 對 'pdata' 做某些操作 */

for (;;) {
/* task 程式碼 */
}
}

如果我想從其他的函式中呼叫 MyTask(),C 編譯器就會先將呼叫 MyTask() 的函式的返回位址保存到堆疊中,再將參數保存到堆疊中。實際上有些編譯器會將 pdata 參數傳至一個或多個暫存器中。在後面我會討論這類情況。假設 pdata 會被編譯器保存到堆疊中,OSTaskStkInit() 就會簡單的模仿編譯器的這種動作,將 pdata 保存到堆疊中 [F8.3(1)]。但是結果表明,與 C 函式呼叫不一樣,呼叫者的返回位址是未知的。使用者所擁有的是 task 的開始位址,而不是呼叫該函式 (task) 的函式的返回位址!事實上使用者不必太在意這點,因為 task 並不希望返回到其他函式中。


這時,使用者需要將暫存器保存到堆疊中,當處理器發現並開始執行中斷的時候,它會自動地完成該過程的。一些處理器會將所有的暫存器存入堆疊,而其他一些處理器只將部分暫存器存入堆疊。一般而言,處理器至少得將程式計數器的值 (中斷返回位址) 和處理器的狀態字存入堆疊 [F8.3(2)]。很明顯,處理器是按一定的順序將暫存器存入堆疊的,而使用者在將暫存器存入堆疊的時候也就必須依照這一順序。


接著,使用者需要將剩下的處理器暫存器保存到堆疊中 [F8.3(3)]。保存的命令依賴於使用者的處理器是否允許使用者保存它們。有些處理器用一個或多個指令就可以馬上將許多暫存器都保存起來。使用者必須用特定的指令來完成這一過程。例如,Intel 80x86 使用 PUSHA 指令將 8 個暫存器保存到堆疊中。對 Motorola 68HC11 處理器而言,在中斷回應期間,所有的暫存器都會按一定順序自動的保存到堆疊中,所以在使用者將暫存器存入堆疊的時候,也必須依照這一順序。


現在是時候討論這個問題了:如果使用者的 C 編譯器將 pdata 參數傳遞到暫存器中而不是堆疊中該作些什麼?使用者需要從編譯器的檔案中找到 pdata 儲存在哪個暫存器中。pdata 的內容就會隨著這個暫存器的儲存被存放在堆疊中。



Figure 8.4 Stack frame initialization with 'pdata' passed in register


一旦使用者初始化了堆疊,OSTaskStkInit() 就需要返回堆疊指標所指的位址 [F8.3(4)]。OSTaskCreate() 和 OSTaskCreateExt() 會獲得該位址並將它保存到 TCB (OS_TCB) 中。處理器檔案會告訴使用者堆疊指標會指向下一個堆疊未使用位置,還是會指向最後存入資料的堆疊單元位置。例如,對 Intel 80x86 處理器而言,堆疊指標會指向最後存入資料的堆疊單元位置,而對 Motorola 68HC11 處理器而言,堆疊指標會指向下一個未使用的位置。


OS_CPU_C.C, OSTaskCreateHook()


當用 OSTaskCreate() 或 OSTaskCreateExt() 建立 task 的時候就會呼叫 OSTaskCreateHook()。該函式允許使用者或使用使用者的移植實例的使用者擴展 uC/OS-II 的功能。當 uC/OS-II 設完了自己的內部結構後,會在呼叫排程器之前呼叫 OSTaskCreateHook()。該函式被呼叫的時候中斷是禁止的。因此使用者應儘量減少該函式中的程式碼以縮短中斷的回應時間。


當 OSTaskCreateHook() 被呼叫的時候,它會收到指向已建立 task 的 OS_TCB 的指標,這樣它就可以存取所有的結構成員了。當使用 OSTaskCreate() 建立 task 時, OSTaskCreateHook() 的功能是有限的。但當使用者使用 OSTaskCreateExt() 建立 task 時,使用者會得到 OS_TCB 中的擴展指標 (OSTCBExtPtr),該指標可用來存取 task 的附加資料,如浮點暫存器,MMU 暫存器, task 計數器的內容,以及除錯資訊。


只用當 OS_CFG.H 中的 OS_CPU_HOOKS_EN 被設為 1 時才會產生 OSTaskCreateHook() 的程式碼。這樣,使用使用者的移植實例的使用者可以在其他的檔案中重新定義 hook 函式。


OS_CPU_C.C, OSTaskDelHook()


當 task 被刪除的時候就會呼叫 OSTaskDelHook()。該函式在把 task 從 uC/OS-II 的內部 task 鏈表中解開之前被呼叫。當 OSTaskDelHook() 被呼叫的時候,它會收到指向正被刪除 task 的 OS_TCB 的指標,這樣它就可以存取所有的結構成員了。OSTaskDelHook() 可以用來檢驗 TCB 擴展是否被建立了 (一個非空指標) 並進行一些清除操作。OSTaskDelHook() 不返回任何值。


只用當 OS_CFG.H 中的 OS_CPU_HOOKS_EN 被設為 1 時才會產生 OSTaskDelHook() 的程式碼。


OS_CPU_C.C, OSTaskSwHook()


當發生 task switch 的時候呼叫 OSTaskSwHook()。不管 task switch 是透過 OSCtxSw() 還是 OSIntCtxSw() 來執行的都會呼叫該函式。OSTaskSwHook() 可以直接存取 OSTCBCur 和 OSTCBHighRdy,因為它們是全域變數。OSTCBCur 指向被切換出去的 task 的 OS_TCB,而 OSTCBHighRdy 指向新 task 的 OS_TCB。注意在呼叫 OSTaskSwHook() 期間中斷一直是被禁止的。因為程式碼的多少會影響到中斷的回應時間,所以使用者應儘量使程式碼簡化。OSTaskSwHook() 沒有任何參數,也不返回任何值。


只用當 OS_CFG.H 中的 OS_CPU_HOOKS_EN 被設為 1 時才會產生 OSTaskSwHook() 的程式碼。


OS_CPU_C.C, OSTaskStatHook()


OSTaskStatHook() 每秒鐘都會被 OSTaskStat() 呼叫一次。使用者可以用 OSTaskStatHook() 來擴展統計功能。例如,使用者可以保持並顯示每個 task 的執行時間,每個 task 所用的 CPU 程度,以及每個 task 執行的頻率等等。OSTaskStatHook() 沒有任何參數,也不返回任何值。


只用當 OS_CFG.H 中的 OS_CPU_HOOKS_EN 被設為 1 時才會產生 OSTaskStatHook() 的程式碼。


OS_CPU_C.C, OSTimeTickHook()


OSTaskTimeHook() 在每個時鐘節拍都會被 OSTaskTick() 呼叫。實際上,OSTaskTimeHook() 是在節拍被 uC/OS-II 真正處理,並通知使用者的移植實例或應用程式之前被呼叫的。OSTaskTimeHook() 沒有任何參數,也不返回任何值。


只用當 OS_CFG.H 中的 OS_CPU_HOOKS_EN 被設為 1 時才會產生 OSTaskTimeHook() 的程式碼。



創作者介紹
創作者 立你斯 的頭像
立你斯

立你斯學習記錄

立你斯 發表在 痞客邦 留言(0) 人氣()