一.序言
本資料是Trevor Martin編寫的《The Designers Guide to the Cortex-M Processor Family》的摘要,并得到Elsevier的再版許可。查詢更多細節,請到本資料尾部進階章節。
本資料著力于介紹RTX,RTX可運行在基于Cortex-M構架的微控制器上。尤其,RTX符合CMSIS標準。CMSIS全稱"Cortex Microcontroller Interface Standard",定義了基于Cortex-M構架的微控制器標準的RTOS Api。CMSIS RTOS Api提供基于RTOS開發的接口,掌握后可跨多系列微控制器使用。另外,CMSIS RTOS Api也為高級應用(如Java虛擬機,UML)等提供標準接口。同時,CMSIS RTOS Api也是不依賴于硬件層的標準接口,支持代碼重復使用。
作為新手,適應RTOS需要大量的練習,但RTOS的便利性會使使用者再也不想回到裸板程序。
1.1.起步-安裝工具
要運行本資料中的示例代碼,必須安裝MDK-ARM工具鏈。可在如下地址下載最新版本,并運行安裝程序。
http://www.keil.com/mdk5/install 
該安裝程序是MDK-ARM的核心,包括IDE,編譯/鏈接工具和基本調試模塊。不包括對基于Cortex-M構架微控制器的具體型號支持,要支持具體的型號需要下載"Device Family Pack"(集合了啟動文件,Flash編程算法和調試支持)。
本資料中的練習,均基于STM32F103RB微控制器,所以需要安裝對應的"Device Family Pack"。
初次安裝完畢,Pack Installer會自動啟動。也可通過工具欄啟動,如圖所示:
在Pack Installer選擇相應的模塊安裝,Pack Installer自動下載并安裝。 1.2.安裝例程
本資料中涉及到的例程也被做成CMSIS pack下載。在Pack Installer中雙擊Hitex.CMSIS_RTOS_Turorial.xxxx.pack file即可自動安裝。 安裝完成后界面如圖: 1.3.硬件需求
不需要硬件支持!
Keil工具鏈包含基于Cortex-M構架微控制器的模擬器,并可完全模擬其模型(包括CPU和外設)。這就意味著可以在debug模式下模擬運行。
二.綜述
學習本資料,分三個步驟:
- 首先建立一個基于Cortex-M構架微控制器的RTOS工程,并運行起來;
- 進一步深入RTOS每個細節,體驗RTOS對應用程序的貢獻和閃光點;
- 對RTOS有個整體認識后,深入探討如何配置RTOS選項;
對于沒有接觸過RTOS的新手來說,以下兩點需要克服:
- 進程(或者叫任務),重點在理解進程的運行原理;
- 進程見通訊,重點是理解進程間的同步通訊;
2.1.進入RTOS的第一步
- RTOS的核心是調度器(支持輪換、搶占和協同多任務),時間和內存管理服務。進程間通訊由額外模塊如信號、信號量、互斥量、消息隊列、消息郵箱等支持完成。而,中斷則通過特權進程由內核調度。
 
2.2導入CMSIS-RTOS Api
- 添加頭文件 <cmsis_os.h>即可調用CMSIS_RTOS Api,如下:
#include <cmsis_os.h> 該頭文件作為CMSIS-RTOS標準文件。對于符合CMSIS-RTOS標準的Keil 內置RTX是默認的Api。其他RTOS應該會包含其特有Api,但只要支持CMSIS_RTOS,即可通過此方式引入。
2.3進程
2.4創建進程
- 一個創建進程的示例:
Void thread1(void const *parm);
osThreadDef(thread1,osPriorityNormal, 1, 0);
優先級 實例數 stacksize(0默認size)
osThreadId thread1_id = osThreadCreate(osThread(thread1),NULL);
Void thread1(void const *parm)
{
//init code While(1) //thread body { ...... } }
2.5進程優先級和進程管理
- 一經創建,系統分配進程ID,進程ID是管理進程的標識。
進程有優先級特性,如圖:
管理進程包括設置優先級,讀取優先級,進程消亡等,Api如下: osStatus osThreadSetPriority(threadID,priority); osPriority osThreadGetPriority(threadID); osStatus osThreadTerminate(threadID);
2.6多實例進程
2.7開始RTOS
默認,RTOS自main函數起接管系統調度,所以main函數是第一個運行中進程。一旦進入main函數,首先調用osKernelInitialize()停用調度,以爭取時間完成硬件初始化(如GPIO配置,USART配置等)和創建需要的進程或其他RTOS組件,而后調用osKernelStart()將系統控制權交還給RTOS,如下代碼示例:
Void main(void)
{
osKernelInitialize(); //user code Init_thread(); osKernelStart(); }
上例中,main進程在完成進程創建后運行到"}"時消亡,這在RTOS中是不推薦的。Main函數也可作為一個進程,并通過ID管理,如下:
osThreadId main_id;
Void main(void)
{
osKernelInitialize(); //user code main_id = osThreadGetId(); //返回當前進程ID Init_thread(); osKernelStart(); While(1) //thread body { } }
三.時間管理RTOS提供基本的時間管理組件。
3.1延時函數
事件組件中最基本的服務就是延時函數,在應用程序中可直接調用延時函數,非常方便。
插曲:盡管RTOS內核約5K左右,相比于非RTOS系統中延時循環的無用代碼消耗,RTOS還是優勢明顯,這就是RTOS出現的原因。
函數原型:void osDelay(uint32_t millisec)
調用延時函數的進程會進入WAIT_DELAY狀態并持續延時函數制定的時間(millisec),調度器轉向其他READY狀態進程。延時結束,進程進入READY狀態,等待調度器調度。
3.2等待事件
除了等待制定的時間,osWait也可中斷進程并使進程進入等待狀態直至被重新觸發(觸發時間可以是信號、信號量、消息等),并且osWait同樣支持指定延時的周期。
函數原型:osStatus osWait(uint32_t millisec)
注意:keil RTX不支持此節內容。
3.3虛擬定時器
CMSIS-RTOS支持虛擬定時器,虛擬定時器向下計數,溢出時運行用戶定義的call-back函數。虛擬定時器可以定位為單次和循環模式,步驟如下:
- 定義回調函數(call-back);
- 定義定時器結構體;
- 創建定時器;
- 啟動定時器;
如Ex 6 Virtual Timers代碼摘錄: void callback(void const *pram) { Switch((uint32_t) pram) { case 0: GPIOB->ODR ^= 0x8; Break; case 1: GPIOB->ODR ^= 0x4; Break; case 2: GPIOB->ODR ^= 0x2; Break; case 3: Break; } } osTimerDef(osTimer(Timer0_handle),callback); osTimerDef(osTimer(Timer1_handle),callback); osTimerDef(osTimer(Timer2_handle),callback); osTimerDef(osTimer(Timer3_handle),callback); osTimerId timer0 = osTimerCreate(osTimer(Timer0_hanlder),osTimerPeriodic,(void *)0); osTimerId timer1 = osTimerCreate(osTimer(Timer1_hanlder),osTimerPeriodic,(void *)1); osTimerId timer2 = osTimerCreate(osTimer(Timer2_hanlder),osTimerPeriodic,(void *)2); osTimerId timer3 = osTimerCreate(osTimer(Timer3_hanlder),osTimerPeriodic,(void *)3); osTimerStart(timer0_handle,0x100); osTimerStart(timer1_handle,0x100); osTimerStart(timer2_handle,0x100); osTimerStart(timer3_handle,0x100); 3.4微秒延時
借用系統中的Systick原始計數值可實現微妙延時。微妙延時不觸發調度,它僅僅暫停執行指定時間段,遵循如下步驟:
- 獲取Systick原始計數值;
- 定義延時時間段;
- 等待延時結束;
示例代碼如下: Uint32_t tick,delayPeriod;
tick = osKernelSysTick(); //1
delayPeriod = osKernelTickMicroSec(100); //2
do{
.... }while((osKernelSysTick()-tick) < delayPeriod); //3 3.5空閑進程
空閑進程在系統沒有可用進程時被調用,以防止系統沒有進程可用。
空閑進程定義在RTX_Conf_CM.c中,并允許用戶自定義代碼。通常情況下,空閑進程中配置CPU進入低功耗狀態。如此一來,當Systick中斷或其他中斷時喚醒調度,如有可運行進程則運行進程,否則繼續進入低功耗狀態。
示例代碼:Ex 7 Idle
四.信號
4.1功能描述
CMSIS-RTOS keil RTX的進程支持多達16個信號,信號保存在進程控制塊中。當進程中存在等待信號時(不管是單個等待信號還是多個等待信號),進程暫停執行直至其他進程發出了被等待的信號。
調用信號等待函數,將觸發當前進程中止運行并進入等待狀態。處于等待狀態的進程滿足以下兩個條件時退出等待,進入可被調度狀態: 信號等待函數原型:osEvent osSignalWait(uint32_t signals,uint32_t millisec) 等待時間設置為0fffff,表示始終不溢出; 等待時間設置為0時,表示任一信號置位即可引起中止等待; 其他進程可置位或清除等待信號: Uint32_t osSignalSet(osThreadId thread_id,uint32_t signals) Uint32_t osSignalClear(osThreadId thread_id,uint32_t signals) 另,調用osEvent.value.signals,返回值指示當前被置位信號。 4.2例程
"Ex8 Signals"
4.3中斷進程
CMSIS-RTOS使用Systick中斷作為系統時鐘。因Systick中斷的服務等級設定為最低級,當中斷服務程序(ISR)的運行時間超出一個Systick中斷時,系統中斷將受到影響。
于是,在CMSIS-RTOS中,正確的處理方式是將中斷服務定義為一個進程,進程中等待信號。而在中斷服務中,僅僅給中斷進程發送信號。從而極大縮短中斷服務程序的長度,轉而在線程中執行。
示例代碼:
osThreadDef(osThread(isr_thread),osPriorityNormal,1,0); osThreadId Isr_thread_id = osThreadCreate(osThread(isr_thread),NULL); 中斷線程:void isr_thread(void const *pram)
{ .... While(1) { osSignalWait(isrSignal,waitForever); ..... } } 中斷服務程序:void IRQ_Handler(void) { osSignalSet(Isr_thread_id,isrSignal); } 4.4內核權限調用SVC
CMSIS-RTOS運行在unprivilege模式下,當需要在進程中訪問privilege資源時,有兩種方式:
- 在配置文件中提升進程的權限至privilege狀態(如下圖所示),但會造成所有的進程運行在privilege模式下,影響系統安全。
參考例程"Ex9 interrupt signal" - 在需要privilege權限時運行"系統級"代碼。
4.4.1SVC
遵循如下步驟:
- 新建"系統級"代碼列表(.s匯編文件),如下圖:
SVC_Tables.s代碼如下:
AREA SVC_TABLE, CODE, READONLY EXPORT SVC_Count SVC_Cnt EQU (SVC_End-SVC_Table)/4 SVC_Count DCD SVC_Cnt ; Import user SVC functions here. IMPORT __SVC_1 //第一個"系統級"代碼 EXPORT SVC_Table SVC_Table ; Insert user SVC functions here. SVC 0 used by RTL Kernel. DCD __SVC_1 ; user SVC function SVC_End END 其中__SVC_1就是"系統級"用戶代碼函數。 - 建立"系統級"代碼與"進程級"代碼接口,示例:
void __svc(1) init_ADC (void); __svc(1)代表SVC_Tables.s中第一個"系統級"代碼,同樣如果有多個應用,依次遞增即可。 - 編寫"系統級"代碼,如下圖:
Void __SVC_1 (void) { ...... } 在完成定義后,進程中調用init_ADC()將自動執行__SVC_1()中的代碼。
五.信號量
5.1功能描述
與信號類似,信號量是兩個或多個進程同步的方法。
信號量項是一個包含多個信號的容器。當進程執行到需要信號量的代碼段(進程申請信號量),如果信號量中有信號可用(包含不少于一個的信號),則進程繼續執行,并且信號量中的信號數自減一。相反,如信號量中無信號可用(包含0個信號),則進程中止執行并等待信號量中的信號可用。
同時,進程中可向信號量添加信號數目,從而引起信號量中的可用信號數增一。
如上圖。假定信號量初始化為只有一個可用信號,當任務1提出申請時,信號量中含有1個可用信號,則任務1繼續執行并引起信號量中的可用信號為0。此時任務2若提出申請因信號量中無可用信號,任務2進入信號量等待狀態,直至任務1釋放信號量。 可見,進程可以釋放信號給信號量。 5.2創建信號量
示例代碼:
osSemaphoreId sem1; osSemaphoreDef(sem1); sem1 = osSemaphoreCreate(osSemaphore(sem1),SIX_TOKENS); 定義了一個含有6個可用信號的信號量sem1。 信號量初始化后,進程中即可申請信號量,使用函數:
osSemaphoreWait(osSemaphoreId sem_id,uint_32 millisec)
Millisec = 0xffff wait for ever
信號量使用結束后,釋放信號量,使用函數:
osSemaphoreRelease(osSemaphoreId sem_id);
5.3例程
Ex11 Interrupt Signals
5.4使用場景
5.4.1信號
兩個線程間同步執行是信號量最基本的應用。示例代碼:
osSemaphoreId sem_id;
osSemaphoreDef(sem_id);
void task1(void)
{
Sem_id = osSemaphoreCreate(osSemaphore(sem1),0); While(1) { Fun(A); osSemaphoreRelease(sem1); } }
Void task2(void)
{
While(1) { osSemaphoreWait(sem1,osWaitForever); Fun(B); } }
在這個案例中,Fun(A)始終先于Fun(B)執行。
5.4.2限額
限額用于限制某些資源的配額。例如,某個指定的內存塊只運行指定數目的應用訪問。
如下例程,信號量初始化為5個信號,每個申請信號量的線程造成信號自減,當獲取信號量的進程為5個時,后續申請信號量的進程進入等待狀態,直至已獲取配額的進程釋放信號量,代碼例程:
osThreadId sem_id;
osThreadDef(sem1);
Void task1(void)
{
Sem_id = osThreadCreate(osThread(sem1),5); While(1) { osSemaphoreWait(sem1,osWaitForever); ProcessBuffer(); osSemaphoreRelease(sem1); } }
Void task2(void)
{
While(1) { osSemaphoreWait(sem1,osWaitForever); ProcessBuffer(); osSemaphoreRelease(sem1); } }
......
例程"Ex12 Multiplex"。
5.4.3互鎖(2個線程同步)
互鎖是兩個線程同步的另一種通用模式。互鎖確保兩個線程得到同一互鎖點。如下例程:
osSemaphoreId arrival1,arrival2;
osSemaphoreDef(sem1);
osSemaphoreDef(sem2);
Void task1(void)
{
arrival1 = osSemaphore(osSemephore(sem1),0); arrival2= osSemaphore(osSemephore(sem2),0); While(1) { FunA1(); osSemaphoreRelease(arrival2); osSemaphoreWait(arrival1); FunA2(); } }
Void task2(void)
{
While(1) { FunB1(); osSemaphoreRelease(arrival1); osSemaphoreWait(arrival2); FunB2(); } }
此例程中,確保FunA2()、FunB2()同步執行。
5.4.4屏障(多個線程同步)
屏障是多個進程同步的有效模式,它的總體思路:設置一個初始化為0的信號量作為屏障,并在所有進程達到同步點時依次釋放屏障中的信號量,達到同步執行的目的。
例程"Ex14 Barrier"
5.5注意事項
信號量是RTOS中極端有效的模式。然而,因信號量可在進程中增減甚至銷毀,信號量中可用配額數比較難把控,使用時必須實時把控可用配額數。
六.互斥量6.1功能描述
單從功能來講,互斥量可以看做只含有一個可用配額且不可被創建和銷毀的特殊信號量。互斥量主要用于防止對硬件的訪問沖突,比如同一時刻只能有一個應用訪問串口,否則將造成數據混亂。
申請互斥量的進程,必須等待互斥量中存在有效配額,否則進入等待狀態。
6.2創建互斥量
創建互斥量與創建信號量類似,示例代碼如下:
osMutexId uart_mutex;
osMutexDef(Mutex1);
進程中創建互斥量:uart_mutex = osMutexCreate(osMutex(Mutex1));
其他進程申請互斥量:osMutexWait(uart_mutex);
使用完畢釋放互斥量:osMutexRelease(uart_mutex);
6.3例程
例程"Ex15 Mutex"
6.4注意事項
互斥量的使用限制多,也更安全,但扔要注意以下內容:
- 使用完畢必須及時釋放互斥量,否則將造成后續進程無法使用該資源;
- 調用ThreadTerminate()函數消亡進程時,必須確保該進程沒有占用互斥量,否則將造成后續進程無法使用該資源;
七.數據交換信號、信號量、互斥量只用于進程之間的觸發,但對進程間的數據交換無能為力。進程間數據交換最簡單的方式是全局變量,但即使在簡單的系統中,把握和靈活應用全局變量也是不小的挑戰,因為全局變量會引起一系列不可預知錯誤。
在RTOS中,消息隊列和郵箱隊列是進程間數據交互最為有效、安全的方式。
消息隊列和郵箱隊列的工作方式基本一樣,唯一的區別是消息隊列中傳輸的是待交換數據,而郵箱隊列中傳輸是指向待交換數據的指針,如下圖所示:
使用消息隊列和郵箱隊列進行數據交換有如下好處: - 規范進程間數據交換的接口和緩存,為設計子系統提供可能;
- 規范進程的輸入、輸出,使進程獨立測試、調試成為可能;
7.1消息隊列
7.1.1創建消息隊列
創建消息隊列,遵循如下步驟:
- 聲明消息隊列ID,示例:osMessageQId Q_id;
- 定義消息隊列結構體,示例:osMessageQDef(Q1,16_Message_Slots,unsigned int);其中16_Message_Slots指示空間大小為16,unsigned int指示空間類型;
- 在進程中創建消息隊列,示例:Q_id = osMessageQCreate(osMessageQ(Q1),NULL);
- 聲明解析消息隊列數據的osEvent類型數據,示例:osEvent result;
- 在進程中發送數據到消息隊列,例程:osMessagePut(Q_id,Data,osWaitForever);
- 在另一進程中獲取消息隊列數據,例程:result = osMessageGet(Q_id,osWaitForever);result.value.xxx;
其中,osEvent是個union結構體,如下所示: Union{ Uint32_t v; Void *p; Uint32_t signals; }value 7.1.2例程
"Ex16 Message queue"
7.2內存鏈
7.2.1功能描述
消息隊列中的數據類型可以是數據本身,也可以是指向數據的指針。
消息隊列中存儲的數據是指向特定內存區域的指針,這樣的進程間交換數據的方式成為內存鏈。
結構體可以達到規范化特定內存區域的目的。
7.2.2創建內存鏈
創建內存鏈,遵循如下步驟:
1.定義結構體,用于規范內存塊及初始化指針,示例:
Typedef struct { Uint8_t led1; Uint8_t led2; Uint8_t led3; Uint8_t led4; }memory_block_t; 2.初始化內存鏈,示例:
osPoolId pool_id; osPoolDef(pool_t,ten_blocks,memory_block_t); 在進程中創建pool_id = osPoolCreate(osPool(pool_t)); 3.初始化消息隊列,示例:
osMessageQDef(q1,ten_blocks,memory_block_t); osMessageQid q_id; 在進程中創建:q_id = osMessageQCreate(osMessageQ(q1),NULL); 4.發送消息隊列,示例:
memory_block_t *led = (memory_block_t *)osPoolAlloc(pool_id); led->led1 = xx;.... osMessagePut(q_id,led,osWaitForever); 5.讀取消息隊列,示例:
osEvent evt; evt = osMessageGet(q_id,osWaitForever); memory_block_t *temp = (memory_block_t *)evt.value.p; 6.使用完畢釋放內存鏈:示例:
osPoolFree(pool_id,temp); 7.2.3例程
"Ex16 MemoryPool"
7.3郵箱隊列
7.3.1功能描述
郵箱隊列是將內存鏈融合到消息隊列中而成,郵箱隊列中存儲的同樣是指向特定內存區域的指針。
7.3.2創建郵箱隊列
同樣采用7.2.2中的結構體作為數據基礎,Typedef struct {
Uint8_t led1; Uint8_t led2; Uint8_t led3; Uint8_t led4; }memory_block_t; 創建郵箱隊列遵循如下步驟:
1.創建郵箱隊列,例程:
osMailQDef(MQ_1,ten_blocks,memory_block_t); osMailQId mq_id; 進程中創建,mq_id = osMailQCreate(osMailQ(MQ_1),NULL); 2.發送數據,例程:
Memory_block_t *led = (memory_block_t *)osMailAlloc(mq_id); led->led1 = xx;.... osMailQPut(mq_id,led); 3.讀取數據,例程:
osEvent evt; evt = osMailGet(mq_id,osWaitForever); Memory_block_t *temp = (memory_block_t *)evt.value.p; 4.使用完畢釋放郵箱隊列,示例:
osMailFree(mq_id,temp); 7.3.3例程
"Ex17 MailQueue"
八.系統配置
掌握前面的內容,對CMSIS-RTOS有了總體的認識。CMSIS-RTOS包括進程管理、時間管理、進程間通訊等。
本章著力于討論如何配置系統。CMSIS-RTOS針對基于Cortex-M構架的處理器,提供一個統一的配置文件,RTX_Conf_CM.c,如下圖:

8.1進程參數
在討論進程的相關章節中一經接受了創建進程的基礎知識。
每個進程,系統分配一塊內存空間用作進程棧(默認200bytes),棧空間在進程創建時指定。
應用中最多允許運行的進程數可配置。
由于進程棧空間、進程數可配置,應用中的內存需求也可很容易的計算出來。
8.2內核調試支持
內核可配置項,包括:
8.2.1追蹤溢出
選擇該項"stack overflow checking",出現進程棧溢出RTOS內核調用os_error函數并進入死循環。該項主要用于調試階段的問題追蹤,當然也可自定義os_error函數用于最終的應用中打印錯誤信息,os_error代碼在RTX_CONF_CM.C文件中,源碼: /* OS Error Codes */ #define OS_ERROR_STACK_OVF 1 #define OS_ERROR_FIFO_OVF 2 #define OS_ERROR_MBX_OVF 3 #define OS_ERROR_TIMER_OVF 4 extern osThreadId svcThreadGetId (void); /// \brief Called when a runtime error is detected /// \param[in] error_code actual error code that has been detected void os_error (uint32_t error_code) { /* HERE: include optional code to be executed on runtime error. */ switch (error_code) { case OS_ERROR_STACK_OVF: /* Stack overflow detected for the currently running task. */ /* Thread can be identified by calling svcThreadGetId(). */ break; case OS_ERROR_FIFO_OVF: /* ISR FIFO Queue buffer overflow detected. */ break; case OS_ERROR_MBX_OVF: /* Mailbox overflow detected. */ break; case OS_ERROR_TIMER_OVF: /* User Timer Callback Queue overflow detected. */ break; default: break; } for (;;); } 8.2.2監控棧使用率
選擇"stack usage watermark"項,oxcc樣式自動寫入進程棧。運行時,watermark用于計算最大棧內存使用率,并在"system and Event viewer"窗口報告,如下圖所示: 8.2.3用戶定時器數
如用戶定時器數量與應用中使用的虛擬定時器不符,會造成os_timer()函數失效。 8.2.4進程運行權限選擇
如"Ex9 interruter signal",進程運行權限可配置。 8.3系統時基
默認的系統時基是Cortex-M中的SysTick定時器。但,也支持自定義使用其他定時器作為系統時基。
8.4時間片
默認的時間片是5ms。
8.5調度選項
調度器支持如下三種調度模式:
1.搶占式
此模式下,系統中進程擁有不同的優先級,當擁有高優先級的進程進入"ready"狀態,調度器轉入高優先級進程運行。 2.輪詢式
此模式下,系統根據時間片為每個進程分配運行時間,處于運行態的進程在時間片到來時觸發調度(注意,即使高優先級的進程進入"ready"狀態也要等時間片結束)。 3.輪詢、搶占式(默認狀態)
此模式下,系統根據時間片為每個進程分配運行時間,處于運行態的進程在時間片到來時或高優先級的進程進入"ready"態觸發調度(注意,高優先級的進程進入"ready"狀態將馬上觸發調度)。 4.協同式
此模式下,進程擁有相同的優先級,有且僅有運行態的進程主動申請系統調度才會引起調度。 8.6源碼調試
如果用戶需要源碼級別的調試,遵循如下步驟:
- 新建文本文件,命名為"xxx.ini";
- "xxx.ini"中添加,SET SRC = <PATH>,其中<PATH>是RTX源碼的文件夾,默認是C:\Keil\ARM\pack\arm\cmsis\<version>\cmsis\rots\rtx。
- 在調試文件中導入"xxx.ini",如下圖所示:
注:"xxx.ini"中xxx代表任意長度滿足PC操作系統命名規格的字符串。
|