1.開啟任務調度器
vTaskStartScheduler()
作用:用于啟動任務調度器,任務調度器啟動后, FreeRTOS 便會開始進行任務調度【動態創建任務為例】
- 創建空閑任務
- 如果使能軟件定時器,則創建定時器任務
- 關閉中斷,防止調度器開啟之前或過程中,受中斷干擾,會在運行第一個任務時打開中斷
- 初始化全局變量,并將任務調度器的運行標志設置為已運行
- 初始化任務運行時間統計功能的時基定時器 【可選】
- 調用函數 xPortStartScheduler()
xPortStartScheduler()
作用:該函數用于完成啟動任務調度器中與硬件架構相關的配置部分,以及啟動第一個任務
- 檢測用戶在 FreeRTOSConfig.h 文件中對中斷的相關配置是否有誤
- 配置 PendSV 和 SysTick 的中斷優先級為最低優先級
- 調用函數 vPortSetupTimerInterrupt()配置 SysTick(清空計數值、配置節拍頻率、重裝載值、啟動計數與中斷)
- 初始化臨界區嵌套計數器為 0
- 調用函數 prvEnableVFP()使能 FPU
- 將FPCCR寄存器的[31:30]置l,這樣在進出異常時,FPU的相關寄存器就會自動地保存和恢復(M4/M7)
- 調用函數prvStartFirstTask() 啟動第一個任務
2.啟動第一個任務
prvStartFirstTask()
__asm void prvStartFirstTask( void ) { /* 8字節對齊 */ PRESERVE8 ldr r0, =0xE000ED08 /* 0xE000ED08為VTOR地址 */ ldr r0, [ r0 ] /* 獲取VTOR的值 */ ldr r0, [ r0 ] /* 獲取MSP的初始值 */ /* 初始化MSP */ msr msp, r0/* 使能全局中斷 */ cpsie i cpsie f dsb isb /* 調用SVC啟動第一個任務 */ svc 0 nop nop
}
執行過程為:
- 獲取MSP的初始值(棧頂地址)
- 將MSP重新賦值為棧底指針(讓MSP回到原點,啟動任務一去不復返)
- 使能全局中斷
- 使用SVC指令,傳入系統調用信號,出發SVC中斷vPortSVCHandler ()
- 關于MSP指針
程序在運行過程中需要一定的棧空間來保存局部變量等一些信息。當有信息保存到棧中時,MCU 會自動更新 SP 指針,ARM Cortex-M 內核提供了兩個棧空間
主堆棧指針(MSP)它由 OS 內核、異常服務例程以及所有需要特權訪問的應用程序代碼來使用。
進程堆棧指針(PSP)用于常規的應用程序代碼(不處于異常服務例程中時)。
在裸機中,程序全部使用MSP,在FreeRTOS中,中斷使用MSP(主堆棧),中斷以外使用PSP(進程堆棧)- 關于0xE000ED08
0xE000ED08是VTOR(中斷向量表)的地址,向量表的第一個是 MSP 指針,取 MSP 的初始值的思路是先根據向量表的位置寄存器 VTOR (0xE000ED08) 來獲取向量表存儲的地址;在根據向量表存儲的地址,來訪問第一個元素,也就是初始的 MSP。
vPortSVCHandler ()
__asm void vPortSVCHandler( void )
{ /* 8字節對齊 */ PRESERVE8 /* 獲取任務棧地址 */ ldr r3, = pxCurrentTCB /* r3指向優先級最高的就緒態任務的任務控制塊 */ ldr r1, [ r3 ] /* r1為任務控制塊地址 */ ldr r0, [ r1 ] /* r0為任務控制塊的第一個元素(棧頂) */ /* 模擬出棧,并設置PSP */ ldmia r0 !, { r4 - r11 } /* 任務棧彈出到CPU寄存器 */ msr psp, r0 /* 設置PSP為任務棧指針 */ isb /* 使能所有中斷 */ mov r0, # 0 msr basepri, /* 使用PSP指針,并跳轉到任務函數 */ orr r14, # 0xd bx r14 }
運行過程為:
- 獲取優先級最高的就緒任務的TCB,并取其棧頂元素pxTopOfStack
- 模擬出棧,將寄存器值出棧至CPU寄存器,并設置PSP指針
- 開啟中斷
- 線與0xd,將r14設置為線程模式并使用PSP
- 跳轉到任務的任務函數中運行,CPU自動出棧R0-xPSR等寄存器(M4:若EXC_RETURN使用FPU,則恢復浮點單元)
M4的vPortSVCHandler () ,除了手動出棧r4-r11外,還有r14,這是因為M4等系列支持FPU,需要該變量進行判別
M4的vPortSVCHandler () ,不需要線與0xd,因為在初始化時,已經對EXC_RETURN進行賦值了,不需要再線與
一般情況下,使用動態創建任務,第一個啟動的任務是軟件定時器任務
注意:SVC中斷只在啟動第一次任務時會調用一次,以后均不調用
開啟任務調度器及啟動第一個任務總結
3.任務切換
任務切換的本質:就是CPU寄存器的切換(又稱上下文切換),在PendSV中斷服務函數中完成 主要分為兩步:
- 需暫停任務A的執行,并將此時任務A的寄存器保存到任務堆棧,這個過程叫做保存現場
- 將任務B的各個寄存器值(被存于任務堆棧中)恢復到CPU寄存器中,這個過程叫做恢復現場
觸發PendSV中斷方式
- 滴答定時器中斷調用
- 執行FreeRTOS提供的相關API函數:portYIELD()
- 本質:通過向中斷控制和狀態寄存器 ICSR 的bit28 寫入 1 掛起 PendSV 來啟動 PendSV 中斷
PendSV中斷服務函數xPortPendSVHandler()
- 進入中斷,使用PSP自動壓棧
- 當前的psp是正在運行的任務的棧指針,讀取當前PSP進程指針,存入r0(M4還要考慮FPU壓棧)
- 手動壓棧,并將最終結果封存至pxTopOfStack,方便下次恢復
- 屏蔽中斷
- 調用vTaskSeitchContext(),獲取當前最高優先級任務的任務控制塊
- 使能中斷
- 從最高優先級的TCB中獲取pxTopOfStack,并手動出棧
- 更新切換后的任務的的棧指針給PSP
- PSP負責自動出棧
- bx r14 執行新任務函數
查找最優先級任務vTaskSwitchContext( )
通過這個函數完成:taskSELECT_HIGHEST_PRIORITY_TASK( )
- 使用硬件方式(本文使用)
- 使用軟件方式
#define taskSELECT_HIGHEST_PRIORITY_TASK()
{UBaseType_t uxTopPriority;portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0);listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );
}
前導置零指令
所謂的前導置零指令,大家可以簡單理解為計算一個 32位數,頭部 0 的個數。通過前導置零指令獲得最高優先級
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
獲取最高優先級任務的任務控制塊
通過該函數獲取當前最高優先級任務的任務控制塊
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )
{List_t * const pxConstList = ( pxList );( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ){(pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;}( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
}