本篇內容比較簡單,但卻很繁瑣,篇幅也很長,畢竟是囊括了整個操做系統的生命週期。這篇文章的目的是做爲後續設計多任務開發的鋪墊,後續會單獨再抽出一篇分析任務的相關知識。另外本篇文章以單核MCU爲背景,而且以最新的3.1.xLTS版本源碼進行分析。主要內容目錄以下:php
基於bsp/stm32/stm32f103-mini-system爲背景c++
Cortex-M3的堆棧基礎概念算法
C語言main函數和rt-thread的mainshell
rt-thread操做系統的傳統初始化與自動初始化組件數組
任務是怎樣運行起來的安全
Idle任務與新的構想ruby
關於體系結構的知識這裏不作過多的介紹,由於這些知識要講清楚的話足以寫出一本大部頭的書出來。不過會簡單介紹一些必要的東西。微信
Stm32f103單片機是cortex-m3內核,在cortex-m3內核中使用雙堆棧psp和msp,模式分爲線程模式和handler模式,權限級別分爲非特權級別和特權級別(如今只須要知道這麼多就好了),handler模式就是當處理髮生中斷的時候自動進入的模式,其handler模式永遠爲特權級。數據結構
上電開機最開始運行的是MCU內部的ROM部分,這部分代碼咱們一般看不到,其一般是對芯片進行必要的初始化,好比FLASH和RAM的時鐘初始化等,而後跳轉到用戶flash區域運行用戶代碼。在STM32中用戶flash地址從0x08000000開始。咱們寫的代碼都是從這裏開始運行的。其次因爲cortexM規定其用戶FLASH區域的最前面必須是一張中斷向量表。因此也就是說STM32的0x08000000開始是一張中斷向量表,這是必須的也是默認的,固然在以後還能夠重映射其它地方的向量表。這張向量表中的第一項是一個棧地址,第二項復位向量地址。下面貼一段向量表部分代碼(摘錄自startup_stm32f103xb.s):app
__Vectors DCD __initial_sp ; Topof Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMIHandler DCD HardFault_Handler ; Hard Fault Handler DCD MemManage_Handler ; MPU Fault Handler DCD BusFault_Handler ; Bus Fault Handler DCD UsageFault_Handler ; Usage Fault Handler DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD SVC_Handler ; SVCall Handler DCD DebugMon_Handler ; Debug Monitor Handler DCD 0 ; Reserved DCD PendSV_Handler ; PendSV Handler DCD SysTick_Handler ; SysTick Handler
另外須要注意的是開機後會自動進入復位異常,一般咱們叫上電覆位過程,不過意外的是上電覆位處理的模式是特權級線程模式。在特權模式下堆棧指針將使用MSP,非特權模式下能夠被切換到PSP。RT-Thread操做系統就是這麼作的。因此回過頭來看,中斷向量表第一項指定了MSP的棧起始地址,並被自動加載到MSP,第二項指定了復位向量地址,也被自動加載到PC並運行。這樣一來開機後咱們能經過debug看到PC指針最早指向復位向量的第一條指令上。咱們看一下stm32f103在armcc編譯器上的復位向量代碼:
; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT __main IMPORT SystemInit LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP
在Cortex-M3的處理器內核上堆棧指針分爲PSP和MSP。handler模式下老是使用MSP,線程模式能夠經過CONTROL寄存器來配置(修改的時候必須處於特權模式才能夠)。
之因此須要這樣設計就是爲了將普通軟件和系統軟件經過權限隔離開,避免普通用戶權限操做系統關鍵資源帶來安全風險。當咱們使用帶有操做系統的環境進行開發時,操做系統就會將關鍵操做例如任務切換、中斷處理等在特權模式操做。而其它的操做都會運行在非特權模式下完成。
操做系統通常都會將必要的操做封裝出API接口,以提供給普通軟件調用。而這背後的設計思想就是經過觸發異常,而後進入特權模式運行異常向量處理程序。而這段異常處理程序早就讓操做系統實現了,進而這部分特權操做是操做系統接管處理的。這也就避免用戶普通軟件去進行沒必要要的特權操做。例如用戶任務想主動放棄CPU從而調用yield,yield將進行任務切換,其中過程大概是「選出另外一個任務」->」觸發SVC或者Pendsv異常」->進入SVC/Pendsv的handler異常處理程序,此時是特權模式,完成操做後返回到新任務運行。在RT-Thread中進入任務切換是經過觸發Pendsv異常。
前面提到過開機啓動最後進入復位向量處運行,最終調用__main就跑到咱們外面寫的C語言的main函數了。但這並不是這麼簡單,在從__main到咱們的main中間還有一系列操做好比初始化堆棧、初始化全局變量區域、初始化C運行時庫等,而後再在最後調用用戶的main函數。
不過在不一樣的編譯器上這個__main並不是是固定的,這裏也就armcc是如此,若是是GCC和IAR的話其就不太同樣,不過不影響咱們分析核心主題。這裏僅以借用armcc爲例來分析主題中心思想。另外在說明RT-Thread中開啓RT_USING_USER_MAIN的時候在ARMCC編譯器上還有一個支持掛鉤的操做,這種操做通常見於補丁修復的時候。其實現方式是在原有函數的名字前加上$Sub$$
前綴就能夠將原有函數劫持下來,並經過加上$Super$$
前綴再調用原始函數。具體以下:The followingexample shows how to use $Super$$and $Sub$$ to insert a callto the function ExtraFunc() before the call to the legacy functionfoo().
extern void ExtraFunc(void); extern void $Super$$foo(void); /* this functionis called instead of the original foo() */ void $Sub$$foo(void) { ExtraFunc(); /* does some extra setup work */ $Super$$foo(); /* calls the original foo() function */ /* To avoid calling the original foo() function * omit the $Super$$foo(); function call. */ }
上例中本來有一個原始函數叫作foo,可是如今經過$Sub$$foo來劫持全部調用foo的地方,自動會調用$Sub$$foo,而後新的$Sub$$foo裏面先調用本身的擴展實現ExtraFunc後,再接着調用原始版本的foo函數,不過調用原始的foo是加了前綴$Super$$的$Super$$foo.
當使用RT-Thread操做系統開啓RT_USING_USER_MAIN後就是利用這種騷操做來完成RT-Thread操做系統的初始化過程的。(代碼摘錄自components.c)
extern int $Super$$main(void); /* re-definemain function */ int $Sub$$main(void) { rtthread_startup(); return 0; }
關於rtthread_startup函數稍後再講解,不過先接着看下面這個函數:
/* the systemmain thread */ void main_thread_entry(void*parameter) { extern int main(void); extern int $Super$$main(void); /* RT-Thread components initialization*/ rt_components_init(); /* invoke system main function */ #if defined(__CC_ARM) || defined(__CLANG_ARM) $Super$$main(); /* for ARMCC. */ #elif defined(__ICCARM__) || defined(__GNUC__) main(); #endif }
上面這個函數實際上是個小任務,就是完成組件初始化後再跳轉到用戶main函數的。這個小任務在rtthread_startup中調用rt_application_init時建立的,因此此時rt-thread系統早就以經跑起來了。也就是說當調用rtthread_startup後正常狀況就再也不會返回到原來的調用地方,接下來會交給系統的調度器去接管,切換運行任務去了。看下面的代碼瞭解rt_application_init:
void rt_application_init(void) { rt_thread_t tid; #ifdef RT_USING_HEAP tid = rt_thread_create("main", main_thread_entry, RT_NULL, RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20); RT_ASSERT(tid != RT_NULL); #else rt_err_t result; tid = &main_thread; result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL, main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, 20); RT_ASSERT(result == RT_EOK); /* if not define RT_USING_HEAP, using toeliminate the warning */ (void)result; #endif rt_thread_startup(tid); }
至此,關於各類main的子子孫孫以經差很少了解清楚了,其流程大概以下:
ResetHandle->__main->$Sub$$main->(rtthread_startup->rt_application_init->main_thread_entry)->$Super$$main。
其中$Super$$main就是咱們的用戶main函數。若是沒有啓用RT_USING_USER_MAIN那就簡單了,其流程以下:
ResetHandle->__main->main
接下來再接着分析$Sub$$main中調用的rtthread_startup函數。
這裏着重討論rtthread_startup函數,由於這就是RT-Thread操做系統的入口和初始化流程。不過既然說到rtthread_startup函數了,就不得不一塊兒介紹一下RT-Thread操做系統的自動初始化組件了。
rtthread_startup函數是一個函數調用鏈,依次調用各個階段的初始化函數,並在最後啓動調度器再也不返回。代碼摘錄自components.c
int rtthread_startup(void) { rt_hw_interrupt_disable(); /* board level initialization * NOTE: please initialize heap insideboard initialization. */ rt_hw_board_init(); /* show RT-Thread version */ rt_show_version(); /* timer system initialization */ rt_system_timer_init(); /* scheduler system initialization */ rt_system_scheduler_init(); #ifdef RT_USING_SIGNALS /* signal system initialization */ rt_system_signal_init(); #endif /* create init_thread */ rt_application_init(); /* timer thread initialization */ rt_system_timer_thread_init(); /* idle thread initialization */ rt_thread_idle_init(); /* start scheduler */ rt_system_scheduler_start(); /* never reach here */ return 0; }
以上代碼咱們主要脈絡是這樣的:先關閉全局中斷->初始化硬件板上的資源->打印RT-Thread的LOGO->系統定時器功能初始化->調度器初始化->signal功能初始化->應用程序初始化(這個一般是用來建立用戶任務的)->系統軟timer任務初始化->系統idle任務初始化->啓動調度器,永遠再也不返回。
這裏咱們先來講一下爲何要先關閉全局中斷,由於在初始化過程當中,有可能MCU就有其它的中斷和異常觸發了,這個時候系統尚未初始化完成,這就勢必致使系統出現故障,因此先關閉全局中斷,並在啓動調度器後再打開。
rt_hw_board_init很是關鍵,在這個函數裏面必須完成一些必須的初始化過程:堆內存系統的初始化和硬件資源模塊以及若是開啓了自動初始化組件時還須要調用rt_components_board_init完成必要的初始化,這個函數是自動初始化組件的一個接口。(代碼摘錄自bsp\stm32\libraries\HAL_Drivers\drv_common.c)
RT_WEAK void rt_hw_board_init() { #ifdef SCB_EnableICache /* EnableI-Cache---------------------------------------------------------*/ SCB_EnableICache(); #endif #ifdef SCB_EnableDCache /* Enable D-Cache---------------------------------------------------------*/ SCB_EnableDCache(); #endif /* HAL_Init() function is called at thebeginning of the program */ HAL_Init(); /* System clock initialization */ SystemClock_Config(); rt_hw_systick_init(); /* Heap initialization */ #if defined(RT_USING_HEAP) rt_system_heap_init((void*)HEAP_BEGIN, (void*)HEAP_END); #endif /* Pin driver initialization is open bydefault */ #ifdef RT_USING_PIN rt_hw_pin_init(); #endif /* USART driver initialization is openby default */ #ifdef RT_USING_SERIAL rt_hw_usart_init(); #endif /* Set the shell console output device*/ #ifdef RT_USING_CONSOLE rt_console_set_device(RT_CONSOLE_DEVICE_NAME); #endif /* Board underlying hardwareinitialization */ #ifdef RT_USING_COMPONENTS_INIT rt_components_board_init(); #endif }
而後回到rtthread_startup函數中再看rt_application_init函數,因爲咱們是用的stm32的BSP,這個bsp系列是使用自動初始化組件和RT_USING_USER_MAIN功能的,因此過程稍微隱蔽一些,先是在rt_application_init中建立了一個小任務,而後再在小任務中調用了rt_components_init,這也是自動初始化組件的接口。若是沒有開啓自動初始化組件的話,一般咱們的用戶任務能夠在rt_application_init中建立了。也能夠像這裏的實現同樣,先建立一個小任務,而後再在小任務裏完成一些初始化和建立用戶任務。
而後再回到rthtread_startup中看到有初始化軟timer和idle任務的,其中軟件timer功能是能夠經過裁剪配置選擇的,若是打開後就能夠在後續建立softtimer。不然全部的timer都會在OS TICK的中斷上下文中計時。另外這個idle任務也是系統中必不可少和優先級最低的任務。即便咱們啓動調度器後沒有建立任何用戶任務,系統中也有一個idle任務在運行。Idle任務的優先級最低,在此我建議開發人員最好不要將本身的用戶任務優先級配置成最低以避免和idle競爭時間片,這會給你從此的開發帶來沒必要要的麻煩。關於這個問題,我最後會提出一些新的設計構想。不過這裏先要介紹一下idle任務的功能。Idle任務會在系統空閒時被調度運行,因此咱們一般在idle任務裏作低功耗設計。其次idle任務裏還會完成系統資源的回收。例如被刪除的任務,被刪除的module等。
最後rthtread_startup啓動調度器rt_system_scheduler_start開始調度系統的任務,今後就開始運行任務,再也不返回。這裏又要記住一個概念,在上文提到的PSP和MSP,到目前爲止MCU仍是使用一開始中斷向量表中指定的MSP棧。可是當調度任務後,任務會有本身的棧,且rt-thread系統會將任務的棧切換到PSP棧指針。值得注意的是,這個MSP是全局共享的,全部的中斷程序都會使用這個棧空間,因此咱們須要根據本身的狀況來配置這個MSP棧的空間大小。
接下來咱們再來介紹自動初始化組件。RT-Thread中的自動初始化組件思路來自於Linux內核。其實現手段是將須要初始化的函數接口經過連接器指令放在特殊的section中。這個section的概念是當咱們程序最終連接成一個image後會造成一個標準格式的文件,其中armcc中叫作ARM ELF。詳細的介紹能夠查閱官方資料。其中ELF文件就有將代碼分紅稱爲section的區域,能夠稱做段。而且能夠指定本身的代碼放在指定名稱的段中,且能夠指定這個section段的ROM地址。這樣當咱們設計玩初始化接口後,經過連接器的指令以及連接腳本文件將咱們的初始化代碼放在特定的地方,而且利用命名規則來作到順序排序。等須要調用初始化的時候能夠利用這些section的地址轉換成函數指針直接批量循環調用。一般你會在MDK的工程文件連接器參數中看到這樣的指令:--keep *.o(.rti_fn.*),這是爲了在連接階段保證這些自定義段不被刪除。同時也能夠看出rti_fn就是自動初始化組件的section名字。相似的將函數放置在這些段中的連接器指令以下:(摘錄自rtdef.h)
/*initialization export */ #ifdef RT_USING_COMPONENTS_INIT typedef int (*init_fn_t)(void); #ifdef _MSC_VER/* we do notsupport MS VC++ compiler */ #define INIT_EXPORT(fn,level) #else #if RT_DEBUG_INIT struct rt_init_desc { const char* fn_name; const init_fn_t fn; }; #define INIT_EXPORT(fn, level) \ const char __rti_##fn##_name[] =#fn; \ RT_USED const struct rt_init_desc __rt_init_desc_##fn SECTION(".rti_fn."level)=\ { __rti_##fn##_name, fn}; #else #define INIT_EXPORT(fn, level) \ RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn."level)= fn #endif #endif #else #define INIT_EXPORT(fn, level) #endif /* board initroutines will be called in board_init() function */ #define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn,"1") /*pre/device/component/env/app init routines will be called in init_thread */ /* componentspre-initialization (pure software initilization) */ #define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn,"2") /* deviceinitialization */ #define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn,"3") /* componentsinitialization (dfs, lwip, ...) */ #define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn,"4") /* environmentinitialization (mount disk, ...) */ #define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn,"5") /* appliationinitialization (rtgui application etc ...) */ #define INIT_APP_EXPORT(fn) INIT_EXPORT(fn,"6")
其中不一樣的數字表明不一樣的初始化順序,能夠根據須要來選擇。接着如上文提到的兩個函數rt_components_board_init和rt_components_init是如何實現的:摘錄自components.c
#ifdef RT_USING_COMPONENTS_INIT /* * Components Initialization will initializesome driver and components as following * order: * rti_start --> 0 * BOARD_EXPORT --> 1 * rti_board_end --> 1.end * * DEVICE_EXPORT --> 2 * COMPONENT_EXPORT --> 3 * FS_EXPORT --> 4 * ENV_EXPORT --> 5 * APP_EXPORT --> 6 * * rti_end --> 6.end * * These automatically initialization, thedriver or component initial function must * be defined with: * INIT_BOARD_EXPORT(fn); * INIT_DEVICE_EXPORT(fn); * ... * INIT_APP_EXPORT(fn); * etc. */ static int rti_start(void) { return 0; } INIT_EXPORT(rti_start,"0"); static int rti_board_start(void) { return 0; } INIT_EXPORT(rti_board_start,"0.end"); static int rti_board_end(void) { return 0; } INIT_EXPORT(rti_board_end,"1.end"); static int rti_end(void) { return 0; } INIT_EXPORT(rti_end,"6.end"); /** * RT-Thread Components Initialization forboard */ void rt_components_board_init(void) { #if RT_DEBUG_INIT int result; const struct rt_init_desc *desc; for (desc = &__rt_init_desc_rti_board_start; desc < &__rt_init_desc_rti_board_end; desc ++) { rt_kprintf("initialize %s", desc->fn_name); result = desc->fn(); rt_kprintf(":%d done\n", result); } #else const init_fn_t *fn_ptr; for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++) { (*fn_ptr)(); } #endif } /** * RT-Thread Components Initialization */ void rt_components_init(void) { #if RT_DEBUG_INIT int result; const struct rt_init_desc *desc; rt_kprintf("do components initialization.\n"); for (desc = &__rt_init_desc_rti_board_end; desc < &__rt_init_desc_rti_end; desc++) { rt_kprintf("initialize %s", desc->fn_name); result = desc->fn(); rt_kprintf(":%d done\n", result); } #else const init_fn_t *fn_ptr; for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr++) { (*fn_ptr)(); } #endif }
之因此要分開這兩個函數就是由於board階段的初始化比其它普通的組件初始化早,board階段的初始化一般沒什麼系統資源依賴。而其它狀況下則一般在操做系統已經完成必要的初始化後才能作的初始化纔會放在rt_components_init裏。
要說明任務是怎麼運行起來的,就得知道任務是怎麼建立的,其次結合以前寫的文章<源碼解讀·RT-Thread多任務調度算法>就差很少了。那麼這裏就介紹一下任務的建立。照樣用上面的rt_application_init裏建立任務的代碼來舉例:
void rt_application_init(void) { rt_thread_t tid; #ifdef RT_USING_HEAP tid = rt_thread_create("main", main_thread_entry, RT_NULL, RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20); RT_ASSERT(tid != RT_NULL); #else rt_err_t result; tid = &main_thread; result =rt_thread_init(tid, "main", main_thread_entry, RT_NULL, main_stack, sizeof(main_stack),RT_MAIN_THREAD_PRIORITY, 20); RT_ASSERT(result == RT_EOK); /* if not define RT_USING_HEAP, using toeliminate the warning */ (void)result; #endif rt_thread_startup(tid); }
首先要說明的是RT-Thread任務建立有兩種,一種是動態的,一種是靜態的。所謂的動態就是其任務棧自動在堆內存中分配;靜態是用戶本身指定棧空間,固然一般這個棧來自於用戶定義的數組。如上例中當RT_USING_HEAP宏被打開,也就是有堆內存的時候會採用rt_thread_create接口來建立動態資源的任務。固然能夠利用rt_thread_init來建立一個靜態資源的任務。先來了解一下這兩個函數在建立任務時的一些參數:」main」這是任務的名稱,任務名稱用一個字符串來指定,不是很重要,不過最好能起到必定的說明性,有利於從此調試用。main_thread_entry這是任務的入口函數,所謂的任務就是一個C語言中的函數而已。RT_NULL,這是傳給任務入口函數的參數,若是沒有就爲NULL.由於RT_Thread中的任務原型爲:void (*entry)(void*parameter);RT_MAIN_THREAD_STACK_SIZE爲任務的棧大小,以字節爲單位。RT_MAIN_THREAD_PRIORITY爲任務的優先級號。20爲任務的時間片大小。其中靜態任務中還有tid表明任務的TCB數據結構句柄。main_stack爲棧空間起始地址。當用動態建立的方法建立成功後會返回一個任務的TCB任務句柄出來。以後咱們利用rt_thread_startup(任務句柄)的形式啓動任務便可。例如上例中rt_thread_startup(tid);不過rt_thread_startup函數真正的功能是將任務放置於調度隊列中,並置任務狀態爲ready,由此交給調度器去調度,能不能立馬運行取決與調度器的調度。通常狀況下,要想任務得到運行必須知足的條件:調度器已經運行,任務已經ready,沒有更高優先級任務,沒有中斷髮生。只要條件知足調度器就會調度此任務,作好必要的棧初始化和狀態置位,就會切換到任務開始運行。只要任務得到運行就會使用建立任務時指定的棧空間。
不過通常的任務一般是一直運行,持續的服務。形式以下:
void task(void *parameter) { while (1) { // do_work(); } }
上面解釋過idle任務在rt-thread操做系統中的功能:釋放資源、低功耗設計。
關於資源釋放一般是任務的析構過程,這就是任務的結束。例如上例中的main_thread_entry任務之因此稱爲小任務的緣由就是它作完事情就結束了。那麼可能就會想,既然任務都結束了那麼它的資源如何釋放呢?好比棧空間,TCB等。這就是idle該乾的事情。即便全部的用戶任務都結束,最後也會剩下idle任務在運行。若是有必要的話,能夠在idle任務中能夠經過調用低功耗組件進入低功耗或者乾脆調用電源開關控制來關機。
其次idle任務佔用了最低優先級。雖然用戶任務也可使用和idle任務相同的優先級,可是並不建議這樣作,好比在低功耗設計時就會出問題。另外我我的在思考一個問題,idel任務既然以經在設計之初就明確了其得到運行的條件,那麼何不作成無需優先級的任務,惟一的調度決策就是:當調度器沒有任務處於ready狀態時就切換到idel任務運行。這就無需關注最低優先級被idle霸佔的問題了。
感謝各位網友的支持,能夠關注個人微信公衆號:鵬城碼夫 (微信號:rocotona)