2. 任务调度基础:FreeRTOS任务状态机、任务优先级与抢占机制、任务创建与删除的API
好,咱们进入正题。任务调度,说白了就是操作系统怎么安排这些任务轮流用CPU。你想想看,一个单片机只有一个核,但我们要同时处理传感器读取、控制算法、通信上报……怎么做到的?就是靠调度器来回切换。
我个人习惯,在讲任何RTOS之前,先让团队把任务状态机搞明白。状态机看不懂,后面优先级、抢占、API全是空中楼阁。
2.1 FreeRTOS任务状态机:任务的一生
一个任务从创建到删除,会经历几个状态。FreeRTOS里主要有四种:运行态、就绪态、阻塞态、挂起态。
我画个简图给你看:
创建任务 → 就绪态 ←→ 运行态
↓ ↓
阻塞态 挂起态
↑ ↓
超时/事件 恢复
嗯,这里要注意:一个时刻只有一个任务处于运行态。其他任务要么等着被调度(就绪态),要么在等某个事件(阻塞态),要么被我们主动挂起了。
我在项目中遇到过一个问题:有个新手把传感器读取任务里加了个vTaskDelay(1000),然后抱怨说“为什么我的任务不跑了?”——其实任务没死,只是进入了阻塞态,等1秒后自动回到就绪态。这就是状态机没理解透。
核心记忆点:
- 运行态 → 就绪态:被更高优先级任务抢占
- 运行态 → 阻塞态:主动调用延时或等待信号量
- 阻塞态 → 就绪态:延时结束或事件发生
- 运行态 → 挂起态:调用
vTaskSuspend()
2.2 任务优先级与抢占机制:谁先跑?
优先级,就是给任务排个队。数字越大,优先级越高。FreeRTOS支持从0到(configMAX_PRIORITIES - 1),一般配置成32或64级。
抢占机制是什么?说白了就是:高优先级任务一就绪,低优先级任务立马让路。
举个例子:
任务A:优先级2,负责姿态解算,每10ms执行一次
任务B:优先级1,负责LED闪烁,每100ms执行一次
假设任务B正在运行,突然任务A因为定时器触发了。这时候调度器会立刻暂停任务B,把CPU交给任务A。等任务A跑完(或者主动阻塞),任务B才能继续。
我曾经踩过一个坑:把通信任务优先级设得比控制任务还高。结果通信一忙,控制任务被频繁抢占,姿态解算周期抖动得厉害,飞控直接炸了。嗯,从那以后我定了个规矩:实时性要求越高的任务,优先级越高。
避坑指南:
我曾经见过一个团队,把所有任务优先级都设成一样。结果调度器只能按时间片轮转,关键任务得不到及时响应。记住:优先级是实时系统的命根子,别乱设。
2.3 任务创建与删除的API:动手实操
好,理论讲完了,咱们看看代码。任务创建最核心的API就一个:xTaskCreate()。
2.3.1 创建任务
函数原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名字(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字
void *pvParameters, // 传给任务的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t *pxCreatedTask // 任务句柄,用来删除或挂起
);
我习惯这么用:
TaskHandle_t xControlTaskHandle = NULL;
void vControlTask(void *pvParameters)
{
// 姿态控制循环
for(;;)
{
// 读取传感器
// 解算姿态
// 输出控制量
vTaskDelay(pdMS_TO_TICKS(5)); // 5ms周期
}
}
void main(void)
{
// 创建控制任务
xTaskCreate(
vControlTask,
"Control",
256, // 栈大小,单位是字
NULL,
3, // 优先级3
&xControlTaskHandle
);
// 启动调度器
vTaskStartScheduler();
}
这里有个细节:栈大小怎么定? 我一般先给个256字(1024字节),跑起来后用uxTaskGetStackHighWaterMark()看剩余量,再调整。别一开始就给太大,浪费RAM。
2.3.2 删除任务
删除任务用vTaskDelete()。参数传任务句柄,如果传NULL就是删除自己。
// 删除指定任务
vTaskDelete(xControlTaskHandle);
// 任务自己删除自己
vTaskDelete(NULL);
嗯,这里要注意:删除任务后,任务占用的栈和TCB会被释放,但任务自己申请的内存(比如malloc出来的)不会自动释放。我见过有人删任务前忘了free,内存泄漏了。
我的习惯:
创建任务时,我会在任务函数开头加个打印,确认栈大小够用。删除任务前,我会确保所有动态分配的内存已经释放。别嫌麻烦,嵌入式系统里内存泄漏查起来真要命。
2.4 实战中的调度策略选择
讲完基础,咱们聊聊实际项目中怎么用。FreeRTOS默认是抢占式调度,但也可以配成协作式调度。我99%的项目都用抢占式,因为实时性要求摆在那。
但抢占式有个问题:优先级反转。低优先级任务拿着高优先级任务需要的资源,高优先级任务反而被阻塞。解决方法是优先级继承或互斥量。这个后面章节会细讲,你先有个印象。
我总结一下任务调度的几个原则:
| 原则 | 说明 |
|---|---|
| 关键任务高优先级 | 控制、安全相关的任务优先级最高 |
| 避免优先级相同 | 相同优先级只能时间片轮转,实时性不可控 |
| 任务不要死循环 | 每个循环里至少有一个阻塞点(delay或等待事件) |
| 栈大小要留余量 | 至少留20%的余量,防止栈溢出 |
好了,这一章就到这里。任务调度是实时系统的骨架,搞懂了状态机和优先级,后面学信号量、队列、中断管理就轻松多了。下一章咱们聊聊任务间通信,那又是另一片天地。