系列文章目錄
FreeRTOS源碼分析一:task創建(RISCV架構)
文章目錄
- 系列文章目錄
- 前言
- vTaskStartScheduler 調度器啟動函數
- xPortStartScheduler架構特定調度器啟動函數
- vPortSetupTimerInterrupt啟動 RISCV 定時器中斷
- xPortStartFirstTask啟動第一個任務
- 附
- 空閑任務
- 總結
前言
本文繼續看 task 的運行。主要解析函數 vTaskStartScheduler
。
主函數中調用函數 vTaskStartScheduler
開始調度。
int main_blinky( void )
{vSendString( "Hello FreeRTOS!" );/* Create the queue. */xQueue = xQueueCreate( mainQUEUE_LENGTH, sizeof( uint32_t ) );if( xQueue != NULL ){/* Start the two tasks as described in the comments at the top of this* file. */xTaskCreate( prvQueueReceiveTask, "Rx", configMINIMAL_STACK_SIZE * 2U, NULL,mainQUEUE_RECEIVE_TASK_PRIORITY, NULL );xTaskCreate( prvQueueSendTask, "Tx", configMINIMAL_STACK_SIZE * 2U, NULL,mainQUEUE_SEND_TASK_PRIORITY, NULL );}vTaskStartScheduler();return 0;
}
vTaskStartScheduler 調度器啟動函數
初始化核心調度數據結構、創建必要的后臺任務(如 idle task 和 timer service task),并啟動第一個任務的上下文切換,從而進入多任務運行模式。
void vTaskStartScheduler( void )
{BaseType_t xReturn;// 創建 Idle Task(空閑任務)xReturn = prvCreateIdleTasks();// 啟用了軟件定時器(configUSE_TIMERS)#if ( configUSE_TIMERS == 1 ){if( xReturn == pdPASS ){// 創建 Timer Service Task。xReturn = xTimerCreateTimerTask();}}#endif /* configUSE_TIMERS */if( xReturn == pdPASS ){/* Interrupts are turned off here, to ensure a tick does not occur* before or during the call to xPortStartScheduler(). The stacks of* the created tasks contain a status word with interrupts switched on* so interrupts will automatically get re-enabled when the first task* starts to run. */// 關閉中斷,防止 tick 提前進入,xPortStartScheduler中我們會開啟中斷portDISABLE_INTERRUPTS();// 設置 Tick 計數器、調度狀態。xNextTaskUnblockTime = portMAX_DELAY;xSchedulerRunning = pdTRUE;xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;/* Setting up the timer tick is hardware specific and thus in the* portable interface. */// 開啟調度( void ) xPortStartScheduler();// 大部分情況下不會返回,除非內存不足}else{configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );}( void ) xIdleTaskHandles;( void ) uxTopUsedPriority;
}
我們暫時先跳過空閑任務創建和時鐘服務創建這兩個地方,著重看 xPortStartScheduler
這個架構特定的調度器啟動函數的實現。
xPortStartScheduler架構特定調度器啟動函數
檢查中斷服務的堆棧對齊并填充字節,初始化必要的硬件設置并啟動多任務調度
BaseType_t xPortStartScheduler( void )
{/* 聲明外部函數:啟動第一個任務的匯編函數 */extern void xPortStartFirstTask( void );/* 如果啟用了斷言檢查 */#if ( configASSERT_DEFINED == 1 ){/* 檢查中斷棧頂地址的字節對齊* 中斷棧與調度器啟動前main()函數使用的棧是同一個* 確保棧頂地址符合平臺的字節對齊要求 */configASSERT( ( xISRStackTop & portBYTE_ALIGNMENT_MASK ) == 0 );/* 如果配置了中斷棧大小(以字為單位) */#ifdef configISR_STACK_SIZE_WORDS{/* 使用特定的填充字節初始化整個中斷棧內存區域* 這有助于調試時檢測棧溢出和棧使用情況 */memset( ( void * ) xISRStack, portISR_STACK_FILL_BYTE, sizeof( xISRStack ) );}#endif /* configISR_STACK_SIZE_WORDS */}#endif /* configASSERT_DEFINED *//* 設置定時器中斷 */vPortSetupTimerInterrupt();/* 如果配置了MTIME和MTIMECMP寄存器的基地址(RISC-V標準定時器) */#if ( ( configMTIME_BASE_ADDRESS != 0 ) && ( configMTIMECMP_BASE_ADDRESS != 0 ) ){/* 啟用machine模式下的定時器中斷和外部中斷* 通過設置mie(Machine Interrupt Enable)寄存器:* - 位7 (0x80): 啟用定時器中斷* - 位11 (0x800): 啟用外部中斷* 0x880 = 0x80 | 0x800 */__asm volatile ( "csrs mie, %0" ::"r" ( 0x880 ) );}#endif /* ( configMTIME_BASE_ADDRESS != 0 ) && ( configMTIMECMP_BASE_ADDRESS != 0 ) *//* 啟動第一個任務* 這個函數會切換到第一個就緒任務的上下文* 從這點開始,系統進入多任務調度模式 */xPortStartFirstTask();/* 正常情況下不應該執行到這里* 因為調用xPortStartFirstTask()后,只有任務應該在執行* 如果執行到這里,說明調度器啟動失敗 */return pdFAIL;
}
這里我們需要關注兩個函數,一個是 vPortSetupTimerInterrupt
用于設置定時器中斷。一個是 xPortStartFirstTask
,啟動第一個任務。
vPortSetupTimerInterrupt啟動 RISCV 定時器中斷
簡單來說,只需要讀取 mtime 得到當前系統運行時間,并加上余量,設置 mtimecmp 即可在 mtime > mtimecmp 時觸發始終中斷。
實際實現也是這樣。
void vPortSetupTimerInterrupt( void )
{uint32_t ulCurrentTimeHigh, ulCurrentTimeLow;/* 設置指向MTIME寄存器的指針,MTIME是64位寄存器,高32位在+4字節偏移處 */volatile uint32_t * const pulTimeHigh = ( volatile uint32_t * const ) ( ( configMTIME_BASE_ADDRESS ) + 4UL ); /* 8-byte type so high 32-bit word is 4 bytes up. */volatile uint32_t * const pulTimeLow = ( volatile uint32_t * const ) ( configMTIME_BASE_ADDRESS );volatile uint32_t ulHartId;/* 讀取當前硬件線程ID(Hart ID),用于確定使用哪個定時器比較寄存器 */__asm volatile ( "csrr %0, mhartid" : "=r" ( ulHartId ) );/* 根據Hart ID計算對應的MTIMECMP寄存器地址,每個hart有獨立的比較寄存器 */pullMachineTimerCompareRegister = ( volatile uint64_t * ) ( ullMachineTimerCompareRegisterBase + ( ulHartId * sizeof( uint64_t ) ) );/* 原子地讀取64位MTIME寄存器值,防止在讀取過程中高位發生變化 */do{ulCurrentTimeHigh = *pulTimeHigh; /* 先讀取高32位 */ulCurrentTimeLow = *pulTimeLow; /* 再讀取低32位 */} while( ulCurrentTimeHigh != *pulTimeHigh ); /* 確保讀取期間高位沒有變化 *//* 將32位的高低位組合成64位的當前時間值 */ullNextTime = ( uint64_t ) ulCurrentTimeHigh;ullNextTime <<= 32ULL; /* 高32位左移到正確位置 */ullNextTime |= ( uint64_t ) ulCurrentTimeLow; /* 或上低32位 *//* 計算下次定時器中斷的時間點:當前時間 + 一個tick的時間增量 */ullNextTime += ( uint64_t ) uxTimerIncrementsForOneTick;/* 設置機器定時器比較寄存器,當MTIME達到這個值時觸發中斷 */*pullMachineTimerCompareRegister = ullNextTime;/* 預先計算下下次中斷的時間,為下次中斷處理做準備 */ullNextTime += ( uint64_t ) uxTimerIncrementsForOneTick;
}
xPortStartFirstTask啟動第一個任務
xPortStartFirstTask 只需按照堆棧中放置數據的約定,把數據放入合適的寄存器,把PC放入RA寄存器,調用ret返回即可執行任務
xPortStartFirstTask:/* 任務啟動函數 - 啟動第一個FreeRTOS任務 */load_x sp, pxCurrentTCB /* 將當前任務控制塊(TCB)的地址加載到棧指針寄存器 */load_x sp, 0( sp ) /* 從TCB的第一個成員讀取該任務的棧指針值,更新sp *//* 恢復任務的上下文 - 按照棧中保存的順序恢復寄存器 */load_x x1, 0( sp ) /* 恢復x1寄存器(ra - 返回地址),用作任務函數的返回地址 */load_x x5, 1 * portWORD_SIZE( sp ) /* 恢復初始mstatus寄存器值到x5(t0) */addi x5, x5, 0x08 /* 設置MIE位(Machine Interrupt Enable),使任務啟動時中斷使能 */csrw mstatus, x5 /* 將修改后的mstatus寫入控制狀態寄存器,從此處開始中斷使能! */portasmRESTORE_ADDITIONAL_REGISTERS /* 恢復RISC-V實現特有的額外寄存器(在freertos_risc_v_chip_specific_extensions.h中定義) *//* 恢復通用寄存器 - 臨時寄存器和參數寄存器 */load_x x7, 5 * portWORD_SIZE( sp ) /* 恢復t2寄存器 */load_x x8, 6 * portWORD_SIZE( sp ) /* 恢復s0/fp寄存器(幀指針) */load_x x9, 7 * portWORD_SIZE( sp ) /* 恢復s1寄存器 */load_x x10, 8 * portWORD_SIZE( sp ) /* 恢復a0寄存器(第一個參數/返回值) */load_x x11, 9 * portWORD_SIZE( sp ) /* 恢復a1寄存器(第二個參數) */load_x x12, 10 * portWORD_SIZE( sp ) /* 恢復a2寄存器(第三個參數) */load_x x13, 11 * portWORD_SIZE( sp ) /* 恢復a3寄存器(第四個參數) */load_x x14, 12 * portWORD_SIZE( sp ) /* 恢復a4寄存器(第五個參數) */load_x x15, 13 * portWORD_SIZE( sp ) /* 恢復a5寄存器(第六個參數) */#ifndef __riscv_32e/* 非RV32E架構(完整寄存器集)才需要恢復以下寄存器 */load_x x16, 14 * portWORD_SIZE( sp ) /* 恢復a6寄存器(第七個參數) */load_x x17, 15 * portWORD_SIZE( sp ) /* 恢復a7寄存器(第八個參數) */load_x x18, 16 * portWORD_SIZE( sp ) /* 恢復s2寄存器(保存寄存器) */load_x x19, 17 * portWORD_SIZE( sp ) /* 恢復s3寄存器(保存寄存器) */load_x x20, 18 * portWORD_SIZE( sp ) /* 恢復s4寄存器(保存寄存器) */load_x x21, 19 * portWORD_SIZE( sp ) /* 恢復s5寄存器(保存寄存器) */load_x x22, 20 * portWORD_SIZE( sp ) /* 恢復s6寄存器(保存寄存器) */load_x x23, 21 * portWORD_SIZE( sp ) /* 恢復s7寄存器(保存寄存器) */load_x x24, 22 * portWORD_SIZE( sp ) /* 恢復s8寄存器(保存寄存器) */load_x x25, 23 * portWORD_SIZE( sp ) /* 恢復s9寄存器(保存寄存器) */load_x x26, 24 * portWORD_SIZE( sp ) /* 恢復s10寄存器(保存寄存器) */load_x x27, 25 * portWORD_SIZE( sp ) /* 恢復s11寄存器(保存寄存器) */load_x x28, 26 * portWORD_SIZE( sp ) /* 恢復t3寄存器(臨時寄存器) */load_x x29, 27 * portWORD_SIZE( sp ) /* 恢復t4寄存器(臨時寄存器) */load_x x30, 28 * portWORD_SIZE( sp ) /* 恢復t5寄存器(臨時寄存器) */load_x x31, 29 * portWORD_SIZE( sp ) /* 恢復t6寄存器(臨時寄存器) */
#endif/* 恢復任務的臨界區嵌套計數器 */load_x x5, portCRITICAL_NESTING_OFFSET * portWORD_SIZE( sp ) /* 從任務棧中獲取該任務的臨界嵌套計數值 */load_x x6, pxCriticalNesting /* 將全局臨界嵌套變量的地址加載到x6 */store_x x5, 0( x6 ) /* 恢復該任務的臨界嵌套計數值到全局變量 *//* 恢復最后兩個臨時寄存器 */load_x x5, 3 * portWORD_SIZE( sp ) /* 恢復x5(t0)寄存器的初始值 */load_x x6, 4 * portWORD_SIZE( sp ) /* 恢復x6(t1)寄存器的初始值 *//* 調整棧指針,釋放上下文保存空間 */addi sp, sp, portCONTEXT_SIZE /* 棧指針向上調整,跳過已恢復的上下文數據 *//* 跳轉到任務函數開始執行 */ret /* 返回到x1(ra)寄存器中保存的任務函數地址,開始執行任務 */
總結調用鏈如下所示:
main_blinky() // 主函數:創建隊列和任務,啟動調度器└── vTaskStartScheduler() // 調度器啟動:初始化系統任務和調度狀態├── prvCreateIdleTasks() // 創建空閑任務(系統必需的后臺任務)├── xTimerCreateTimerTask() // 創建定時器服務任務(軟件定時器功能)└── xPortStartScheduler() // 架構相關啟動:硬件初始化和任務切換├── vPortSetupTimerInterrupt() // 設置RISC-V定時器中斷(任務切換時基)└── xPortStartFirstTask() // 啟動第一個任務(上下文切換到用戶任務)
附
這里我們簡單看一下前面在開始第一次調度的時候,創建的空閑任務具體內容。
空閑任務
為每一個CPU創建空閑任務,當前僅一個CPU
static BaseType_t prvCreateIdleTasks( void )
{BaseType_t xReturn = pdPASS; // 函數返回值,初始化為成功BaseType_t xCoreID; // 當前處理的CPU核心IDchar cIdleName[ configMAX_TASK_NAME_LEN ] = { 0 }; // 空閑任務名稱緩沖區TaskFunction_t pxIdleTaskFunction = NULL; // 空閑任務函數指針UBaseType_t xIdleTaskNameIndex; // 任務名稱字符索引// 第一步:構建空閑任務的基礎名稱// 從配置文件中的空閑任務名稱復制字符,直到遇到空字符或達到最大長度for( xIdleTaskNameIndex = 0U; xIdleTaskNameIndex < ( configMAX_TASK_NAME_LEN - taskRESERVED_TASK_NAME_LENGTH ); xIdleTaskNameIndex++ ){// 逐字符復制配置的空閑任務名稱cIdleName[ xIdleTaskNameIndex ] = configIDLE_TASK_NAME[ xIdleTaskNameIndex ];// 如果遇到字符串結束符,停止復制if( cIdleName[ xIdleTaskNameIndex ] == ( char ) 0x00 ){break;}}// 確保字符串以空字符結尾cIdleName[ xIdleTaskNameIndex ] = '\0';// 第二步:為每個CPU核心創建空閑任務// 以最低優先級為每個核心添加空閑任務for( xCoreID = ( BaseType_t ) 0; xCoreID < ( BaseType_t ) configNUMBER_OF_CORES; xCoreID++ ){// 根據系統配置選擇合適的空閑任務函數#if ( configNUMBER_OF_CORES == 1 ){// 單核系統:使用標準空閑任務函數pxIdleTaskFunction = &prvIdleTask;}#else /* #if ( configNUMBER_OF_CORES == 1 ) */{/* 在FreeRTOS SMP中,除了主空閑任務外,還會創建 configNUMBER_OF_CORES - 1 個* 被動空閑任務,確保每個核心在沒有其他任務可運行時都有空閑任務可執行 */if( xCoreID == 0 ){// 核心0:使用主空閑任務函數pxIdleTaskFunction = &prvIdleTask;}else{// 其他核心:使用被動空閑任務函數pxIdleTaskFunction = &prvPassiveIdleTask;}}#endif /* #if ( configNUMBER_OF_CORES == 1 ) */// 第三步:為多核系統更新空閑任務名稱,添加核心ID后綴以區分不同核心的空閑任務/* 在單核FreeRTOS中不需要此功能,因為只有一個空閑任務 */#if ( configNUMBER_OF_CORES > 1 ){// 宏不成立}#else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */{// 動態內存分配方式創建空閑任務/* 空閑任務使用動態分配的RAM創建 */xReturn = xTaskCreate( pxIdleTaskFunction, // 任務函數cIdleName, // 任務名稱configMINIMAL_STACK_SIZE, // 最小棧大小( void * ) NULL, // 任務參數portPRIVILEGE_BIT, // 優先級(實際上是 tskIDLE_PRIORITY | portPRIVILEGE_BIT,但tskIDLE_PRIORITY為0)&xIdleTaskHandles[ xCoreID ] ); // 任務句柄存儲位置}#endif /* configSUPPORT_STATIC_ALLOCATION */}return xReturn; // 返回創建結果(pdPASS表示成功,pdFAIL表示失敗)
}
pxIdleTaskFunction
是任務函數,具體內容非常簡單:
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{/* Stop warnings. */( void ) pvParameters;for( ; configCONTROL_INFINITE_LOOP(); ){/* See if any tasks have deleted themselves - if so then the idle task* is responsible for freeing the deleted task's TCB and stack. */prvCheckTasksWaitingTermination();}
}
簡單來說就是循環檢查是否需要回收任務空間。
總結
完結撒花!!!