FreeRTOS内核:任务机制深度解析
一、任务的本质:函数 + 独立栈
函数比较容易理解,它定义了任务的具体行为逻辑。
// 任务函数示例:无限循环处理
void WorkerTask(void *pvParams) {
while(1) {
// 任务业务逻辑:传感器数据处理、控制算法等
ProcessSensorData();
ToggleOutputPin();
vTaskDelay(500); // 主动放弃CPU使用权
}
}
但栈的作用是什么(注意是独立栈)?
- 保存局部变量:函数内的局部变量(如数组、临时变量)存储在栈中;
- 保存调用关系:函数嵌套调用时,返回地址(LR)保存在栈中;
- 保存任务现场:任务被切换(中断)时,CPU寄存器的值存入栈,恢复时从栈中读回。
二、重新理解任务创建
1、任务创建函数与TCB
BaseType_t xTaskCreate(
TaskFunction_t pvTaskFunction, // 1. 任务函数指针(必传!)
const char * const pcName, // 2. 任务名称(可选)
uint16_t usStackDepth, // 3. 栈深度(字节数,如 150*4=600)
void *pvParameters, // 4. 函数参数(如 float value=3.14)
UBaseType_t uxPriority, // 5. 任务优先级(0~configMAX_PRIORITIES-1)
TaskHandle_t *pxCreatedTask // 6. 任务句柄(输出,用于操作任务)
);
结合计算机体系结构知识
| 参数 | 作用 | 代码示例 |
|---|---|---|
pvTaskFunction |
任务函数地址(PC 寄存器指向它) |
WorkerTask 函数地址 → xTaskCreate(WorkerTask, ...) |
usStackDepth |
栈大小(字节数)→ 由 pvPortMalloc() 分配 |
usStackDepth = 150 → 150*4=600字节栈 |
pvParameters |
传递给任务函数的参数(存入 R0) |
pvParameters = &sensorValue → 任务函数 WorkerTask(float *value) |
uxPriority |
任务优先级(决定调度顺序) | uxPriority = 2(优先级 2 比 1 高) |
TCB任务控制块(精简版本)
TCB = Task Control Block,是一个C语言结构体,用于"描述和管理一个任务",相当于任务的"身份证"。
typedef struct tskTaskControlBlock {
volatile StackType_t *pxTopOfStack; // 1. 栈顶指针(栈起始位置)
#if (configUSE_TRACE_FACILITY == 1)
UBaseType_t uxTCBNumber; // 任务编号
#endif
UBaseType_t uxPriority; // 2. 任务优先级
ListItem_t xStateListItem; // 3. 用于链表(就绪/阻塞列表)
ListItem_t xEventListItem; // 4. 用于事件列表(如队列)
#if (configUSE_MUTEXES == 1)
UBaseType_t uxBasePriority; // 优先级继承用
#endif
char pcTaskName[configMAX_TASK_NAME_LEN]; // 5. 任务名称
} tskTCB;
| TCB 字段 | 作用 | 代码中如何体现 |
|---|---|---|
pxTopOfStack |
栈顶地址(任务切换时恢复寄存器) | pxStack = (StackType_t *)pvPortMalloc(usStackDepth * sizeof(StackType_t)); |
uxPriority |
优先级(决定调度顺序) | uxPriority = 2 → 优先级 2 |
xStateListItem |
链表节点(连接到就绪列表/阻塞列表) | vListInsertEnd(&xReadyTasksLists[uxPriority], &pxNewTCB->xStateListItem); |
pcTaskName |
任务名称(调试用) | pcTaskName = "WorkerTask" |
TCB 里为什么没有"函数指针"?
函数指针和函数参数不是直接存储在TCB中,而是作为"初始现场"存储在任务栈中!
- 任务刚创建时,会在栈中预先填充:
- PC寄存器的值 = 任务函数的地址(让任务第一次运行时从函数入口开始);
- R0寄存器的值 = 任务函数的参数(
pvParameters); - 任务第一次被调度时,从栈中恢复这些寄存器,自动跳转到任务函数执行。
2、任务栈
FreeRTOS任务栈大小的单位:xTaskCreate的usStackDepth参数是栈项数(不是字节数)
- 针对Cortex-M内核(STM32),栈按**4字节(1个字)**对齐,因此栈项数×4 = 实际栈字节数;
- 例:
usStackDepth=200→ 实际栈大小 = 200×4=800字节;usStackDepth=512→ 2048字节。 - 栈增长方向:Cortex-M内核栈从高地址向低地址增长,栈溢出会覆盖低地址的内存(触发内存踩踏)。
2.1栈的大小如何确定?
栈的大小取决于两点:
void HelperFunction(void) {
// 延时循环
int counter; // 在函数开始处声明(C90要求)
for(counter = 0; counter < 200; counter++)
{
// 空循环体用于延时
}
}
void DataProcessingTask( void *pvParams )
{
const char *pcTaskName = "DataProc\r\n";
// 定义150个int的数组,占150*4=600字节
volatile int dataBuffer[150];
// 防止编译器优化,故意使用数组
dataBuffer[0] = 999;
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 处理数据任务信息 */
printf( pcTaskName );
/* 延迟一会(比较简单粗暴) */
HelperFunction();
}
}
(1)局部变量的大小
反汇编验证:编译后会看到
局部变量占用栈空间的大小可以通过反汇编确认。编译器会自动为局部变量分配栈空间。
(2)函数调用的深度
如果任务中嵌套调用子函数,LR(返回地址)会被保存,必须将LR压入栈,后续才能POP出(整个流程由编译器自动执行,我们只需注意是否会栈溢出即可)
2.2实际开发中栈大小的设置原则
估算:局部变量大小 × 2(保险起见,留余量)
之后使用FreeRTOS自带工具验证(核心方法),FreeRTOS提供uxTaskGetStackHighWaterMark()函数(栈高水位标记),能返回任务栈的剩余最小空间(栈项数),是判断栈是否够用的核心工具。
//开启宏定义,注意这个宏定义只适合调试阶段,发布后就不要使用了
#ifndef INCLUDE_uxTaskGetStackHighWaterMark
#define INCLUDE_uxTaskGetStackHighWaterMark 1
#endif
// 获取栈剩余最小空间(栈项数)
UBaseType_t uxMinSpace = uxTaskGetStackHighWaterMark(xDataTaskHandle);
// 打印:比如返回30,表示任务运行以来,栈最少剩余30个栈项(120字节)
printf("Data Task Stack High Water Mark: %u\n", uxMinSpace);
关键判断规则:
- 若
uxMinSpace返回值大于0且大于栈大小的20%:栈大小足够(比如栈大小200,剩余≥40,说明有足够余量); - 若返回值接近0(比如≤5):栈大小不足,需增大(比如从200→400);
- 若返回值等于栈大小:任务几乎没使用栈,可减小(比如从400→200)。
底层实现逻辑(学习后可以自己实现):
先把整个任务栈空间填充一个固定的"魔术字"(默认是tskSTACK_FILL_BYTE为0x5A,可在源码中修改),这个操作是栈高水位检测的基础
当任务运行后,堆栈会被使用,此时从栈底开始魔术字会被替代,读取剩余魔术字的个数就可知剩余堆栈大小(从栈底读会更方便,因为是从栈顶开始覆盖的)
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask )
{
TCB_t * pxTCB; // 定义任务控制块指针
uint8_t * pucStackStart;// 栈遍历的起始地址
UBaseType_t uxResult; // 返回值(剩余栈项数)
// 步骤1:获取任务TCB(xTask=NULL时,返回当前运行任务的TCB)
pxTCB = prvGetTCBFromHandle( xTask );
// 步骤2:根据栈增长方向,确定遍历的起始地址(跨架构兼容的核心)
#if portSTACK_GROWTH < 0 // 栈从高地址→低地址增长(Cortex-M、ARM)
{
// 起始地址 = 栈底地址(pxStack)
pucStackStart = ( uint8_t * ) pxTCB->pxStack;
}
#else // 栈从低地址→高地址增长(如x86)
{
// 起始地址 = 栈顶地址(pxEndOfStack)
pucStackStart = ( uint8_t * ) pxTCB->pxEndOfStack;
}
#endif
// 步骤3:调用底层函数,统计从起始地址开始的连续魔术字数量(剩余栈空间)
uxResult = ( UBaseType_t ) prvCheckFreeStackSpace( pucStackStart );
// 步骤4:返回剩余栈项数
return uxResult;
}
FreeRTOS提供了两种高水位检测方法
| 对比维度 | uxTaskGetStackHighWaterMark(v1) | uxTaskGetStackHighWaterMark2(v2) |
|---|---|---|
| 遍历范围 | 从起始地址遍历,遇到第一个非魔术字就停止(仅统计"连续未被覆盖的魔术字") | 遍历整个栈空间,统计所有未被覆盖的魔术字(全程遍历) |
| 检测精度 | 低(可能误判,无法反映真实最小剩余空间) | 高(精准统计运行以来栈剩余的最小值,无漏判) |
| CPU开销 | 小(遍历到第一个非魔术字即终止) | 稍大(遍历整个栈空间,但实际影响可忽略) |
| 设计定位 | 早期版本,性能优先 | 升级版,精度优先(FreeRTOS推荐使用) |
| 适用场景 | 性能极度敏感、栈使用简单的场景 | 调试/排查栈溢出、需要精准检测的场景 |
2.3发生内存踩踏怎么办?
(1)内存踩踏的核心定义
内存踩踏(也叫内存越界/缓冲区溢出)是指程序访问了超出分配给它的内存范围的地址,导致覆盖了其他内存区域的数据(比如任务栈溢出覆盖相邻任务的栈、数组越界写覆盖全局变量)。
嵌入式中内存踩踏分两类:
- 栈踩踏(最常见):任务栈溢出、数组越界写栈内存;
- 堆踩踏:堆内存越界写、free后重复使用(野指针)、内存碎片导致的越界。
| 成因类型 | 具体例子 |
|---|---|
| 任务栈过小导致栈溢出 | 任务栈大小100栈项(400字节),但局部数组占500字节,栈溢出覆盖相邻内存 |
| 数组/缓冲区越界读写 | char buffer[20]; buffer[20] = 0;(下标20超出buffer的0~19范围) |
| 指针操作不当 | 野指针(指向已释放内存的指针)、空指针解引用、指针偏移越界(p = p + 150) |
| 中断嵌套过深 | 多级中断嵌套,消耗任务栈超出预期 |
| 递归调用无终止 | 递归函数无退出条件,不断压栈导致溢出 |
| 堆内存管理不当 | pvPortMalloc(20)后,写入30字节数据;free后未置NULL,重复使用 |
(2)预防方法(核心,从源头避免)
① 合理设置任务栈大小(最关键)
结合前面的"栈高水位"方法,确保每个任务的栈有足够余量(≥20%)。
② 开启FreeRTOS栈溢出检测
FreeRTOS提供两种栈溢出检测机制,在FreeRTOSConfig.h中配置:
configCHECK_FOR_STACK_OVERFLOW=1:检测栈指针是否超出栈范围(简单,开销小);configCHECK_FOR_STACK_OVERFLOW=2:检测栈末尾的"魔术字"是否被覆盖(更精准,开销稍大)。
//开启栈溢出钩子函数宏定义
#ifndef configCHECK_FOR_STACK_OVERFLOW
#define configCHECK_FOR_STACK_OVERFLOW 1
#endif
// 栈溢出钩子函数(检测到溢出时调用)需要自己写函数内容
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
// 打印溢出的任务名,或触发硬件报警(如LED常亮)
printf("Stack Overflow: %s\n", pcTaskName);
// 紧急处理:停止任务/重启系统
vTaskDelete(xTask);
}
③ 禁止数组越界,规范指针使用
- 数组操作前做边界检查:
if(i < sizeof(buffer)/sizeof(buffer[0])) { buffer[i] = 0; }; - 指针初始化:所有指针定义后立即置NULL,使用前检查是否为NULL;
- 禁止直接操作裸指针:封装指针操作函数,增加边界检查。
④ 使用MPU(内存保护单元)
针对Cortex-M3/M4/M7内核,FreeRTOS支持MPU,可给每个任务分配独立的内存区域,一旦访问越界,立即触发硬件异常(HardFault),精准定位问题。
⑤ 避免大局部变量,禁止无终止递归
- 大局部变量(如>256字节)改用全局/静态变量,或动态分配;
- 递归函数必须有明确的退出条件,且限制递归深度。
(3)排查方法(定位已发生的内存踩踏)
① 内存填充法
在栈/内存区域填充"魔术字",运行后检查是否被覆盖
② 硬件调试(精准定位)
使用调试器连接MCU:
- 查看内存布局(Map文件),确定任务栈、全局变量的地址范围;
- 给可疑内存地址设置"写断点"(Write Breakpoint),当该地址被改写时,程序暂停,直接定位到改写的代码行;
- 查看调用栈(Call Stack),分析函数嵌套和栈使用情况。
③ 用FreeRTOS工具监控
vTaskList():打印所有任务的状态、栈剩余空间,快速找到栈不足的任务;- Segger SystemView:可视化监控任务运行、栈使用、中断触发,定位栈溢出时机;
xPortGetHeapStats():查看堆内存使用情况,排查堆踩踏。
④ 编译器开启严格警告
2.4FreeRTOS栈的内存从哪里来?
FreeRTOS的"取巧"方法:定义一个巨大的全局数组作为"内存池",创建任务时从这个数组里划分一块内存作为任务栈。
创建任务时,用**pvPortMalloc从ucHeap**里分配栈空间,栈的起始地址和大小保存在TCB里
三、任务状态与链表
1.任务状态
| 状态 | 说明 | 例子 |
|---|---|---|
| 运行态 (Running) | 当前正在CPU上执行 | 任务A正在运行 |
| 就绪态 (Ready) | 已准备好运行,等待调度 | 任务A优先级高,排队等待 |
| 阻塞态 (Blocked) | 等待事件(如延时、队列、信号量) | vTaskDelay(15) → 等待15ms |
| 挂起态 (Suspended) | 被显式暂停(如vTaskSuspend()) |
任务B被暂停,不参与调度 |
- 运行态→就绪态:时间片到(同优先级任务切换),或被高优先级任务抢占;
- 运行态→阻塞态:调用
vTaskDelay(等待时间)、读队列没数据(等待事件); - 阻塞态→就绪态:等待的时间到了,或等待的事件发生了;
- 运行态→挂起态:调用
vTaskSuspend; - 挂起态→就绪态:调用
vTaskResume。
2.三大链表
| 链表类型 | 作用 | 结构 |
|---|---|---|
| 就绪链表(Ready List) | 管理所有处于"就绪态"的任务,调度器从中选择最高优先级的任务运行。 | 通常是一个数组,数组的每个索引对应一个优先级,每个元素是一个双向链表,存储该优先级下所有就绪的任务。 |
| 延迟链表(Delay List) | 管理所有处于"阻塞态(等待时间)"的任务,用于处理任务延时或超时唤醒。 | 通常是一个或两个按超时时间(唤醒时间)排序的链表。系统时钟节拍中断会检查链表头部,将已超时的任务移回就绪链表。 |
| 挂起链表(Suspended List) | 管理所有处于"挂起态"的任务,这些任务被显式暂停,不参与任何调度直到被恢复。 | 通常是一个简单的双向链表,将所有被挂起的任务链接在一起,不区分优先级或时间顺序。 |
(1)创建任务时:加入就绪链表
(2)任务调用vTaskDelay时:从就绪链表移到延迟链表
(3)Tick中断时执行xTaskIncrementTick:检查延迟链表,时间到了移回就绪链表
3.任务切换
四、调度器的核心
1. 调度的核心规则
优先找最高优先级:从就绪链表的"最高优先级"开始往下找(比如先找优先级5,再找4,…,最后找0);
同优先级轮流执行:如果最高优先级的链表有多个任务,取出第一个执行,执行完一个tick后放到链表尾部,再取下一个。
A.补充说明PendSV(Pendable Request for System Service可挂起的服务请求)(重要)
在ARM Cortex-M内核中,PendSV是NVIC(嵌套向量中断控制器)管理的一个异常源(Exception)。其核心特点如下:
- 可手动触发:无硬件自动触发源,必须通过写NVIC的寄存器手动触发;
- 可挂起:触发后若当前有更高优先级的中断/异常在执行,会被NVIC标记为"挂起状态",直到高优先级处理完才执行;
- 优先级可配置:通过NVIC的优先级寄存器配置优先级,FreeRTOS中会将其设为最低优先级。
// 这行代码不会立即跳转!
// 它只是把NVIC内部的一个小旗帜(Pending位)竖起来了
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
FreeRTOS利用这个机制,把PendSV用于任务切换。FreeRTOS有两个接口可挂起PendSV:
- xPortSysTickHandler:SysTick中断服务函数,FreeRTOS的"心跳",定时触发调度(延时到期/时间片用完),最终通过置位PendSV触发调度;
- portYIELD:宏定义,主动触发调度(任务主动让权),直接置位PendSV触发调度;
2. 调度的触发者:Tick中断
- Tick中断是"定时器中断",每隔固定时间(默认1ms可设置)产生一次;
- 硬件触发:SysTick定时器溢出。
- 入口:
SysTick_Handler(启动文件中) -> 映射到xPortSysTickHandler(port.c)。 - 逻辑判断:
xTaskIncrementTick(tasks.c) -> 发现任务A延时结束/时间片用完,任务B优先级更高。 - 请求切换:
xPortSysTickHandler设置SCB->ICSR |= SCB_ICSR_PENDSVSET。 - 退出SysTick:回到主程序或被打断的低优先级中断。
- 进入PendSV:
xPortPendSVHandler(port.c汇编部分)。(SysTick中断优先级比PendSV高) - 执行切换:保存任务A现场 -> 调度器选任务B -> 恢复任务B现场。
- 运行新任务:CPU开始执行任务B。
/*-----------------------------------------------------------*/
/* FreeRTOS SysTick 中断处理程序 (ARM Cortex-M) */
/* 文件位置:FreeRTOS/Source/portable/[编译器]/[架构]/port.c */
/* 核心功能:更新系统节拍 + 决策是否需要任务切换 */
/*-----------------------------------------------------------*/
void xPortSysTickHandler( void )
{
/*
* 【关键背景】
* 在标准的 FreeRTOS Cortex-M 端口中,SysTick 中断的优先级被配置为
* configKERNEL_INTERRUPT_PRIORITY (通常是最低优先级,如 0xFF)。
*
* 这意味着:
* 1. 当 SysTick 运行时,所有比它优先级高的中断(用户中断)本来就可以打断它。
* 2. 但是,我们需要防止比它优先级【低】或【相等】的中断(实际上没有更低的了,主要是防止逻辑冲突)
* 干扰 FreeRTOS 的内部临界区操作(如链表修改)。
*
* 【优化策略】
* 因为已知当前中断优先级是最低的,所以不需要保存之前的中断屏蔽状态(因为之前肯定是全开的)。
* 直接使用 vPortRaiseBASEPRI() 快速提升优先级屏蔽阈值,比通用的 portSET_INTERRUPT_MASK_FROM_ISR() 更快。
*/
/* 1. 提升 BASEPRI,屏蔽所有 <= configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断 */
/* 这创建了一个短暂的临界区,保护下面的 xTaskIncrementTick 操作 */
vPortRaiseBASEPRI();
{
/* 2. 【核心逻辑】更新 RTOS 节拍计数 */
/* 此函数位于 tasks.c,执行以下操作:
* a) xTickCount++ (时间加 1)
* b) 检查延迟链表 (Delay List),将超时任务移至就绪链表 (Ready List)
* c) 检查时间片轮转 (如果开启)
*
* 返回值含义:
* pdFALSE: 没有更高优先级任务就绪,且不需要时间片轮转 -> 无需切换
* pdTRUE : 有更高优先级任务就绪,或时间片到期 -> 需要切换
*/
if( xTaskIncrementTick() != pdFALSE )
{
/* 3. 【决策】需要上下文切换 */
/* 重要设计:为什么不直接在这里切换?
* 因为 SysTick 优先级较低(或者说为了保持中断响应快),
* 真正的"保存/恢复寄存器"这种耗时操作应该交给优先级【最低】的 PendSV 中断去做。
* 这样可以让高优先级的硬件中断(如电机保护、通信)随时打断任务切换过程。
*/
/* 4. 【触发 PendSV】
* 向 NVIC 的中断控制状态寄存器 (ICSR) 写入 PENDSVSET 位。
* 这不会立即执行 PendSV,而是将其标记为"待决 (Pending)"。
*
* 硬件行为:
* 当当前中断 (SysTick) 退出后,CPU 会检查是否有 pending 的中断。
* 由于 PendSV 优先级最低,它会等所有其他高优先级中断都处理完后,
* 最后才执行 xPortPendSVHandler 进行真正的任务切换。
*/
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
/* 5. 恢复中断优先级屏蔽 */
vPortClearBASEPRIFromISR();
}
3. 任务切换的本质:保存当前任务现场 + 恢复新任务现场(进入 PendSV)
在ARM Cortex-M中,异常发生时硬件会自动保存一部分寄存器(r0r3, r12, LR, PC, xPSR)到当前栈中。因此,PendSV只需保存剩余的callee-saved寄存器(r4r11)。
/*-----------------------------------------------------------*/
/* FreeRTOS PendSV 中断处理程序 (ARM Cortex-M) */
/* 功能:实现任务切换的核心逻辑 (保存旧任务 -> 调度 -> 恢复新任务) */
/*-----------------------------------------------------------*/
__asm void xPortPendSVHandler( void )
{
/* 声明外部变量和函数,链接器会在链接阶段填入实际地址 */
extern uxCriticalNesting; /* 临界区嵌套计数 */
extern pxCurrentTCB; /* 全局指针:指向当前运行任务的 TCB */
extern vTaskSwitchContext; /* C 语言函数:调度器核心,负责选择下一个最高优先级的任务 */
/* *INDENT-OFF* */
PRESERVE8 /* 指示编译器保持堆栈 8 字节对齐 (ARM 架构要求) */
/* ======================================================== */
/* 阶段 1: 保存当前任务 (Current Task) 的上下文 */
/* ======================================================== */
/* 此时 CPU 已经自动将 R0-R3, R12, LR, PC, xPSR 压入栈中。 */
/* 我们需要手动保存剩下的 R4-R11。 */
mrs r0, psp /* [读] 获取当前任务的栈指针 (Process Stack Pointer) */
isb /* [同步] 指令同步屏障,确保上面的 MSR 操作完成后再执行后续 */
ldr r3, =pxCurrentTCB /* [读] 将全局变量 pxCurrentTCB 的地址加载到 r3 */
ldr r2, [ r3 ] /* [读] 解引用:r2 = pxCurrentTCB 的值 (即当前任务 TCB 结构体的地址) */
stmdb r0 !, { r4 - r11 } /* [写/核心] 保存剩余寄存器 */
/* 含义:将 r4-r11 的值压入 r0 指向的栈内存中 */
/* "db" = Decrement Before (先减地址再存,向下生长栈) */
/* "!" = 写回 (Write-back):更新 r0 为新的栈顶地址 */
str r0, [ r2 ] /* [写/核心] 更新 TCB 中的栈指针记录 */
/* 含义:将新的栈顶地址 (r0) 写入 TCB 的第一个成员 (pxTopOfStack) */
/* ======================================================== */
/* 阶段 2: 调用调度器 (Scheduler) 选择新任务 */
/* ======================================================== */
stmdb sp !, { r3, r14 } /* [写] 保护现场 */
/* 将 r3 (pxCurrentTCB 的地址) 和 r14 (返回地址) 压入中断栈 (MSP) */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
/* [配置] 设置中断屏蔽阈值 */
msr basepri, r0 /* [写] 屏蔽所有优先级 <= configMAX... 的中断 */
dsb /* [同步] 数据同步屏障,确保写操作完成 */
isb /* [同步] 指令同步屏障 */
bl vTaskSwitchContext /* [调用] 跳转到 C 语言调度器 */
/* 函数内部逻辑: */
/* 1. 遍历就绪链表 (Ready List) */
/* 2. 找到优先级最高的就绪任务 */
/* 3. 更新全局变量 pxCurrentTCB 指向这个新任务的 TCB */
mov r0, #0 /* [配置] 准备恢复中断 */
msr basepri, r0 /* [写] 清除 BASEPRI,重新允许所有中断 */
ldmia sp !, { r3, r14 } /* [读] 恢复保护现场 */
/* ======================================================== */
/* 阶段 3: 恢复新任务 (Next Task) 的上下文 */
/* ======================================================== */
ldr r1, [ r3 ] /* [读] 获取新任务的 TCB 地址 */
ldr r0, [ r1 ] /* [读] 获取新任务的栈顶指针 */
ldmia r0 !, { r4 - r11 } /* [读/核心] 恢复剩余寄存器 */
msr psp, r0 /* [写/核心] 切换栈指针 */
isb /* [同步] 确保 PSP 更新生效 */
bx r14 /* [跳转] 中断返回 */
nop /* [填充] 空操作,通常用于对齐或调试断点 */
/* *INDENT-ON* */
}
4.特殊任务:空闲任务
- 优先级最低(优先级0);
- 调度器启动时自动创建;
- 核心工作:做"清理工作"—— 比如删除任务后,释放任务的栈和TCB。
5.关键配置
配置项:configUSE_PREEMPTION(抢占式调度)
- 设为1:开启抢占式(默认),高优先级任务抢占低优先级;
- 设为0:关闭抢占式,任务只能主动放弃CPU,高优先级任务不会抢占。
配置项:configUSE_TIME_SLICING(时间片轮转)
- 设为1:开启时间片轮转(默认),同优先级任务轮流执行一个tick;
- 设为0:关闭时间片轮转,同优先级任务中,第一个任务会一直运行,直到主动放弃。
五、总结
任务 = 函数 + 独立栈:函数是业务逻辑,栈是保存局部变量、调用关系、现场的空间;
TCB是任务的"身份证":保存栈顶指针、优先级、链表节点,用来管理任务;
链表用于任务管理:就绪链表、延迟链表、挂起链表,分别管理不同状态的任务;
调度器靠Tick中断驱动:每隔一段时间检查延迟链表、切换任务;
核心调度规则:高优先级抢占,同优先级时间片轮转;
空闲任务是"兜底":优先级最低,做清理工作,还会礼让同优先级任务;
**中断优先级分层:**硬件中断(包括Tick) > PendSV中断 > 任务。