劉長(zhǎng)勇,王宜懷 ,蔡闖華,蔣建武
1.武夷學(xué)院 數(shù)學(xué)與計(jì)算機(jī)學(xué)院,福建 武夷山 354300
2.蘇州大學(xué) 計(jì)算機(jī)科學(xué)與技術(shù)學(xué)院,江蘇 蘇州 215006
3.認(rèn)知計(jì)算與智能信息處理福建省高校重點(diǎn)實(shí)驗(yàn)室,福建 武夷山 354300
2014 年ARM 公司推出了mbedOS,它是一種專為物聯(lián)網(wǎng)(IoT)中的“物體”設(shè)計(jì)的開源嵌入式實(shí)時(shí)操作系統(tǒng)(Real-Time Operating System,RTOS)[1-2],能提供精確的實(shí)時(shí)控制,以保證系統(tǒng)的實(shí)時(shí)性需求[3],具有線程管理與調(diào)度、內(nèi)存管理、時(shí)間管理、隊(duì)列管理等基本功能要素,在協(xié)議棧和IP 網(wǎng)絡(luò)組件[4]、通信技術(shù)和安全訪問服務(wù)機(jī)制[5]、物聯(lián)網(wǎng)設(shè)備平臺(tái)[6]等方面得到廣泛應(yīng)用。基于RTOS 的嵌入式開發(fā),對(duì)應(yīng)用系統(tǒng)的穩(wěn)定性、實(shí)時(shí)性和啟動(dòng)時(shí)間等都有嚴(yán)格的要求,在啟動(dòng)過程中要完成??臻g、堆空間、線程棧大小、時(shí)間嘀嗒、線程調(diào)度機(jī)制等方面的設(shè)置[7],具有啟動(dòng)時(shí)間短且復(fù)雜性高的特點(diǎn)。因此,充分理解RTOS 的啟動(dòng)流程,有助于開發(fā)人員設(shè)計(jì)出響應(yīng)速度快、穩(wěn)定性強(qiáng)的嵌入式系統(tǒng)。目前,有關(guān)操作系統(tǒng)啟動(dòng)的研究集中在Android 操作系統(tǒng)[8]、MQX 操作 系 統(tǒng)[9]、μC/OS-III 操 作 系 統(tǒng)[10]、Linux 操 作 系 統(tǒng)[11]、MTX 操作系統(tǒng)[12]以及ARM 嵌入式系統(tǒng)的啟動(dòng)過程[13]等方面,但對(duì)mbedOS的啟動(dòng)剖析研究方面缺乏相關(guān)資料。為此,本文將利用蘇州大學(xué)與ARM 公司聯(lián)合出品的嵌入式開發(fā)集成開發(fā)環(huán)境AHL-GEC-IDE 和金葫蘆AHL-A 系列Cortex-M0+內(nèi)核的KL36 微控制器[14](即AHL-AN100VL 型號(hào)開發(fā)板),基于SD-mbedOS 工程框架對(duì)mbedOS的啟動(dòng)流程進(jìn)行分析,剖析其從芯片上電啟動(dòng),到main函數(shù),最終進(jìn)入mbedOS啟動(dòng)的全過程,結(jié)合關(guān)鍵代碼、宏定義函數(shù)、流程圖、SVC中斷[15]等分析其實(shí)現(xiàn)的原理,可為mbedOS的應(yīng)用研究和在不同微控制器上的移植提供基礎(chǔ),也可為其他RTOS的啟動(dòng)分析提供借鑒參考。
SD-mbedOS工程框架的啟動(dòng)過程分為芯片上電啟動(dòng)和實(shí)時(shí)操作系統(tǒng)mbedOS 啟動(dòng)兩部分,如圖1 所示。芯片上電啟動(dòng)包括堆棧指針初始化、啟動(dòng)復(fù)位向量、關(guān)中斷、關(guān)閉看門狗、系統(tǒng)時(shí)鐘初始化、開中斷、復(fù)制初始化數(shù)據(jù)至RAM、清空BSS數(shù)據(jù)段、初始化標(biāo)準(zhǔn)庫(kù)函數(shù)和運(yùn)行主函數(shù)main,這一部分的內(nèi)容與操作系統(tǒng)無關(guān)。當(dāng)進(jìn)入主函數(shù)main 中調(diào)用mbedOS_start 函數(shù)時(shí),才會(huì)將系統(tǒng)控制權(quán)移交給mbedOS,由它完成線程的調(diào)度工作。
圖1 SD-mbedOS工程框架啟動(dòng)流程
芯片上電啟動(dòng)是從調(diào)用startup_MKL36Z4.S這個(gè)啟動(dòng)文件開始,芯片內(nèi)部機(jī)制首先從Flash 的0x00000000地址處取出中斷向量表第一個(gè)表項(xiàng)的內(nèi)容,賦給內(nèi)核寄存器主棧指針MSP,即完成堆棧指針的設(shè)置;芯片內(nèi)部機(jī)制從Flash 的0x00000004 地址處取出第二個(gè)表項(xiàng)(即復(fù)位向量Reset_Handler 的首地址),賦給程序計(jì)數(shù)器PC。然后,關(guān)中斷、調(diào)用系統(tǒng)初始化函數(shù)SystemInit 完成系統(tǒng)時(shí)鐘初始化和開中斷。接著,將已初始化的數(shù)據(jù)復(fù)制到RAM 中、清空BSS 數(shù)據(jù)段和調(diào)用__libc_init_array 完成系統(tǒng)標(biāo)準(zhǔn)庫(kù)函數(shù)的初始化。最后,轉(zhuǎn)入main函數(shù)執(zhí)行,調(diào)用mbedOS_start函數(shù)完成mbedOS啟動(dòng)。
從芯片上電啟動(dòng)到main函數(shù)后,將進(jìn)行mbedOS啟動(dòng)。在該函數(shù)中會(huì)將主線程thd_main 和主線程執(zhí)行函數(shù)app_init 作為參數(shù)傳入mbedOS_start 函數(shù),由它負(fù)責(zé)mbedOS 啟動(dòng)。mbedOS 啟動(dòng)過程包括定義臨時(shí)變量、設(shè)置mbedOS 堆棧區(qū)、重定向中斷向量表至RAM、內(nèi)核初始化、設(shè)置主線程屬性、創(chuàng)建主線程、啟動(dòng)內(nèi)核等方面。
(1)定義臨時(shí)變量
定義三個(gè)臨時(shí)變量用于創(chuàng)建主線程,變量main_obj用于存儲(chǔ)線程控制塊,變量main_attr用于存儲(chǔ)將要?jiǎng)?chuàng)建的主線程屬性,main_stack數(shù)組用來作為主線程的??臻g。
os_thread_t main_obj;
osThreadAttr_t main_attr;
__attribute__((aligned(8)))char main_stack[512];
(2)設(shè)置mbedOS堆棧區(qū)
在mbedOS中只有一個(gè)主棧,主棧的棧底位置通常設(shè)置在RAM 的最高地址加1 處,由高地址向低地址方向分配??臻g,主棧指針MSP 指向棧頂位置。堆通常用于存放臨時(shí)變量,由程序員動(dòng)態(tài)分配和釋放,它一般采用鏈表的方式來管理變量,堆在內(nèi)存中位于bss 區(qū)和棧區(qū)之間,堆是從RAM 的低地址向高地址方向使用。調(diào)用函數(shù)mbed_set_stack_heap 設(shè)置mbedOS 的堆與棧的起始位置和大小,主要是對(duì)已定義的四個(gè)變量進(jìn)行初始化操作。
//取得空閑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)重定向中斷向量表
在系統(tǒng)啟動(dòng)時(shí)中斷向量表是在Flash中的,位于Flash的0x00000000地址處,通過函數(shù)mbed_cpy_nvic重定向中斷向量表到RAM 中,實(shí)際上就是將中斷向量表拷貝到RAM中。這樣做的好處在于當(dāng)用戶程序需要改寫中斷服務(wù)程序時(shí),可以將相應(yīng)的中斷向量指向用戶改寫后的中斷服務(wù)程序,即將中斷向量表中相應(yīng)的表項(xiàng)改寫為中斷服務(wù)程序的地址。
//取得系統(tǒng)控制塊VTOR寄存器的值
uint32_t*old_vectors=(uint32_t*)SCB->VTOR;
//內(nèi)存地址起始地址:0x1FFFF800
uint32_t*vectors=(uint32_t*)NVIC_RAM_VECTOR_ADDRESS;
//將48個(gè)中斷向量拷貝到內(nèi)存地址中
for(int i=0;i<NVIC_NUM_VECTORS;i++)
vectors[i]=old_vectors[i];
//設(shè)置VTOR寄存器指向新的地址
SCB->VTOR=(uint32_t)NVIC_RAM_VECTOR_ADDRESS;
內(nèi)核初始化過程主要由內(nèi)核初始化函數(shù)osKernel-Initialize、SVC 觸發(fā)封裝函數(shù)__svcKernelInitialize、實(shí)際初始化函數(shù)svcRtxKernelInitialize 以及中斷服務(wù)程序SVC_Handler 組成。其調(diào)用順序?yàn)閛sKernelInitialize→__svcKernelInitialize→ 觸 發(fā) SVC 中 斷 SVC_Handler→svcRtxKernelInitialize。
(1)SVC觸發(fā)封裝函數(shù)
內(nèi)核初始化函數(shù)osKernelInitialize 功能是判斷當(dāng)前是否處于中斷服務(wù)程序中或已經(jīng)屏蔽了中斷,若處于中斷服務(wù)程序中或已經(jīng)屏蔽了中斷,則返回出錯(cuò)代碼;否則調(diào)用SVC 觸發(fā)封裝函數(shù)。SVC 觸發(fā)封裝函數(shù)__svcKernelInitialize 是一個(gè)宏定義函數(shù),展開后是C 語言與匯編語言混合編程代碼,其功能是為觸發(fā)SVC 中斷服務(wù)程序做前期準(zhǔn)備工作,主要有:①將要執(zhí)行的實(shí)際內(nèi)核初始化函數(shù)指針放入R7 寄存器中,即將svcRtxKernelInitialize函數(shù)地址給R7;②使線程棧指針PSP 中的值為觸發(fā)SVC 中斷后的棧頂;③觸發(fā)SVC 中斷;④將調(diào)用R7中函數(shù)得到的返回值存放在PSP棧中。其宏定義為SVC0_0M(KernelInitialize,osStatus_t),展開后如下:
#define SVC0_0M(f,t) //宏定義
__attribute__((always_inline)) //強(qiáng)制內(nèi)聯(lián)
//定義為靜態(tài)內(nèi)聯(lián)函數(shù)
__STATIC_INLINE t __svc##f(void){
SVC_ArgN(0); //定義r0作為通用寄存器
//保存svcRtxKernelInitialize函數(shù)地址到R7中
SVC_ArgF(svcRtx##f);
//用于觸發(fā)SVC中斷服務(wù)程序
SVC_Call0M(SVC_In0,SVC_Out1,SVC_CL1);
return(t)__r0; } //函數(shù)返回值由r0傳回
(2)SVC中斷服務(wù)程序
SVC中斷服務(wù)程序執(zhí)行流程如圖2所示,分為兩部分:前一部分為調(diào)用內(nèi)核初始化實(shí)際函數(shù)svcRtxKernel-Initialize 前的流程,主要完成對(duì)SVC 調(diào)用號(hào)的判斷、讀出準(zhǔn)備調(diào)用函數(shù)的入口地址等工作;后一部分為調(diào)用內(nèi)核初始化實(shí)際函數(shù)svcRtxKernelInitialize 函數(shù)(R7 寄存器存放該函數(shù)的地址)后的流程,主要完成恢復(fù)調(diào)用前的堆棧指針、函數(shù)調(diào)用后的返回值入棧、判斷是否進(jìn)行上下文切換、退出SVC中斷等工作。
圖2 SVC中斷處理程序執(zhí)行流程
內(nèi)核初始化之后,需要?jiǎng)?chuàng)建一個(gè)自啟動(dòng)線程,以便內(nèi)核啟動(dòng)后執(zhí)行它,由它創(chuàng)建其他用戶線程,這個(gè)自啟動(dòng)線程稱為“主線程(thd_main)”。創(chuàng)建主線程的過程主要由變量定義、創(chuàng)建線程函數(shù)osThreadNew、帶上下文創(chuàng)建線程函數(shù)osThreadContextNew、SVC觸發(fā)封裝函數(shù)__svcThreadNew、實(shí)際創(chuàng)建線程函數(shù)svcRtxThreadNew以及中斷服務(wù)程序SVC_Handler構(gòu)成。
其調(diào)用順序?yàn)閛sThreadNew→osThreadContextNew→__svcThreadNew→觸發(fā)SVC中斷服務(wù)程序SVC_Handler→svcRtxThreadNew。
(1)創(chuàng)建主線程的準(zhǔn)備工作
在mbedOS內(nèi)核中,使用線程控制塊TCB指針來表示一個(gè)線程。因此,創(chuàng)建主線程前要給TCB 和棧分配空間,并對(duì)屬性結(jié)構(gòu)體main_attr進(jìn)行初始化。
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;//優(yōu)先級(jí)為 24
main_attr.name="main_thread";//名稱
(2)主線程的創(chuàng)建
主線程的創(chuàng)建最終是通過觸發(fā)SVC中斷服務(wù)程序調(diào)用svcRtxThreadNew 函數(shù)來完成的,該函數(shù)的主要任務(wù)包括定義臨時(shí)變量、判斷參數(shù)及內(nèi)存空間的合法性、初始化主線程TCB和線程棧、調(diào)用線程提交服務(wù)程序、設(shè)置主線程狀態(tài)并放入就緒隊(duì)列中等方面,其執(zhí)行流程如圖3所示。
圖3 主線程創(chuàng)建執(zhí)行流程
在主線程創(chuàng)建成功后,mbedOS 會(huì)把主線程加入到就緒隊(duì)列中等待調(diào)用,接著將進(jìn)行內(nèi)核的啟動(dòng),為操作系統(tǒng)的運(yùn)行做最后的準(zhǔn)備工作。啟動(dòng)內(nèi)核主要由內(nèi)核啟動(dòng)函數(shù)osKernelStart、SVC觸發(fā)封裝函數(shù)__svcKernelStart、實(shí)際內(nèi)核啟動(dòng)函數(shù)svcRtxKernelStart 及中斷服務(wù)程序SVC_Handler 組成。其調(diào)用順序?yàn)閛sKernelStart→__svcKernelStart→觸發(fā)SVC中斷服務(wù)程序SVC_Handler→svcRtxKernelStart。
(1)內(nèi)核啟動(dòng)的實(shí)際執(zhí)行函數(shù)
最終實(shí)現(xiàn)內(nèi)核啟動(dòng)的是svcRtxKernelStart 函數(shù),其主要任務(wù)包括為線程的調(diào)度做好所有必要的準(zhǔn)備、創(chuàng)建必要的功能線程、設(shè)置時(shí)間嘀嗒、使能定時(shí)器中斷、線程調(diào)度、切換棧指針、修改內(nèi)核狀態(tài)等方面,其執(zhí)行流程如圖4所示。
圖4 內(nèi)核啟動(dòng)執(zhí)行流程
(2)運(yùn)行到主線程
在mbedOS 啟動(dòng)過程中,通過調(diào)用svcRtxKernel-Start函數(shù)來啟動(dòng)內(nèi)核。在內(nèi)核啟動(dòng)期間,先后建立了主線程main_thread(優(yōu)先級(jí)為24)、空閑線程osRtxInfo.thread.idle(優(yōu)先級(jí)為1)和定時(shí)器線程osRtxInfo.timer.thread(優(yōu)先級(jí)為40),這三個(gè)線程的狀態(tài)都為就緒態(tài),都被放到就緒隊(duì)列中,并按優(yōu)先級(jí)高低排列就緒,即定時(shí)器線程、主線程和空閑線程。當(dāng)svcRtxKernelStart 函數(shù)執(zhí)行完成后返回到SVC 中斷時(shí),會(huì)在SVC 中斷中進(jìn)行上下文切換,此時(shí)由于有一個(gè)優(yōu)先級(jí)最高的線程(即定時(shí)器線程)處于激活態(tài),它的線程控制塊指針被放在了osRtxInfo.thread.run.next 中,當(dāng)前線程與下一線程是不同的(即osRtxInfo.thread.run.curr≠osRtxInfo.thread.run.next),這時(shí)就會(huì)進(jìn)行上下文切換,將定時(shí)器線程切換為當(dāng)前線程,當(dāng)從SVC 中斷返回時(shí)就會(huì)轉(zhuǎn)到定時(shí)器線程中執(zhí)行。在定時(shí)器線程osRtxInfo.timer.thread 啟動(dòng)后,先創(chuàng)建一個(gè)消息隊(duì)列,再?gòu)南㈥?duì)列取消息,由于此時(shí)消息隊(duì)列是空的,定時(shí)器線程被阻塞。之后mbedOS會(huì)進(jìn)行線程調(diào)度,從就緒隊(duì)列中選擇優(yōu)先級(jí)最高的線程(此時(shí)為主線程main_thread),將其狀態(tài)設(shè)置為激活態(tài),準(zhǔn)備運(yùn)行。至此,CPU的控制權(quán)轉(zhuǎn)交給主線程,接著將由主線程執(zhí)行函數(shù)app_init(定義在08_mbedOsPrgapp_init.cpp 文件中)負(fù)責(zé)創(chuàng)建用戶線程。圖5 展示了從定時(shí)器線程切換到主線程運(yùn)行這一過程中的函數(shù)調(diào)用關(guān)系,從中可以看出最終轉(zhuǎn)到app_init函數(shù)執(zhí)行。
在mbedOS 啟動(dòng)過程中,涉及到中斷向量表、程序代碼、常量、變量、堆、棧等空間分配問題,下面先給出KL36 微控制器結(jié)構(gòu),然后分析mbedOS 啟動(dòng)過程中Flash和RAM空間的使用情況。
圖5 從定時(shí)器線程切換到主線程運(yùn)行的函數(shù)調(diào)用關(guān)系
KL36 微控制器包括ARM Cortex-M0+內(nèi)核、存儲(chǔ)器模塊、外設(shè)模塊及相關(guān)總線等,存儲(chǔ)器模塊與32位的高性能系統(tǒng)總線相連,外設(shè)模塊與32位外設(shè)總線相連,還提供擴(kuò)展總線連接其他外圍設(shè)備,其結(jié)構(gòu)如圖6所示。
圖6 KL36微控制器結(jié)構(gòu)
KL36 片內(nèi) Flash 大 小 為 64 KB,地址范圍為0x00000000~0x0000FFFF,一般用來存放中斷向量、程序代碼、常數(shù)等。mbedOS啟動(dòng)后Flash中各個(gè)區(qū)的地址范圍、大小及作用如表1 所示(表中數(shù)據(jù)采用十六進(jìn)制表示,下同)。
(1)mbedOS啟動(dòng)后RAM使用情況分析
KL36片內(nèi)RAM為靜態(tài)隨機(jī)存儲(chǔ)器SRAM,大小為8 KB,地址范圍為0x1FFFF800~0x200017FF,一般用來存儲(chǔ)全局變量、靜態(tài)變量、臨時(shí)變量(堆??臻g)等。該芯片??臻g的使用方向是從大地址向小地址方向進(jìn)行的。因此,棧空間的棧頂設(shè)置在RAM 地址的最大值+1處。而堆空間的使用方向是從小地址向大地址方向進(jìn)行的,這樣可以減少重疊錯(cuò)誤。mbedOS啟動(dòng)后RAM中各個(gè)段的地址范圍、大小及作用如表2所示。
(2)各線程RAM分配情況分析
在mbedOS 的啟動(dòng)過程中,先后建立了主線程main_thread、空閑線程osRtxInfo.thread.idle、定時(shí)器線程osRtxInfo.timer.thread,并啟動(dòng)定時(shí)器SysTick 中斷。在切換到主線程函數(shù)app_init 執(zhí)行之前,這三個(gè)線程的RAM 分配情況如表3 所示,在鏈表中的關(guān)系如圖7 所示。表中的成員名來源于線程控制塊結(jié)構(gòu)體和線程屬性結(jié)構(gòu)體,sp的值等于stack_mem+stack_size-64(這個(gè)64 Byte 的固定區(qū)域是用于在線程進(jìn)行上下文切換時(shí),保存線程的上下文,即R0~R12、R14、R15、xPSR等16個(gè)寄存器),0x1FFFF8E0地址表示就緒隊(duì)列頭指針。
當(dāng)定時(shí)器線程啟動(dòng)之后就被阻塞,轉(zhuǎn)由主線程控制CPU 的使用權(quán),在主線程函數(shù)app_init 中分別建立紅燈線程thd_redlight、藍(lán)燈線程thd_bluelight 和綠燈線程thd_greenlight三個(gè)用戶線程,當(dāng)這三個(gè)用戶線程啟動(dòng)完后,主線程進(jìn)入阻塞狀態(tài)。此時(shí),系統(tǒng)中有四個(gè)線程,分別是空閑線程、紅燈線程、藍(lán)燈線程和綠燈線程,這四個(gè)線程的RAM分配如表4所示,在鏈表中的關(guān)系如圖8所示。
表1 Flash中的各區(qū)地址范圍、大小及作用
表2 RAM中的各段地址范圍、大小及作用
表3 執(zhí)行app_init之前系統(tǒng)線程的RAM分配情況表
圖7 系統(tǒng)線程之間的關(guān)系
表4 執(zhí)行app_init之后線程的RAM分配情況表
圖8 用戶線程之間的關(guān)系
mbedOS 的啟動(dòng)過程是一個(gè)極其復(fù)雜的過程,涉及到操作系統(tǒng)運(yùn)行時(shí)所需的??臻g、堆空間、線程控制塊等資源的初始化,系統(tǒng)時(shí)鐘的設(shè)置,就緒隊(duì)列、延時(shí)隊(duì)列和等待隊(duì)列等的管理,以及對(duì)線程的調(diào)度,涉及到函數(shù)調(diào)用關(guān)系也極為復(fù)雜。本文通過對(duì)SD-mbedOS工程框架啟動(dòng)流程的分析,簡(jiǎn)要地給出了芯片上電啟動(dòng)過程,通過源碼、宏定義函數(shù)、SVC中斷、流程圖等方式來著重剖析mbedOS 的啟動(dòng)過程,最后分析了mbedOS 的存儲(chǔ)器使用情況。通過剖析,有助于讀者快速理解mbedOS的啟動(dòng)過程、調(diào)度機(jī)制和整體架構(gòu),為簡(jiǎn)化啟動(dòng)流程、優(yōu)化執(zhí)行過程、提升啟動(dòng)速度等進(jìn)一步研究工作提供研究基礎(chǔ),也為mbedOS在不同微控制器上的移植提供了技術(shù)基礎(chǔ)。本文涉及到的工程可到蘇州大學(xué)嵌入式學(xué)習(xí)社區(qū)網(wǎng)站(http://sumcu.suda.edu.cn)的“教學(xué)培訓(xùn)-教學(xué)資料-mbedOS”位置,下載“SD_mbedOS_Start(KL36)”查看。