劉長勇,王宜懷 ,蔡闖華,蔣建武
1.武夷學院 數學與計算機學院,福建 武夷山 354300
2.蘇州大學 計算機科學與技術學院,江蘇 蘇州 215006
3.認知計算與智能信息處理福建省高校重點實驗室,福建 武夷山 354300
2014 年ARM 公司推出了mbedOS,它是一種專為物聯網(IoT)中的“物體”設計的開源嵌入式實時操作系統(Real-Time Operating System,RTOS)[1-2],能提供精確的實時控制,以保證系統的實時性需求[3],具有線程管理與調度、內存管理、時間管理、隊列管理等基本功能要素,在協議棧和IP 網絡組件[4]、通信技術和安全訪問服務機制[5]、物聯網設備平臺[6]等方面得到廣泛應用。基于RTOS 的嵌入式開發,對應用系統的穩定性、實時性和啟動時間等都有嚴格的要求,在啟動過程中要完成棧空間、堆空間、線程棧大小、時間嘀嗒、線程調度機制等方面的設置[7],具有啟動時間短且復雜性高的特點。因此,充分理解RTOS 的啟動流程,有助于開發人員設計出響應速度快、穩定性強的嵌入式系統。目前,有關操作系統啟動的研究集中在Android 操作系統[8]、MQX 操作 系 統[9]、μC/OS-III 操 作 系 統[10]、Linux 操 作 系 統[11]、MTX 操作系統[12]以及ARM 嵌入式系統的啟動過程[13]等方面,但對mbedOS的啟動剖析研究方面缺乏相關資料。為此,本文將利用蘇州大學與ARM 公司聯合出品的嵌入式開發集成開發環境AHL-GEC-IDE 和金葫蘆AHL-A 系列Cortex-M0+內核的KL36 微控制器[14](即AHL-AN100VL 型號開發板),基于SD-mbedOS 工程框架對mbedOS的啟動流程進行分析,剖析其從芯片上電啟動,到main函數,最終進入mbedOS啟動的全過程,結合關鍵代碼、宏定義函數、流程圖、SVC中斷[15]等分析其實現的原理,可為mbedOS的應用研究和在不同微控制器上的移植提供基礎,也可為其他RTOS的啟動分析提供借鑒參考。
SD-mbedOS工程框架的啟動過程分為芯片上電啟動和實時操作系統mbedOS 啟動兩部分,如圖1 所示。芯片上電啟動包括堆棧指針初始化、啟動復位向量、關中斷、關閉看門狗、系統時鐘初始化、開中斷、復制初始化數據至RAM、清空BSS數據段、初始化標準庫函數和運行主函數main,這一部分的內容與操作系統無關。當進入主函數main 中調用mbedOS_start 函數時,才會將系統控制權移交給mbedOS,由它完成線程的調度工作。

圖1 SD-mbedOS工程框架啟動流程
芯片上電啟動是從調用startup_MKL36Z4.S這個啟動文件開始,芯片內部機制首先從Flash 的0x00000000地址處取出中斷向量表第一個表項的內容,賦給內核寄存器主棧指針MSP,即完成堆棧指針的設置;芯片內部機制從Flash 的0x00000004 地址處取出第二個表項(即復位向量Reset_Handler 的首地址),賦給程序計數器PC。然后,關中斷、調用系統初始化函數SystemInit 完成系統時鐘初始化和開中斷。接著,將已初始化的數據復制到RAM 中、清空BSS 數據段和調用__libc_init_array 完成系統標準庫函數的初始化。最后,轉入main函數執行,調用mbedOS_start函數完成mbedOS啟動。
從芯片上電啟動到main函數后,將進行mbedOS啟動。在該函數中會將主線程thd_main 和主線程執行函數app_init 作為參數傳入mbedOS_start 函數,由它負責mbedOS 啟動。mbedOS 啟動過程包括定義臨時變量、設置mbedOS 堆棧區、重定向中斷向量表至RAM、內核初始化、設置主線程屬性、創建主線程、啟動內核等方面。
(1)定義臨時變量
定義三個臨時變量用于創建主線程,變量main_obj用于存儲線程控制塊,變量main_attr用于存儲將要創建的主線程屬性,main_stack數組用來作為主線程的棧空間。
os_thread_t main_obj;
osThreadAttr_t main_attr;
__attribute__((aligned(8)))char main_stack[512];
(2)設置mbedOS堆棧區
在mbedOS中只有一個主棧,主棧的棧底位置通常設置在RAM 的最高地址加1 處,由高地址向低地址方向分配棧空間,主棧指針MSP 指向棧頂位置。堆通常用于存放臨時變量,由程序員動態分配和釋放,它一般采用鏈表的方式來管理變量,堆在內存中位于bss 區和棧區之間,堆是從RAM 的低地址向高地址方向使用。調用函數mbed_set_stack_heap 設置mbedOS 的堆與棧的起始位置和大小,主要是對已定義的四個變量進行初始化操作。
//取得空閑RAM起始地址與大小
unsigned char *free_start=HEAP_START;
uint32_t free_size=HEAP_SIZE;
//初始化棧大小與起始地址
mbed_stack_isr_size=ISR_STACK_SIZE<free_size?ISR_STACK_SIZE:free_size;
mbed_stack_isr_start=free_start+free_size-mbed_stack_isr_size;
free_size-=mbed_stack_isr_size;
//初始化堆大小與起始地址
mbed_heap_size=free_size;
mbed_heap_start=free_start;
(3)重定向中斷向量表
在系統啟動時中斷向量表是在Flash中的,位于Flash的0x00000000地址處,通過函數mbed_cpy_nvic重定向中斷向量表到RAM 中,實際上就是將中斷向量表拷貝到RAM中。這樣做的好處在于當用戶程序需要改寫中斷服務程序時,可以將相應的中斷向量指向用戶改寫后的中斷服務程序,即將中斷向量表中相應的表項改寫為中斷服務程序的地址。
//取得系統控制塊VTOR寄存器的值
uint32_t*old_vectors=(uint32_t*)SCB->VTOR;
//內存地址起始地址:0x1FFFF800
uint32_t*vectors=(uint32_t*)NVIC_RAM_VECTOR_ADDRESS;
//將48個中斷向量拷貝到內存地址中
for(int i=0;i<NVIC_NUM_VECTORS;i++)
vectors[i]=old_vectors[i];
//設置VTOR寄存器指向新的地址
SCB->VTOR=(uint32_t)NVIC_RAM_VECTOR_ADDRESS;
內核初始化過程主要由內核初始化函數osKernel-Initialize、SVC 觸發封裝函數__svcKernelInitialize、實際初始化函數svcRtxKernelInitialize 以及中斷服務程序SVC_Handler 組成。其調用順序為osKernelInitialize→__svcKernelInitialize→ 觸 發 SVC 中 斷 SVC_Handler→svcRtxKernelInitialize。
(1)SVC觸發封裝函數
內核初始化函數osKernelInitialize 功能是判斷當前是否處于中斷服務程序中或已經屏蔽了中斷,若處于中斷服務程序中或已經屏蔽了中斷,則返回出錯代碼;否則調用SVC 觸發封裝函數。SVC 觸發封裝函數__svcKernelInitialize 是一個宏定義函數,展開后是C 語言與匯編語言混合編程代碼,其功能是為觸發SVC 中斷服務程序做前期準備工作,主要有:①將要執行的實際內核初始化函數指針放入R7 寄存器中,即將svcRtxKernelInitialize函數地址給R7;②使線程棧指針PSP 中的值為觸發SVC 中斷后的棧頂;③觸發SVC 中斷;④將調用R7中函數得到的返回值存放在PSP棧中。其宏定義為SVC0_0M(KernelInitialize,osStatus_t),展開后如下:
#define SVC0_0M(f,t) //宏定義
__attribute__((always_inline)) //強制內聯
//定義為靜態內聯函數
__STATIC_INLINE t __svc##f(void){
SVC_ArgN(0); //定義r0作為通用寄存器
//保存svcRtxKernelInitialize函數地址到R7中
SVC_ArgF(svcRtx##f);
//用于觸發SVC中斷服務程序
SVC_Call0M(SVC_In0,SVC_Out1,SVC_CL1);
return(t)__r0; } //函數返回值由r0傳回
(2)SVC中斷服務程序
SVC中斷服務程序執行流程如圖2所示,分為兩部分:前一部分為調用內核初始化實際函數svcRtxKernel-Initialize 前的流程,主要完成對SVC 調用號的判斷、讀出準備調用函數的入口地址等工作;后一部分為調用內核初始化實際函數svcRtxKernelInitialize 函數(R7 寄存器存放該函數的地址)后的流程,主要完成恢復調用前的堆棧指針、函數調用后的返回值入棧、判斷是否進行上下文切換、退出SVC中斷等工作。

圖2 SVC中斷處理程序執行流程
內核初始化之后,需要創建一個自啟動線程,以便內核啟動后執行它,由它創建其他用戶線程,這個自啟動線程稱為“主線程(thd_main)”。創建主線程的過程主要由變量定義、創建線程函數osThreadNew、帶上下文創建線程函數osThreadContextNew、SVC觸發封裝函數__svcThreadNew、實際創建線程函數svcRtxThreadNew以及中斷服務程序SVC_Handler構成。
其調用順序為osThreadNew→osThreadContextNew→__svcThreadNew→觸發SVC中斷服務程序SVC_Handler→svcRtxThreadNew。
(1)創建主線程的準備工作
在mbedOS內核中,使用線程控制塊TCB指針來表示一個線程。因此,創建主線程前要給TCB 和棧分配空間,并對屬性結構體main_attr進行初始化。
main_attr.stack_mem=main_stack;//棧指針
main_attr.stack_size=sizeof(main_stack);//棧大小
main_attr.cb_mem=&main_obj;//控制塊指針
main_attr.cb_size=sizeof(main_obj);//控制塊大小
main_attr.priority=osPriorityNormal;//優先級為 24
main_attr.name="main_thread";//名稱
(2)主線程的創建
主線程的創建最終是通過觸發SVC中斷服務程序調用svcRtxThreadNew 函數來完成的,該函數的主要任務包括定義臨時變量、判斷參數及內存空間的合法性、初始化主線程TCB和線程棧、調用線程提交服務程序、設置主線程狀態并放入就緒隊列中等方面,其執行流程如圖3所示。

圖3 主線程創建執行流程
在主線程創建成功后,mbedOS 會把主線程加入到就緒隊列中等待調用,接著將進行內核的啟動,為操作系統的運行做最后的準備工作。啟動內核主要由內核啟動函數osKernelStart、SVC觸發封裝函數__svcKernelStart、實際內核啟動函數svcRtxKernelStart 及中斷服務程序SVC_Handler 組成。其調用順序為osKernelStart→__svcKernelStart→觸發SVC中斷服務程序SVC_Handler→svcRtxKernelStart。
(1)內核啟動的實際執行函數
最終實現內核啟動的是svcRtxKernelStart 函數,其主要任務包括為線程的調度做好所有必要的準備、創建必要的功能線程、設置時間嘀嗒、使能定時器中斷、線程調度、切換棧指針、修改內核狀態等方面,其執行流程如圖4所示。

圖4 內核啟動執行流程
(2)運行到主線程
在mbedOS 啟動過程中,通過調用svcRtxKernel-Start函數來啟動內核。在內核啟動期間,先后建立了主線程main_thread(優先級為24)、空閑線程osRtxInfo.thread.idle(優先級為1)和定時器線程osRtxInfo.timer.thread(優先級為40),這三個線程的狀態都為就緒態,都被放到就緒隊列中,并按優先級高低排列就緒,即定時器線程、主線程和空閑線程。當svcRtxKernelStart 函數執行完成后返回到SVC 中斷時,會在SVC 中斷中進行上下文切換,此時由于有一個優先級最高的線程(即定時器線程)處于激活態,它的線程控制塊指針被放在了osRtxInfo.thread.run.next 中,當前線程與下一線程是不同的(即osRtxInfo.thread.run.curr≠osRtxInfo.thread.run.next),這時就會進行上下文切換,將定時器線程切換為當前線程,當從SVC 中斷返回時就會轉到定時器線程中執行。在定時器線程osRtxInfo.timer.thread 啟動后,先創建一個消息隊列,再從消息隊列取消息,由于此時消息隊列是空的,定時器線程被阻塞。之后mbedOS會進行線程調度,從就緒隊列中選擇優先級最高的線程(此時為主線程main_thread),將其狀態設置為激活態,準備運行。至此,CPU的控制權轉交給主線程,接著將由主線程執行函數app_init(定義在08_mbedOsPrgapp_init.cpp 文件中)負責創建用戶線程。圖5 展示了從定時器線程切換到主線程運行這一過程中的函數調用關系,從中可以看出最終轉到app_init函數執行。
在mbedOS 啟動過程中,涉及到中斷向量表、程序代碼、常量、變量、堆、棧等空間分配問題,下面先給出KL36 微控制器結構,然后分析mbedOS 啟動過程中Flash和RAM空間的使用情況。

圖5 從定時器線程切換到主線程運行的函數調用關系
KL36 微控制器包括ARM Cortex-M0+內核、存儲器模塊、外設模塊及相關總線等,存儲器模塊與32位的高性能系統總線相連,外設模塊與32位外設總線相連,還提供擴展總線連接其他外圍設備,其結構如圖6所示。

圖6 KL36微控制器結構
KL36 片內 Flash 大 小 為 64 KB,地址范圍為0x00000000~0x0000FFFF,一般用來存放中斷向量、程序代碼、常數等。mbedOS啟動后Flash中各個區的地址范圍、大小及作用如表1 所示(表中數據采用十六進制表示,下同)。
(1)mbedOS啟動后RAM使用情況分析
KL36片內RAM為靜態隨機存儲器SRAM,大小為8 KB,地址范圍為0x1FFFF800~0x200017FF,一般用來存儲全局變量、靜態變量、臨時變量(堆棧空間)等。該芯片棧空間的使用方向是從大地址向小地址方向進行的。因此,棧空間的棧頂設置在RAM 地址的最大值+1處。而堆空間的使用方向是從小地址向大地址方向進行的,這樣可以減少重疊錯誤。mbedOS啟動后RAM中各個段的地址范圍、大小及作用如表2所示。
(2)各線程RAM分配情況分析
在mbedOS 的啟動過程中,先后建立了主線程main_thread、空閑線程osRtxInfo.thread.idle、定時器線程osRtxInfo.timer.thread,并啟動定時器SysTick 中斷。在切換到主線程函數app_init 執行之前,這三個線程的RAM 分配情況如表3 所示,在鏈表中的關系如圖7 所示。表中的成員名來源于線程控制塊結構體和線程屬性結構體,sp的值等于stack_mem+stack_size-64(這個64 Byte 的固定區域是用于在線程進行上下文切換時,保存線程的上下文,即R0~R12、R14、R15、xPSR等16個寄存器),0x1FFFF8E0地址表示就緒隊列頭指針。
當定時器線程啟動之后就被阻塞,轉由主線程控制CPU 的使用權,在主線程函數app_init 中分別建立紅燈線程thd_redlight、藍燈線程thd_bluelight 和綠燈線程thd_greenlight三個用戶線程,當這三個用戶線程啟動完后,主線程進入阻塞狀態。此時,系統中有四個線程,分別是空閑線程、紅燈線程、藍燈線程和綠燈線程,這四個線程的RAM分配如表4所示,在鏈表中的關系如圖8所示。

表1 Flash中的各區地址范圍、大小及作用

表2 RAM中的各段地址范圍、大小及作用

表3 執行app_init之前系統線程的RAM分配情況表

圖7 系統線程之間的關系

表4 執行app_init之后線程的RAM分配情況表

圖8 用戶線程之間的關系
mbedOS 的啟動過程是一個極其復雜的過程,涉及到操作系統運行時所需的棧空間、堆空間、線程控制塊等資源的初始化,系統時鐘的設置,就緒隊列、延時隊列和等待隊列等的管理,以及對線程的調度,涉及到函數調用關系也極為復雜。本文通過對SD-mbedOS工程框架啟動流程的分析,簡要地給出了芯片上電啟動過程,通過源碼、宏定義函數、SVC中斷、流程圖等方式來著重剖析mbedOS 的啟動過程,最后分析了mbedOS 的存儲器使用情況。通過剖析,有助于讀者快速理解mbedOS的啟動過程、調度機制和整體架構,為簡化啟動流程、優化執行過程、提升啟動速度等進一步研究工作提供研究基礎,也為mbedOS在不同微控制器上的移植提供了技術基礎。本文涉及到的工程可到蘇州大學嵌入式學習社區網站(http://sumcu.suda.edu.cn)的“教學培訓-教學資料-mbedOS”位置,下載“SD_mbedOS_Start(KL36)”查看。