转自:http://cms.mcuapps.com/devscenes/ds0001/
筆者閱讀之餘,覺得該文足夠精簡摘要地歸納出 RTOS 的要旨,因此嘗試作個翻譯,以饗懶得看英文的讀者。又筆者翻譯的方式有時會較為接近意譯,並不拘泥於原文的字句,也可能會配合文意,另外附加一些補充資料。
此外,MCUApps 將會持續整理一些各具特色的 RTOS 系統的資料,置於
RTOS 技術資料 提供給大家參考。
以下就是我們的譯文
本文將會闡釋 RTOS 的一些基本概念,但不會擴及個別的 RTOS 與其特點。為了簡化起見,只會討論 RTOS 最具代表性的一些重要特性。
在談過 RTOS 的架構,以及為何我們需要採用它之後,我 (IAR System 的 Mats Pettersson) 將會解釋典型 RTOS 中的每一個基本元件,並且展示他們是如何被整合到系統之中。
在本文中出現的少許程式碼範例,所採用的是 Express Logic 的 ThreadX 系統。
Thread 導向的設計
設計嵌入式應用幾乎總是相當具有挑戰性的。為了降低複雜度,我們通常會採用 threads 導向的設計,把一個專案切分成比較易於管理的小塊(也就是 threads),而後每一個 thread 負責該應用程式的一部分。這樣的系統有助於識別 threads 之間的重要順序。也就是說,某些 threads 具有即時性的需求,必須盡量快速並且正確地回應它們。如果你的系統採用了專業的 RTOS,一定會有劃分 threads 優先權 (prioritization) 的設計。除了優先權之外,也會提供一套乾淨而且被仔細測試過的 API,有助於簡化 threads 之間的通訊。
所以如果我們採用 RTOS 的話,就會獲得一些工具能夠:
確保能夠在即時約束條件 (real-time constraints) 內執行時間關鍵 (time-critical) 部分的程式碼。或許同樣都很重要,但高優先權的 threads 所需要的即時行為,並不會受到低優先權 threads 的影響。
確保易於開發和維護複雜的應用。開發和維護小的 threads,比起硬搞整套應用更為容易。此外,對於低優先權 threads 的更動也不會影響到高優先權 threads 的即時處理。
能夠將整體應用程式的不同部分,分派給多個開發人員。每一個開發人員能夠擔負應用程式的一個或多個 threads,而且當他們在進行開發工作時,還會有一套乾淨的 API 能夠讓不同的 modules / threads 相互溝通。
當然也可以不必動用到 RTOS,就把應用大卸八塊成為不同的 threads。但是採用了 RTOS 的話,你不但能夠創建 threads,同時也具備一些讓它們能夠彼此溝通的工具,再加上能夠確保能夠在即時約束條件內執行完畢 threads 具時間關鍵的部分工作。由於採用了 RTOS,不同 threads 之間的介面將會變得非常乾淨,在進行開發的時候你就可以省時省力。
RTOS 是如何工作的?
RTOS 的核心被稱為 kernel ,並提供有一個可以透過 kernel 去創建 threads 的 API。一個 thread 就像是一個擁有自己的堆疊、並帶有 Thread 控制區塊(TCB – Thread Control Block)的函式。除了 thread 本身私有的堆疊之外,每個 TCB 也保有一部分該 thread 的狀態訊息。
kernel 還包含有一個 scheuler ,scheuler 會按照一套排程機制來執行 threads。各種 scheulers 之間主要的差異,就是如何分配執行他們所管理之各種 threads 的時間。基於優先權的 preemptive scheuler 是嵌入式 RTOS 之間最流行和普遍的 threads 調度演算法。通常情況下,相同優先權的 threads 會以 round-robin 循環的方式加以執行。
多數內核還會利用系統時脈 (system tick) 中斷,其典型的頻率為 10ms。如果在 RTOS 中缺乏系統時鐘,仍然能夠有某種基本形式的調度,但時間相關的服務則否。這種與時間有關的服務內容包括:軟體定時器、thread 睡眠 API 呼叫、thread 時間片段、以及逾時的 API 呼叫。
為了實現系統時脈中斷,可以透過嵌入式晶片的硬體計時器。大多數的 RTOS 有能力動態地擴增或重新設置計時器的中斷頻率,以便讓該系統進入睡眠,直到被下一個計時器期限或外部事件喚醒。例如,如果你有一個對耗能敏感的應用程式,您可能不希望每 10ms 就運行一次不必要的系統時脈處理程序。所以假設應用程式處於閒置狀態,想要把下一個定時器期限改為 1000ms。在這種情況下,計時器可以被重新規劃成 1000ms,應用程式則會進入低功耗模式。一旦在這種模式下,處理器將呈現休眠狀態,直到產生了外部事件、或是計時器的 1000ms 到期。在任一種情況之下,當處理器恢復執行時,RTOS 就會根據已經經過了多少時間來調整內部時間,並恢復 RTOS 和應用程式處理。如此一來,處理器只會在執行應用程式有事可做時進行運算。空閒期間處理器可以睡眠,並且節省電力。
在本文稍後,將有進一步關於調度演算法和系統時脈的討論。
思考 thread…
也許開始一個 RTOS 應用程式最好的方式,就是去思考如何將一個應用程式劃分為不同的 threads。例如,一個簡化的引擎輸入控制應用程式可以劃分為以下 threads:
引擎溫度
機油壓力
每分鐘轉數 (RPM, rotation per minute)
用戶輸入
這些模組可以被設置為 threads,也可以被劃分成子 threads。例如:
這種劃分成子 threads 的工作可以不斷繼續下去,直到可以被一個單獨的 thread 加以處理為止。
RTOS 的組件
讓我們來看看一個 RTOS 必須提供哪些功能,而這些功能又如何在不同的應用中派上用場。
Threads
 Threads 類似於函式,但每個 thread 都會有它自己的堆疊和 thread 控制塊(TCB)。然而與大多數函式不同的是,一個 thread 幾乎總是一個無限循環。也就是說,一旦它被創建,它(經常是)永遠不會退出。
1
2
3
4
5
6
void ThreadSendData ( void )
{
while ( 1 ) {
// Send the data...
}
}
一個 thread 總是處於幾種 states 其中之一。一個 thread 可以準備好被執行,也就是說,在 READY 狀態。或者該 thread 可能會被暫停(pending),也就是該 thread 在進入 READY 狀態之前,正在等待某事發生。這就是所謂的 WAITING state。
以下是我們對於 ThreadX 中 states 的一段簡短描述。
State 說明
Executing 這是當前正在運行的 thread。
Ready 這個 thread 已經就緒
Suspended 這個 thread 正在等待某件東西。這可能是一個事件或一個消息,也可能是等 RTOS 時鐘到達某個特定的值(延遲)。
Completed 一個處於完成狀態的 thread 已經完成其運算處理、 並且自它的入口函數返回。(處於完成狀態的 thread 不會再次被執行。)
Terminated 一個 thread 之所以會處於終止狀態,是因為另一個 thread 或是該 thread 本身呼叫了 tx_thread_terminate 服務。(處於終止狀態的 thread 不會再次被執行。)
注 :不同的 RTOS 可能會對這些 states 賦予不同的名稱*
Scheduler
你可以從兩種主要類型的 schedulers 中加以挑選:
事件驅動 (Event-driven) – 具優先權控制的調度演算法
通常,不同的 threads 會有不同的響應要求。例如,在一個控制馬達、鍵盤和顯示器的應用程式中,馬達通常比鍵盤和顯示器需要更快的反應時間。這必須得靠一個事件驅動的 scheduler。
在事件驅動的系統中,每個 thread 都會被分配到一個優先權,而優先權最高的 thread 就會被執行。執行的順序都仰賴於這個優先權。規則非常簡單:schedule 從所有就續的 threads 中挑出具備最高優先權的 thread 予以執行。
分時共享 (Time-sharing)
最常見的分時演算法叫做 round-robin 。也就是 scheduler 列出系統中所有 threads 的清單,然後一一查驗下一個 thread 是否就緒可以被執行。如果 thread 為 READY 時,該 thread 就會執行。每個 thread 又分派到一份 時間切片 (time-slice)
。時間切片是每一回合中,單一 thread 被容許之最長的執行時間。
典型的基於優先權的 preemptive scheduler 會同時支援 preemptive 與 non-preemptive 的調度。在 preemptive 的情況下,較高優先權的 thread 會立即打斷(搶佔)執行中的低優先權 thread。而具備相同優先權的 threads 則會以 non-preemptive 的方式進行調度,而正在被執行的 thread 會繼續完成它的執行,再輪到另一個具備相同或較低優先權的 thread。
指派優先權 (Assigning priorities)
將正確的優先權指派給不同的 threads 是相當重要的。許多論文都在探討在基於 RTOS 的應用程式中,如何把這件工作做到完美。我們並不會深入這個題目,但是在此要談一些有用的規則:
盡量採用最少的優先權層級
僅在搶佔是絕對必要的狀況下指派不同的優先權。這樣可以降低系統中 context switches 的數量,越少的 context switches,就表示有越多的時間是花費在執行應用程式代碼。
確認滿足了你應用程式中所有的時間關鍵約束條件
端視你的應用程式類型而定,這可能會很難搞。有個解法是採用 RMA (Rate Monotonic Algorithm)。ThreadX RTOS 也提供了一個獨門絕活叫做 preemption-threshold。這能夠被用於降低 context switches,也能確保應用程式 threads 的執行。詳見 http://www.nxtbook.com/nxtbooks/cmp/esd0311/#/26
Internet 上有許多關於指派優先權的相關資訊。想要深入的朋友可以看下列兩則文章:
Thread 通訊 (Thread communications)
在 RTOS 應用程式中,我們也必須能夠在 threads 之間相互通訊。通訊可以採用 event、semaphore(旗號)的形式,或者是以訊息的方式傳送給另外一個 thread。
最基本的通訊是透過 event 。一個中斷服務函式 (ISR) 也能夠傳送一個 event 給某個 thread。有些 RTOS 還能將單一 event 傳送給多個 threads。
Semaphores 通常被用於保護共享的資源,譬如說不只一個 thread 想要對同一塊記憶體(變數)進行讀寫時。作法是讓一個變數不會隨著另外一個作用中的 thread 而被改變。原則就是在你讀寫這塊記憶體之前,你必須先獲取一個以一個變數保護著的 semaphore。一旦你獲得這個 semaphore 之後,其他人都不能對這塊記憶體進行讀寫,直到你釋放 semaphore 為止。這樣一來你就可以確保同時只有一個 thread 會對該記憶體位置或變數進行讀寫。
訊息 (messages) 則能夠讓你將資料傳送給一個或多個 threads。這些訊息幾乎可以是任意的大小,通常以 mailbox 或 queue 的方式來實作。而 mailboxes 與 message queues 的行為會隨不同的 RTOS 而有所差異。
通盤整合 (Putting it all tegether)…
讓我們透過下面這張圖重新歸納我們至今所談過的東西。
那麼現在,讓我們看看我們能夠利用手上的組件做些什麼。試著回想一下,我們需要創建一個引擎控制的應用,而我們有一個微控制器和 LCD 顯示屏。我們還想在 LCD 顯示屏上顯示當前時間。我們將會重用早先將此應用程式分為不同的模組和 threads 的例子。(我們會忽略 “用戶輸入” 便於解說。)
針對這個練習,我們將創建下面的 threads:
// Read the temperature and oil pressure of the engine
void T_OilRead(void) and void T_TempRead(void)
// Print the temperature and oil pressure on the LCD
void T_OilLCD(void) and void T_TempLCD(void)
// Controls the engine
void T_EngineCTRL(void)
系統方塊圖如下所式:
其中我們以不同的顏色和外觀,分別表示了 threads、mailboxes、以及 semaphores 等元素。
關於這個系統,我們將需要一個 semaphore 來控制對 LCD 的寫入。因為我們有兩個不同的 threads,所以我們需要這個機制,如果其中一個 thread 被另一個打斷,輸出很有可能會被搞爛掉。
我們需要以下的信號傳導機制。
mailboxes M_TempLCD
與 M_OilLCD
。
這些 mailboxes 包含要列印在 LCD 上的訊息。
mailboxes M_TempCTRL
和 M_OilCTRL
。
這些 mailboxes 包含要送給引擎控制 thread 的訊息。
semaphore S_LCD
。
這個 semaphore 將會確保在同一時刻當下,只有唯一的一個 thread 能夠列印到 LCD 上。
系統時脈 System ticks
我們現在有了一個系統,它可以更新 LCD上的馬達資訊、並顯示當前時間。然而,有一個非常重要的事情還沒搞定,就是 RTOS 的時脈功能。正如早些時候所言,kernel 需要隨時能夠掌控系統,然後才能夠執行例如根據 RTOS 的 scheduler 切換 threads 的工作。這通常是透過 MCU 內部計時器所驅動的時脈處理 API 呼叫達成。如果沒有系統時脈,整個系統就無法動彈。
連接到 MCU 的硬體
拼圖的最後一塊就是將這一切連接到硬體。在我們的例子中,我們將假設我們可以利用一套韌體函式庫。我們假設我們有以下的韌體 API 函式:
// Set text X,Y coordinate in characters
void GLCD_TextSetPos(int X, int Y);
// Set draw window XY coordinate in pixels
void GLCD_SetWindow(int X_Left, int Y_Up, int X_Right, int Y_Down);
// Function that reads the engine temperature
char Engine_ReadTemp(void);
// Function that reads the engine oil pressure
char Engine_ReadOilPressure(void);
// Function that controls the engine
char Engine_Control(int CtrlValue);
現在,我們只要把這些片段兜在一起,看看我們的系統是否如同預期一般工作。最簡單的方法就是使用 RTOS 廠商的 BSP,如果有辦法取得的話。對於大多數 RTOS 而言,都會有為各種設備所設計的許多不同的 BSP。
如果是要在沒有 BSP 的情況下來建立一個應用,則需要:
將編譯器/組譯器所需 include paths 加入到建構工具。
我們需要確保編譯器和組譯器可以找到我們系統所需要的頭文件。
添加共通的 RTOS 函式庫或源碼文件。
我們需要添加 kernel。kernel 可能是源碼,也可能會是函式庫。
添加 target-specific 的 RTOS 文件。
很多 RTOS 對於不同的電路板/設備都會有一個電路板支援包(BSP, Board Support Package)。在這樣的一個情況下,你只需要確保你已經包含了這些 target-specific 的文件。在這樣的一個 BSP 環境下,你應該就會擁有已經配置好時鐘的程式碼,能夠正確配置 RTOS 的系統時脈。
您的應用程式必須初始化 RTOS,並且在啟動 RTOS 之前就創建至少一個 thread。這通常是在 main() 函式中,但也可以在你應用程式的其他地方。
假設我們已經根據 RTOS 的規格添加了所有的文件,並且設置好建構選項,我們就可以開始創建我們應用程式中的 threads。但是在此之前,我們需要定義它們的 Thread Control Blocks(TCB)。
下面就是 Express Logic 之 ThreadX 的 TCB 定義範例。
1
2
3
4
5
6
// Express Logic ThreadX example
TX_THREAD TCBEngineCTRL ;
TX_THREAD TCBOilLCD ;
TX_THREAD TCBTempLCD ;
TX_THREAD TCBOilRead ;
TX_THREAD TCBTempRead ;
現在,剩下的就是在我們創建的 threads 中建立我們的主要功能,以便完成我們所需要的應用程式。
在 ThreadX 中,我們會在 tx_application_define
這個 API 函式中創建我們最初的 threads。因此,在 main
中我們唯一必須做的事情就是去呼叫 API 函式tx_kernel_enter
。隨後 ThreadX 就會呼叫 tx_application_define()
,我們在其中創建了我們的 threads、semaphores、message queues 之類一開始就會需要用到的東西。
大多數的 RTOS 都允許你在運行應用程式時才動態地創建資源,例如 threads 之類的。但是,我們至少需要有一個 thread 作為 schedular,用以接管我們的應用程式。
當做到這一步時,我們就可以啟動我們的 RTOS,並且讓 schedular 接手,以確保在正確的時間執行正確的 thread。
總結
這篇文章解釋了 kernel、threads、訊息(如 events 和 mailboxes)、scheduler、以及 RTOS 應用程式中主要元件的概念。一個真正的應用程式還會需要更多的細節,例如,如何創建 mailboxes 和 events。
如果你剛接觸 RTOS,上手最好的辦法就是從各家 RTOS 供應商的許多範例中,挑一個架起來並且用用看。在上述文章中,我已經用上了 Express Logic 的程式碼片段作為示範。Express Logic 提供了許多的應用範例,並有許多不同電路板和設備的 BSPs (Board Support Packages)。