系列文章目录
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();}
}
简单来说就是循环检查是否需要回收任务空间。
总结
完结撒花!!!