@(嵌入式)
FreeRtos
簡(jiǎn)述
前面文章 < FreeRTOS 任務(wù)調(diào)度 任務(wù)創(chuàng)建 > 介紹了 FreeRTOS 中如何創(chuàng)建任務(wù)以及其具體實(shí)現(xiàn)。
一般來(lái)說(shuō), 我們會(huì)在程序開(kāi)始先創(chuàng)建若干個(gè)任務(wù), 而此時(shí)任務(wù)調(diào)度器還沒(méi)又開(kāi)始運(yùn)行,因此每一次任務(wù)創(chuàng)建后都會(huì)依據(jù)其優(yōu)先級(jí)插入到就緒鏈表,同時(shí)保證全局變量 pxCurrentTCB
指向當(dāng)前創(chuàng)建的所有任務(wù)中優(yōu)先級(jí)最高的一個(gè),但是任務(wù)還沒(méi)開(kāi)始運(yùn)行。
當(dāng)初始化完畢后,調(diào)用函數(shù) vTaskStartScheduler
啟動(dòng)任務(wù)調(diào)度器開(kāi)始開(kāi)始調(diào)度,此時(shí),pxCurrentTCB
所指的任務(wù)才開(kāi)始運(yùn)行。
所以, 本章,介紹任務(wù)調(diào)度器啟動(dòng)以及如何進(jìn)行任務(wù)切換。
調(diào)度器涉及平臺(tái)底層硬件操作,本文以Cotex-M3 架構(gòu)為例, 具體可以參考 《Cortex-M3權(quán)威指南》(文末附)
分析的源碼版本是 v9.0.0
(為了方便查看,github 上保留了一份源碼Source目錄下的拷貝)
啟動(dòng)調(diào)度器
創(chuàng)建任務(wù)后,系統(tǒng)不會(huì)自動(dòng)啟動(dòng)任務(wù)調(diào)度器,需要用戶調(diào)用函數(shù) vTaskStartScheduler 啟動(dòng)調(diào)度器。 該函數(shù)被調(diào)用后,會(huì)先創(chuàng)建系統(tǒng)自己需要用到的任務(wù),比如空閑任務(wù) prvIdleTask
,定時(shí)器管理的任務(wù)等。 之后, 調(diào)用移植層提供的函數(shù) xPortStartScheduler
。
代碼解析如下,
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
// 采用靜態(tài)內(nèi)存創(chuàng)建空閑任務(wù)
StaticTask_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
// 獲取靜態(tài)內(nèi)存地址/參數(shù)
vApplicationGetIdleTaskMemory(
&pxIdleTaskTCBBuffer,
&pxIdleTaskStackBuffer,
&ulIdleTaskStackSize );
// 創(chuàng)建任務(wù)
// 空閑任務(wù)優(yōu)先級(jí)為 0, 也就是其優(yōu)先級(jí)最低
// !! 但是, 設(shè)置了特權(quán)位, 所以其運(yùn)行在 特權(quán)模式
xIdleTaskHandle = xTaskCreateStatic(prvIdleTask, "IDLE",
ulIdleTaskStackSize, (void *) NULL,
(tskIDLE_PRIORITY | portPRIVILEGE_BIT),
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer);
if( xIdleTaskHandle != NULL )
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else
{
// 動(dòng)態(tài)申請(qǐng)內(nèi)存創(chuàng)建任務(wù)
xReturn = xTaskCreate(prvIdleTask,
"IDLE", configMINIMAL_STACK_SIZE,
(void *)NULL,
(tskIDLE_PRIORITY | portPRIVILEGE_BIT),
&xIdleTaskHandle );
}
#endif
// 如果工程使用了軟件定時(shí)器, 需要?jiǎng)?chuàng)建定時(shí)器任務(wù)進(jìn)行管理
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
if( xReturn == pdPASS )
{
// 關(guān)閉中斷, 避免調(diào)度器運(yùn)行前節(jié)拍定時(shí)器產(chǎn)生中斷
// 中斷在第一個(gè)任務(wù)啟動(dòng)時(shí)恢復(fù)
portDISABLE_INTERRUPTS();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
// 如果使用了這個(gè)庫(kù)
// 更新第一個(gè)任務(wù)的的指針到全局變量
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif
// 初始化變量
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) 0U;
// 如果啟動(dòng)統(tǒng)計(jì)任務(wù)運(yùn)行時(shí)間, 宏 configGENERATE_RUN_TIME_STATS = 1
// 需要定義以下宏, 初始化一個(gè)定時(shí)器用于該功能
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
// 設(shè)置系統(tǒng)節(jié)拍計(jì)數(shù)器, 啟動(dòng)任務(wù)
// 硬件相關(guān), 由系統(tǒng)移植層提供, 下面介紹
if( xPortStartScheduler() != pdFALSE )
{
// 不會(huì)運(yùn)行到這里, 如果調(diào)度器運(yùn)行正常
}
else
{
// 當(dāng)調(diào)用 xTaskEndScheduler()才會(huì)來(lái)到這里
}
}
else
{
// 內(nèi)存不足,創(chuàng)建空閑任務(wù)/定時(shí)任務(wù)失敗, 調(diào)度器啟動(dòng)失敗
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
// 預(yù)防編譯器警告
( void ) xIdleTaskHandle;
}
移植層調(diào)度器
上面提到, 創(chuàng)建系統(tǒng)所需任務(wù)和初始化相關(guān)靜態(tài)變量后, 系統(tǒng)調(diào)用了 xPortStartScheduler
設(shè)置節(jié)拍定時(shí)器和啟動(dòng)第一個(gè)任務(wù),開(kāi)始系統(tǒng)正常運(yùn)行調(diào)度。 而對(duì)于不同架構(gòu)平臺(tái),該函數(shù)的實(shí)現(xiàn)可能存在不同,以下, 拿比較常用的 Cotex-M3 架構(gòu)舉例。
對(duì)于 M3, 可以在源碼目錄下 /Source/portable/GCC/ARM_CM3/port.c 看到該函數(shù)的實(shí)現(xiàn)。
與 FreeRTOS 任務(wù)優(yōu)先級(jí)相反, Cotex-M3 優(yōu)先級(jí)值越小, 優(yōu)先級(jí)越高。 Cotex-M3的優(yōu)先級(jí)配置寄存器考慮器件移植而向高位對(duì)齊,實(shí)際可用的 CPU 會(huì)裁掉表達(dá)優(yōu)先級(jí)低端的有效位,以減少優(yōu)先級(jí)數(shù)。 舉例子說(shuō), 加入平臺(tái)支持3bit 表示優(yōu)先級(jí),則其優(yōu)先級(jí)配置寄存器的高三位可以編程寫入,其他位被屏蔽,不管寫入何值,重新讀回都是0。
另外提供搶占優(yōu)先級(jí)和子優(yōu)先級(jí)分段配置相關(guān),詳細(xì)閱讀 《Cortex-M3權(quán)威指南》
在系統(tǒng)調(diào)度過(guò)程中,主要涉及到的三個(gè)異常:
- SVC 系統(tǒng)服務(wù)調(diào)用
操作系統(tǒng)通常不讓用戶程序直接訪問(wèn)硬件,而是通過(guò)提供一些系統(tǒng)服務(wù)函數(shù)。 這里主要觸發(fā)后,在異常服務(wù)中啟動(dòng)第一個(gè)任務(wù) - PendSV 可懸起系統(tǒng)調(diào)用
相比 SVC, PenndSV 異常后可能不會(huì)馬上響應(yīng), 等到其他高優(yōu)先級(jí)中斷處理后才響應(yīng)。 用于上下文切換,同時(shí)保證其他中斷可以被及時(shí)響應(yīng)處理。 - SysTick 節(jié)拍定時(shí)器
在沒(méi)有高優(yōu)先級(jí)任務(wù)強(qiáng)制下,同優(yōu)先級(jí)任務(wù)按時(shí)間片輪流執(zhí)行,每次SysTick中斷,下一個(gè)任務(wù)將獲得一個(gè)時(shí)間片。
BaseType_t xPortStartScheduler( void )
{
configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
#if( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
// 取出中斷優(yōu)先級(jí)寄存器
volatile uint8_t * const pucFirstUserPriorityRegister =
(volatile uint8_t * const) (portNVIC_IP_REGISTERS_OFFSET_16 +
portFIRST_USER_INTERRUPT_NUMBER);
volatile uint8_t ucMaxPriorityValue;
// 保存原有優(yōu)先級(jí)寄存器值
ulOriginalPriority = *pucFirstUserPriorityRegister;
// 判斷平臺(tái)支持優(yōu)先級(jí)位數(shù)
// 先全寫 1
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
// 重新讀回, 不能設(shè)置的位依然是 0
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
// 確保用戶設(shè)置優(yōu)先級(jí)不會(huì)超出范圍
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
// 判斷有幾個(gè)1, 得到對(duì)應(yīng)優(yōu)先級(jí)數(shù)最大值
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
// 恢復(fù)優(yōu)先級(jí)配置寄存器值
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* conifgASSERT_DEFINED */
// 設(shè)置 PendSV 和 SysTIck 異常優(yōu)先級(jí)最低
// 保證系統(tǒng)會(huì)話切換不會(huì)阻塞系統(tǒng)其他中斷的響應(yīng)
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
// 初始化系統(tǒng)節(jié)拍定時(shí)器
vPortSetupTimerInterrupt();
// 初始化邊界嵌套計(jì)數(shù)器
uxCriticalNesting = 0;
// 觸發(fā) svc 異常 啟動(dòng)第一個(gè)任務(wù)
prvPortStartFirstTask();
/* Should not get here! */
prvTaskExitError();
return 0;
}
啟動(dòng)第一個(gè)任務(wù)
函數(shù)中調(diào)用了 prvPortStartFirstTask
來(lái)啟動(dòng)第一個(gè)任務(wù), 該函數(shù)重新初始化了系統(tǒng)的棧指針,表示 FreeRtos 開(kāi)始接手平臺(tái)的控制, 同時(shí)通過(guò)觸發(fā) SVC 系統(tǒng)調(diào)用,運(yùn)行第一個(gè)任務(wù)。具體實(shí)現(xiàn)如下
static void prvPortStartFirstTask( void )
{
__asm volatile(
" ldr r0, =0xE000ED08 \n" /*向量表偏移寄存器地址 CotexM3*/
" ldr r0, [r0] \n" /*取向量表地址*/
" ldr r0, [r0] \n" /*取 MSP 初始值*/
/*重置msp指針 宣示 系統(tǒng)接管*/
" msr msp, r0 \n"
" cpsie i \n" /*開(kāi)中斷*/
" cpsie f \n" /*開(kāi)異常*/
/*流水線相關(guān)*/
" dsb \n" /*數(shù)據(jù)同步隔離*/
" isb \n" /*指令同步隔離*/
/*觸發(fā)異常 啟動(dòng)第一個(gè)任務(wù)*/
" svc 0 \n"
" nop \n"
);
}
前面創(chuàng)建任務(wù)的文章介紹過(guò), 任務(wù)創(chuàng)建后, 對(duì)其棧進(jìn)行了初始化,使其看起來(lái)和任務(wù)運(yùn)行過(guò)后被系統(tǒng)中斷切換了一樣。 所以,為了啟動(dòng)第一個(gè)任務(wù),觸發(fā) SVC 異常后,異常處理函數(shù)中直接執(zhí)行現(xiàn)場(chǎng)恢復(fù), 把 pxCurrentTCB
"恢復(fù)"到運(yùn)行狀態(tài)。
(另外,Cotex-M3 具有三級(jí)流水線,所以切換任務(wù)的時(shí)候需要清除預(yù)取的指令,避免錯(cuò)誤。)
對(duì)于 Cotex-M3 , 其代碼實(shí)現(xiàn)如下,
void vPortSVCHandler( void )
{
__asm volatile (
/*取 pxCurrentTCB 的地址*/
"ldr r3, pxCurrentTCBConst2 \n"
/*取出 pxCurrentTCB 的值 : TCB 地址*/
"ldr r1, [r3] \n"
/*取出 TCB 第一項(xiàng) : 任務(wù)的棧頂 */
"ldr r0, [r1] \n"
/*恢復(fù)寄存器數(shù)據(jù)*/
"ldmia r0!, {r4-r11} \n"
/*設(shè)置線程指針: 任務(wù)的棧指針*/
"msr psp, r0 \n"
/*流水線清洗*/
"isb \n"
"mov r0, #0 \n"
"msr basepri, r0 \n"
/*設(shè)置返回后進(jìn)入線程模式*/
"orr r14, #0xd \n"
"bx r14 \n"
" \n"
".align 4 \n"
"pxCurrentTCBConst2: .word pxCurrentTCB \n"
);
}
異常返回后, 系統(tǒng)進(jìn)入線程模式, 自動(dòng)從堆?;謴?fù)PC等寄存器,而由于此時(shí)棧指針已經(jīng)更新指向?qū)?yīng)準(zhǔn)備運(yùn)行任務(wù)的棧,所以,程序會(huì)從該任務(wù)入口函數(shù)開(kāi)始執(zhí)行。
到此, 第一個(gè)任務(wù)啟動(dòng)。
前面提到, 第一個(gè)任務(wù)啟動(dòng)通過(guò) SVC 異常, 而后續(xù)的任務(wù)切換, 使用的是 PendSV 異常, 而其對(duì)應(yīng)的服務(wù)函數(shù)是 xPortPendSVHandler
。 后續(xù)介紹任務(wù)切換再分析。
任務(wù)切換
FreeRTOS 支持時(shí)間片輪序和優(yōu)先級(jí)搶占。系統(tǒng)調(diào)度器通過(guò)調(diào)度算法確定當(dāng)前需要獲得CPU 使用權(quán)的任務(wù)并讓其處于運(yùn)行狀態(tài)。對(duì)于嵌入式系統(tǒng),某些任務(wù)需要獲得快速的響應(yīng),如果使用時(shí)間片,該任務(wù)可能無(wú)法及時(shí)被運(yùn)行,因此搶占調(diào)度是必須的,高優(yōu)先級(jí)的任務(wù)一旦就緒就能及時(shí)運(yùn)行;而對(duì)于同優(yōu)先級(jí)任務(wù),系統(tǒng)根據(jù)時(shí)間片調(diào)度,給予每個(gè)任務(wù)相同的運(yùn)行時(shí)間片,保證每個(gè)任務(wù)都能獲得CPU 。
- 最高優(yōu)先級(jí)任務(wù) Task 1 運(yùn)行,直到其被阻塞或者掛起釋放CPU
- 就緒鏈表中最高優(yōu)先級(jí)任務(wù)Task 2 開(kāi)始運(yùn)行, 直到...
- 調(diào)用接口進(jìn)入阻塞或者掛起狀態(tài)
- 任務(wù) Task 1 恢復(fù)并搶占 CPU 使用權(quán)
- 同優(yōu)先級(jí)任務(wù)TASK 3 就緒,時(shí)間片調(diào)度
- 沒(méi)有用戶任務(wù)執(zhí)行,運(yùn)行系統(tǒng)空閑任務(wù)。
FreeRTOS 在兩種情況下執(zhí)行任務(wù)切換:
- 同等級(jí)任務(wù)時(shí)間片用完,提前掛起觸發(fā)切換
在 SysTick 節(jié)拍計(jì)數(shù)器中斷中觸發(fā)異常 - 高優(yōu)先任務(wù)恢復(fù)就緒(如信號(hào)量,隊(duì)列等阻塞、掛起狀態(tài)下退出)時(shí)搶占
最終都是通過(guò)調(diào)用移植層提供的portYIELD()
宏懸起 PendSV 異常
但是無(wú)論何種情況下,都是通過(guò)觸發(fā)系統(tǒng) PendSV 異常,在該服務(wù)程序中完成切換。
使用該異常切換上下文的原因是保證切換不會(huì)影響到其他中斷的及時(shí)響應(yīng)(切換上下文搶占了 ISR 的執(zhí)行,延時(shí)時(shí)間不可預(yù)知,對(duì)于實(shí)時(shí)系統(tǒng)是無(wú)法容忍的),在SysTick 中或其他需要進(jìn)行任務(wù)切換的地方懸起一個(gè) PendSV 異常,系統(tǒng)會(huì)直到其他所有 ISR 都完成處理后才執(zhí)行該異常的服務(wù)程序,進(jìn)行上下文切換。
系統(tǒng)響應(yīng) PendSV 異常,在該中斷服務(wù)程序中,保存當(dāng)前任務(wù)現(xiàn)場(chǎng), 選擇切換的下一個(gè)任務(wù),進(jìn)行任務(wù)切換,退出異常恢復(fù)線程模式運(yùn)行新任務(wù),完成任務(wù)切換。
以下是 Cotex-M3 的服務(wù)程序,
首先先要明確的是,系統(tǒng)進(jìn)入異常處理程序的時(shí)候,使用的是主堆棧指針 MSP, 而一般情況下運(yùn)行任務(wù)使用的線程模式使用的是進(jìn)程堆棧指針 PSP。后者使用是系統(tǒng)設(shè)置的,前者是硬件強(qiáng)制設(shè)置的。
對(duì)應(yīng)這兩個(gè)指針,系統(tǒng)有兩種堆棧,系統(tǒng)內(nèi)核和異常程序處理使用的是主堆棧,MSP 指向其棧頂。而對(duì)應(yīng)而不同任務(wù),我們?cè)趧?chuàng)建時(shí)為其分配了空間,作為該任務(wù)的堆棧,在該任務(wù)運(yùn)行時(shí),由系統(tǒng)設(shè)置進(jìn)程堆棧 PSP 指向該棧頂。
如下分析該服務(wù)函數(shù)的執(zhí)行:
void xPortPendSVHandler( void )
{
/* This is a naked function. */
__asm volatile
(
/*取出當(dāng)前任務(wù)的棧頂指針 也就是 psp -> R0*/
" mrs r0, psp \n"
" isb \n"
" \n"
/*取出當(dāng)前任務(wù)控制塊指針 -> R2*/
" ldr r3, pxCurrentTCBConst \n"
" ldr r2, [r3] \n"
" \n"
/*R4-R11 這些系統(tǒng)不會(huì)自動(dòng)入棧,需要手動(dòng)推到當(dāng)前任務(wù)的堆棧*/
" stmdb r0!, {r4-r11} \n"
/*最后,保存當(dāng)前的棧頂指針
R0 保存當(dāng)前任務(wù)棧頂?shù)刂? [R2] 是 TCB 首地址,也就是 pxTopOfStack
下次,任務(wù)激活可以重新取出恢復(fù)棧頂,并取出其他數(shù)據(jù)
*/
" str r0, [r2] \n"
" \n"
/*保護(hù)現(xiàn)場(chǎng),調(diào)用函數(shù)更新下一個(gè)準(zhǔn)備運(yùn)行的新任務(wù)*/
" stmdb sp!, {r3, r14} \n"
/*設(shè)置優(yōu)先級(jí) 第一個(gè)參數(shù),
即:configMAX_SYSCALL_INTERRUPT_PRIORITY
進(jìn)入臨界區(qū)*/
" mov r0, %0 \n"
" msr basepri, r0 \n"
" bl vTaskSwitchContext \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" ldmia sp!, {r3, r14} \n"
" \n"
/*函數(shù)返回 退出臨界區(qū)
pxCurrentTCB 指向新任務(wù)
取出新的 pxCurrentTCB 保存到 R1
*/
" ldr r1, [r3] \n"
/*取出新任務(wù)的棧頂*/
" ldr r0, [r1] \n"
/*恢復(fù)手動(dòng)保存的寄存器*/
" ldmia r0!, {r4-r11} \n"
/*設(shè)置線程指針 psp 指向新任務(wù)棧頂*/
" msr psp, r0 \n"
" isb \n"
/*返回, 硬件執(zhí)行現(xiàn)場(chǎng)恢復(fù)
開(kāi)始執(zhí)行任務(wù)
*/
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst: .word pxCurrentTCB \n"
::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY)
);
}
在服務(wù)程序中,調(diào)用了函數(shù) vTaskSwitchContext
獲取新的運(yùn)行任務(wù), 該函數(shù)會(huì)更新當(dāng)前任務(wù)運(yùn)行時(shí)間,檢查任務(wù)堆棧使用是是否溢出,然后調(diào)用宏 taskSELECT_HIGHEST_PRIORITY_TASK()
設(shè)置新的任務(wù)。該宏實(shí)現(xiàn)分兩種情況,普通情況下使用的定義如下
UBaseType_t uxTopPriority = uxTopReadyPriority;
while(listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority])))
{
--uxTopPriority;
}
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB,
&(pxReadyTasksLists[ uxTopPriority]));
uxTopReadyPriority = uxTopPriority;
通過(guò) while 查找當(dāng)前存在就緒任務(wù)的最高優(yōu)先級(jí)鏈表,獲取鏈表項(xiàng)設(shè)置任務(wù)指針。(通一個(gè)鏈表內(nèi)多個(gè)項(xiàng)目通過(guò)指針循環(huán),實(shí)現(xiàn)同優(yōu)先級(jí)任務(wù)獲得相同時(shí)間片執(zhí)行)。
而另外一種方式,需要平臺(tái)支持,主要差別是查找最高任務(wù)優(yōu)先級(jí),平臺(tái)支持利用平臺(tái)特性,效率會(huì)更高,但是移植性就不好說(shuō)了。
發(fā)生異常跳轉(zhuǎn)到異常處理服務(wù)前,自動(dòng)執(zhí)行的現(xiàn)場(chǎng)保護(hù)會(huì)保留返回模式(線程模式),使用堆棧指針等信息,所以,結(jié)束任務(wù)切換, 通過(guò)執(zhí)行 bx r14
返回,系統(tǒng)會(huì)自動(dòng)恢復(fù)現(xiàn)場(chǎng)(From stack),開(kāi)始運(yùn)行任務(wù)。
至此,任務(wù)切換完成。