3. FreeRTOS内核分析(上):任务创建与删除、任务状态机、优先级与调度
好,咱们今天正式进入FreeRTOS的内核分析。说实话,这部分是很多嵌入式工程师的“拦路虎”。我当年刚接触RTOS时,也被任务状态机绕得晕头转向。但别怕,咱们一步步拆开来看,你会发现其实没那么玄乎。
这一章,我打算重点讲三个核心概念:任务创建与删除、任务状态机、以及优先级与调度。这三块是FreeRTOS的基石,搞懂了它们,后面的信号量、队列、互斥锁什么的,理解起来就顺理成章了。
3.1 任务创建与删除:从无到有,再到无
任务,说白了就是一个永不返回的C函数。它有自己的栈空间、优先级和状态。创建任务,就是告诉内核:“嘿,帮我准备一个执行上下文,以后按这个优先级调度我。”
3.1.1 任务创建:xTaskCreate() 深度解析
最常用的API是 xTaskCreate()。它的原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(Word)
void *pvParameters, // 传给任务函数的参数
UBaseType_t uxPriority, // 任务优先级(0 ~ configMAX_PRIORITIES-1)
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
嗯,这里有个坑,我必须要提醒你。栈深度单位是“字”,不是字节。在32位MCU上,一个字是4字节。所以如果你写 usStackDepth = 128,实际分配的栈空间是 128 × 4 = 512 字节。我曾经见过有人把这个当字节写,结果任务一跑就栈溢出,查了两天才找到原因。
返回值 pdPASS 表示创建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 表示堆空间不够。嗯,这个错误很常见,尤其是你开了很多任务或者栈给得太大时。
3.1.2 任务删除:vTaskDelete() 与善后工作
删除任务用 vTaskDelete()。传入任务句柄,内核就会把这个任务从所有状态列表中移除,并回收它的TCB(任务控制块)和栈空间。
void vTaskDelete(TaskHandle_t xTaskToDelete);
如果传入 NULL,表示删除当前正在运行的任务。这个用法很常见,比如任务执行完自己的使命后,自我了断:
void vTaskCode(void *pvParameters) {
// 做一些初始化工作
// ...
// 任务完成,删除自己
vTaskDelete(NULL);
}
3.2 任务状态机:就绪、运行、阻塞、挂起
FreeRTOS的任务有四种状态:就绪(Ready)、运行(Running)、阻塞(Blocked)、挂起(Suspended)。你想想看,一个CPU核心同一时刻只能运行一个任务,那其他任务去哪了?它们就在这些状态之间来回切换。
下面这张图,是我自己画的任务状态迁移图,你一看就明白了:
我来解释一下这张图里的关键迁移路径:
- 就绪 → 运行: 调度器从就绪列表中选一个优先级最高的任务,让它占用CPU。
- 运行 → 就绪: 要么是时间片到了(同优先级轮转),要么是更高优先级的任务就绪了(抢占)。
- 运行 → 阻塞: 任务主动调用
vTaskDelay(),或者等待某个队列/信号量。说白了,就是“我现在没事干,先睡会儿”。 - 阻塞 → 就绪: 等待的事件到了,或者延时时间到了。内核把任务从阻塞列表移回就绪列表。
- 运行/就绪 → 挂起: 调用
vTaskSuspend()。挂起态的任务,调度器完全忽略它。 - 挂起 → 就绪: 调用
vTaskResume()或xTaskResumeFromISR()。
3.3 任务优先级与调度:谁先跑,谁后跑?
FreeRTOS是一个抢占式优先级调度的RTOS。什么意思?说白了就是:高优先级的任务一旦就绪,立刻抢走CPU,低优先级的任务只能靠边站。
3.3.1 优先级数值与含义
优先级是一个整数,范围从 0 到 configMAX_PRIORITIES - 1。数值越大,优先级越高。这是FreeRTOS的约定,和uC/OS正好相反(uC/OS是数值越小优先级越高)。
| 优先级值 | 含义 | 典型用途 |
|---|---|---|
| 0 | 最低优先级(空闲任务) | 空闲任务、后台统计 |
| 1 ~ 3 | 低优先级 | 非实时性任务(如LED闪烁、按键扫描) |
| 4 ~ 6 | 中优先级 | 常规控制任务(如CAN报文处理) |
| 7 ~ (configMAX_PRIORITIES-1) | 高优先级 | 实时性要求高的任务(如电机控制、传感器采集) |
我建议你把 configMAX_PRIORITIES 设得不要太大,5~32就够用了。优先级太多,调度器的查找开销会变大,而且你也不容易管理。
3.3.2 调度策略:抢占 + 时间片轮转
FreeRTOS的调度策略可以概括为两句话:
- 抢占式调度: 任何时候,只要有一个更高优先级的任务就绪,当前运行的任务立刻被换下。这个切换发生在任何可能的内核点(比如tick中断、API调用)。
- 时间片轮转: 如果多个任务优先级相同,它们会轮流占用CPU,每个任务运行一个时间片(通常1个tick)。
举个例子,假设有三个任务:
- Task_A:优先级 5,一直运行
- Task_B:优先级 3,延时100ms后打印
- Task_C:优先级 5,一直运行
调度过程是这样的:
- 一开始,Task_A和Task_C都是优先级5,它们轮流跑时间片。
- 100ms后,Task_B延时结束,进入就绪态。但它的优先级是3,低于5,所以它抢不过Task_A和Task_C,只能等着。
- 只有当Task_A和Task_C都阻塞了(比如都调用了vTaskDelay),Task_B才有机会运行。
嗯,这里有个常见的误解:很多人以为高优先级任务就绪后,低优先级任务会立刻被暂停。其实不是的——抢占只发生在内核的调度点,比如tick中断、任务切换API调用时。如果你关掉了中断,或者在一个循环里没调用任何内核API,那高优先级任务就只能干等着。
taskYIELD(),问题就解决了。记住:别让同优先级或低优先级的任务长时间霸占CPU不放。
3.3.3 空闲任务与钩子函数
当所有任务都阻塞或挂起时,调度器会运行空闲任务(Idle Task)。空闲任务的优先级是0,最低。它主要做两件事:
- 回收被删除任务的TCB和栈空间
- 调用用户定义的钩子函数
vApplicationIdleHook()
你可以在空闲钩子里做低功耗处理,比如进入睡眠模式。但注意:空闲钩子里不能调用任何可能阻塞的API,否则系统会崩溃。
3.4 实战:创建一个两任务系统
光说不练假把式。咱们写个简单的例子,创建两个任务,一个高优先级负责采集数据,一个低优先级负责打印输出。
#include "FreeRTOS.h"
#include "task.h"
// 任务句柄
TaskHandle_t xTaskCollectHandle = NULL;
TaskHandle_t xTaskPrintHandle = NULL;
// 高优先级:数据采集任务
void vTaskCollect(void *pvParameters) {
uint32_t ulSensorValue = 0;
for (;;) {
// 模拟读取传感器
ulSensorValue++;
// 延时10ms,让出CPU给低优先级任务
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 低优先级:打印任务
void vTaskPrint(void *pvParameters) {
for (;;) {
// 打印信息
// 延时100ms
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void main(void) {
// 创建高优先级任务(优先级2)
xTaskCreate(vTaskCollect, "Collect", 128, NULL, 2, &xTaskCollectHandle);
// 创建低优先级任务(优先级1)
xTaskCreate(vTaskPrint, "Print", 128, NULL, 1, &xTaskPrintHandle);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
for (;;);
}
这个例子里,vTaskCollect 每10ms运行一次,vTaskPrint 每100ms运行一次。因为采集任务的优先级更高,所以只要它就绪,打印任务就得等着。但采集任务每次运行完就主动延时,给了打印任务执行的机会。
好了,这一章的内容就到这里。任务创建、状态机、优先级调度,这三块是FreeRTOS的骨架。你把这些搞清楚了,后面学队列、信号量、互斥锁时,就会觉得“哦,原来内核是这么帮我管理的”。