嵌入式4-freertos 电子说
▼关注公众号:27熊熊嵌入式▼
在嵌入式开发领域,RTOS(实时操作系统)应用广泛——无人机飞行控制、汽车电子ECU、医疗设备、智能家居、智能手表,背后都有它的身影。
很多开发者入门RTOS时,会先接触FreeRTOS、RT-Thread等内核,却容易陷入“会用API,不懂原理”的困境:为什么它能保证实时响应?多任务如何“同时”运行?上下文切换到底在做什么?
先搞懂:RTOS内核到底是什么?
核心认知:RTOS的核心是“内核”,它不是完整的操作系统(无图形界面、无复杂文件系统),而是一套轻量级任务调度与资源管理框架——相当于嵌入式系统的“大脑”,负责分配CPU资源、调度任务执行、管理系统资源,核心使命是在严格时间约束下,确定性响应外部事件。
和Windows、Linux等通用操作系统相比,RTOS内核有三个核心优势:
轻量性:代码体积通常低于10KB,ROM/RAM占用可控,支持裁剪,适配单片机等资源有限硬件(典型任务栈仅需0.5KB~4KB);
实时性:分为硬实时和软实时,硬实时要求任务必须在截止期限前完成(否则系统失效,如汽车ABS制动),软实时允许偶发超时(如音视频同步);
确定性:任务从就绪到执行的最大延迟(最坏情况响应时间)可计算、可验证,这是通用操作系统无法实现的核心特性。
核心拆解1:任务管理——RTOS的“最小执行单元”
RTOS的核心能力,是将复杂系统功能拆分为多个独立任务,每个任务是可独立运行、被调度的最小单元。
任务的3个核心组成(缺一不可)
每个可运行的RTOS任务,包含三个核心部分:
任务函数:核心执行逻辑,通常为无限循环(for(;;)或while(1)),避免任务执行完毕后进入未知状态,“传感器数据采集”“串口指令处理”等业务逻辑均在此编写;
任务堆栈:每个任务的独立工作空间,用于保存上下文数据、局部变量、函数调用返回地址,记录任务运行状态;
任务控制块(TCB):RTOS管理任务的数据结构,包含任务优先级、运行状态、堆栈指针、任务句柄等信息,内核通过TCB定位和管理任务。
FreeRTOS核心代码分析(任务管理)
FreeRTOS中,任务控制块(TCB)的核心定义代码如下(简化版,去除冗余配置),对应上述任务核心组成:
// FreeRTOS任务控制块(TCB)完整定义(来自 tasks.c)typedef struct tskTaskControlBlock{ volatile StackType_t *pxTopOfStack; // 当前栈顶指针(任务切换时保存/恢复) ListItem_t xStateListItem; // 任务状态链表节点(挂载到就绪/阻塞/挂起链表) ListItem_t xEventListItem; // 事件链表节点(用于信号量、队列等待) UBaseType_t uxPriority; // 任务优先级(数值越大优先级越高) StackType_t *pxStack; // 任务堆栈起始地址 char pcTaskName[ configMAX_TASK_NAME_LEN ]; // 任务名称(便于调试) // ... 其他可选成员(如栈溢出检测标记、任务通知值等)} tskTCB;
创建任务的核心API(xTaskCreate),本质是初始化TCB、分配任务堆栈、将任务加入就绪链表
源码:
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, const char * const pcName, const configSTACK_DEPTH_TYPE usStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask ) { TCB_t *pxNewTCB; // 新任务控制块指针 BaseType_t xReturn; // 返回状态 /* 根据堆栈生长方向,决定先分配TCB还是堆栈,避免堆栈覆盖TCB */ #if ( portSTACK_GROWTH > 0 ) /* 堆栈向上生长:先TCB后堆栈 */ pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); if( pxNewTCB != NULL ) { pxNewTCB->pxStack = ( StackType_t * ) pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); if( pxNewTCB->pxStack == NULL ) { vPortFree( pxNewTCB ); pxNewTCB = NULL; } } #else /* portSTACK_GROWTH <= 0 — 堆栈向下生长:先堆栈后TCB */ { StackType_t *pxStack = pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); if( pxStack != NULL ) { pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); if( pxNewTCB != NULL ) { pxNewTCB->pxStack = pxStack; // 将堆栈地址存入TCB } else { vPortFreeStack( pxStack ); } } else { pxNewTCB = NULL; } } #endif /* portSTACK_GROWTH */ if( pxNewTCB != NULL ) { /* 标记任务为动态分配(便于后续删除时释放内存) */ #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB; #endif /* 初始化任务TCB:任务名、优先级、堆栈布局、入口函数等 */ prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL ); /* 将任务加入就绪链表(根据优先级挂载到对应的 pxReadyTasksLists[ ]) */ prvAddNewTaskToReadyList( pxNewTCB ); xReturn = pdPASS; } else { xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY; } return xReturn; }#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
关键说明:FreeRTOS中,所有就绪任务按优先级挂载到对应就绪链表,调度器从该链表选择最高优先级任务执行,这是抢占式调度的基础。
任务的4种核心状态(动态切换)
任务并非一直处于运行状态,而是在4种状态间动态切换,这是理解调度器的基础:
运行态:任务占用CPU,执行核心逻辑(单核CPU同一时刻,仅一个任务处于运行态);
就绪态:任务准备就绪,具备运行条件,但CPU被更高优先级任务占用,无法运行;
阻塞态:任务因等待事件(延时、信号量、消息队列)暂停,即使CPU空闲也无法运行(如任务延时1秒,期间处于阻塞态);
挂起态:任务被手动暂停(通过RTOS API调用),即使等待事件满足,也无法进入就绪态,需手动唤醒。
示例:温湿度采集任务每隔1秒采集一次数据——采集完成后进入阻塞态(等待1秒);1秒延时结束后进入就绪态;无更高优先级任务时,调度器让其进入运行态,开始下一次采集。
核心拆解2:调度器
调度器负责决定“哪个任务优先占用CPU”“何时切换任务”,核心目标是保证高优先级任务优先执行,确保系统实时性。
主流RTOS(FreeRTOS、RT-Thread)采用“抢占式+时间片轮转”混合调度策略,兼顾实时性和公平性:
1. 抢占式调度
这是RTOS实现实时性的关键——更高优先级任务从阻塞态变为就绪态时,调度器立即暂停当前运行的低优先级任务,切换到高优先级任务执行,无需等待低优先级任务完成。
示例:故障报警任务(高优先级)和日志记录任务(低优先级)——日志任务正常运行,检测到故障后,报警任务进入就绪态,调度器立即暂停日志任务,优先执行报警任务,确保故障及时响应。
2. 时间片轮转调度(核心:同优先级任务公平执行)
多个任务优先级相同时,调度器给每个任务分配时间片(如10ms),任务执行完时间片后,主动放弃CPU,切换到下一个同优先级任务,实现“看似同时运行”的效果。
示例:两个同优先级的数据流显示任务和串口接收任务,时间片设为10ms——两任务轮流占用CPU,各执行10ms,从用户角度看,两个功能同步进行。
调度器的调度时机固定,主要在两个场景:一是任务主动放弃CPU(延时、等待事件);二是中断服务程序执行完毕后,调度器检查是否有更高优先级任务就绪,有则触发任务切换。
FreeRTOS核心代码分析(调度器)
FreeRTOS调度器核心函数是vTaskSwitchContext(),作用是“选择下一个执行任务”,核心逻辑是从就绪链表找到最高优先级任务
void vTaskSwitchContext( void ){ if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) { /* 调度器挂起,暂不切换,仅标记需要挂起的任务切换 */ xYieldPending = pdTRUE; } else { xYieldPending = pdFALSE; traceTASK_SWITCHED_OUT(); #if ( configGENERATE_RUN_TIME_STATS == 1 ) /* 记录当前任务的运行时间统计(略) */ // ...... #endif /* 栈溢出检测(若使能) */ taskCHECK_FOR_STACK_OVERFLOW(); /* 选择下一个最高优先级的就绪任务 */ taskSELECT_HIGHEST_PRIORITY_TASK(); /* 核心宏:遍历就绪链表,更新 pxCurrentTCB */ traceTASK_SWITCHED_IN(); /* 更新 Newlib 可重入结构(若使能) */ #if ( configUSE_NEWLIB_REENTRANT == 1 ) _impure_ptr = &( pxCurrentTCB->xNewLib_reent ); #endif }}
补充说明:FreeRTOS中,时间片轮转依赖系统滴答定时器(SysTick)中断,每次中断检查当前任务时间片是否用完,用完则触发调度器切换任务,核心代码在xPortSysTickHandler()中,简化如下:
void xPortSysTickHandler( void ){ /* 进入临界区:提高 BASEPRI 以屏蔽部分中断 */ vPortRaiseBASEPRI(); { /* 增加系统节拍计数,并检查是否需要触发 PendSV 进行任务切换 */ if( xTaskIncrementTick() != pdFALSE ) { /* 触发 PendSV 异常,在最低优先级执行实际上下文切换 */ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; } } vPortClearBASEPRIFromISR();}BaseType_t xTaskIncrementTick( void ){ TCB_t * pxTCB; TickType_t xItemValue; BaseType_t xSwitchRequired = pdFALSE; /* 如果调度器未挂起,正常处理节拍 */ if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1; xTickCount = xConstTickCount; /* 节拍溢出处理:交换延迟链表和溢出延迟链表 */ if( xConstTickCount == ( TickType_t ) 0U ) { taskSWITCH_DELAYED_LISTS(); } /* 检查是否有任务因延时或超时而解除阻塞 */ if( xConstTickCount >= xNextTaskUnblockTime ) { for( ;; ) { if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE ) { xNextTaskUnblockTime = portMAX_DELAY; break; } else { pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) ); if( xConstTickCount < xItemValue ) { xNextTaskUnblockTime = xItemValue; break; } /* 将任务从延迟链表中移除,并加入就绪链表 */ listREMOVE_ITEM( &( pxTCB->xStateListItem ) ); if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ) { listREMOVE_ITEM( &( pxTCB->xEventListItem ) ); } prvAddTaskToReadyList( pxTCB ); #if ( configUSE_PREEMPTION == 1 ) /* 如果解除阻塞的任务优先级 >= 当前任务,则标记需要切换 */ if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) { xSwitchRequired = pdTRUE; } #endif } } } /* 时间片轮转:同优先级就绪任务数 > 1 则触发切换 */ #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ) { xSwitchRequired = pdTRUE; } #endif /* 处理挂起的任务切换请求 */ #if ( configUSE_PREEMPTION == 1 ) if( xYieldPending != pdFALSE ) { xSwitchRequired = pdTRUE; } #endif } else { /* 调度器挂起时,累加挂起的节拍数,稍后补处理 */ ++xPendedTicks; } return xSwitchRequired;}
调度器切换任务时,关键操作是上下文切换
上下文,即任务运行时的全部状态,包括CPU寄存器(PC、SP、通用寄存器)、任务堆栈数据等——即任务“当前运行到哪一步”的所有信息。
上下文切换过程分两步,由内核自动完成,无需开发者干预:
保存上下文:任务被暂停时,内核将其上下文信息(寄存器、堆栈数据)保存到自身任务堆栈,即“保存工作进度”;
恢复上下文:任务再次被调度时,内核从其堆栈中恢复上下文信息到CPU寄存器,即“恢复工作进度”,任务从暂停处继续执行。
FreeRTOS核心代码分析(上下文切换)
FreeRTOS上下文切换分“保存当前任务上下文”和“恢复下一个任务上下文”,核心代码为汇编实现,简化后如下:
__asm void xPortPendSVHandler( void ){ extern uxCriticalNesting; extern pxCurrentTCB; extern vTaskSwitchContext; PRESERVE8 /* 获取当前任务的进程栈指针 (PSP) */ mrs r0, psp isb /* 获取当前任务控制块的指针 */ ldr r3, =pxCurrentTCB ldr r2, [r3] /* 如果使用了 FPU,保存 FPU 高寄存器 */ tst r14, #0x10 it eq vstmdbeq r0!, {s16-s31} /* 保存核心寄存器 (r4-r11 以及 r14) */ stmdb r0!, {r4-r11, r14} /* 将新的栈顶指针保存到 TCB 的第一个成员 */ str r0, [r2] /* 屏蔽中断,调用调度器选择下一个任务 */ stmdb sp!, {r0, r3} mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 dsb isb bl vTaskSwitchContext mov r0, #0 msr basepri, r0 ldmia sp!, {r0, r3} /* 获取新任务的 TCB 和栈顶指针 */ ldr r1, [r3] ldr r0, [r1] /* 恢复新任务的寄存器 */ ldmia r0!, {r4-r11, r14} /* 恢复 FPU 寄存器(如果之前保存过) */ tst r14, #0x10 it eq vldmiaeq r0!, {s16-s31} /* 更新 PSP 并异常返回 */ msr psp, r0 isb bx r14}
补充:不同RTOS内核(FreeRTOS、RT-Thread、uC/OS)实现细节有差异(如优先级数值规则:FreeRTOS数值越大优先级越高,uC/OS则相反),但核心原理一致——掌握这些逻辑,切换任何RTOS都能快速上手。
文章参考:
FreeRTOS源码仓库(可下载完整源码,对照本文代码分析学习):https://github.com/FreeRTOS/FreeRTOS-Kernel
FreeRTOS中文官方文档(适配国内开发者,简化易懂):https://www.freertos.org/(含内核原理、任务管理、调度器等核心模块详解)
审核编辑 黄宇
全部0条评论
快来发表一下你的评论吧 !