第三章:任务管理与调度——让多个任务“有序共舞”
各位同学,欢迎来到第三章。这一章,我们要聊的是RTOS的核心——任务管理与调度。
说白了,任务管理就是回答三个问题:任务怎么生?任务怎么死?任务之间怎么抢CPU?
我在做第一个RTOS项目时,就踩过不少坑。比如任务优先级设反了,结果低优先级的电机控制任务一直抢不到CPU,电机直接“抖”起来了。嗯,那场面,挺尴尬的。
3.1 任务创建与删除
任务创建,就是给RTOS“生”一个任务。每个任务本质上是一个无限循环的函数,加上自己的栈空间和任务控制块(TCB)。
以FreeRTOS为例,创建任务的API长这样:
// 任务函数
void vMotorControlTask(void *pvParameters) {
while(1) {
// 读取编码器
// 计算PID
// 输出PWM
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms周期
}
}
// 创建任务
TaskHandle_t xMotorHandle = NULL;
xTaskCreate(
vMotorControlTask, // 任务函数
"Motor Control", // 任务名(调试用)
256, // 栈深度(单位:字)
NULL, // 参数
5, // 优先级(0最低,configMAX_PRIORITIES-1最高)
&xMotorHandle // 任务句柄
);
我的习惯:创建任务时,栈大小一定要留余量。我一般会多给20%。为什么?因为中断嵌套、函数调用链深了,栈可能爆掉。我曾经在量产前才发现某个任务的栈只差4个字节就溢出了...冷汗都出来了。
任务删除呢?就是让任务“退役”。
// 任务自己删除自己
vTaskDelete(NULL);
// 或者由其他任务删除
vTaskDelete(xMotorHandle);
注意:删除任务前,要确保它占用的资源(如动态分配的内存、信号量、队列等)已经释放。否则会造成资源泄漏。我见过一个同事删了任务但没释放互斥锁,结果其他任务永远等不到这个锁,系统直接“死锁”。
3.2 任务优先级与调度策略
RTOS的调度策略,说白了就是“谁有资格用CPU”。主要有两种:
3.2.1 抢占式调度
高优先级任务就绪时,立即打断低优先级任务。这是实时系统的灵魂。
举个例子:
- 任务A:优先级3,正在运行
- 任务B:优先级5,从阻塞态变为就绪态
- 结果:任务A被挂起,任务B立即运行
为什么会这样?因为RTOS要保证高优先级任务的实时性。你想想看,如果电机过流了,保护任务优先级最高,它必须立刻执行,不能等。
3.2.2 时间片调度
同优先级的任务,轮流使用CPU。每个任务运行一个时间片(通常1-10ms),然后切换给下一个。
我一般这样用:
- 高优先级任务:抢占式,用于实时性要求高的(如电流环、位置环)
- 低优先级任务:时间片,用于非实时性任务(如日志打印、状态显示)
避坑指南:我曾经把两个电机控制任务设为相同优先级,结果它们互相抢时间片,导致每个电机的控制周期都不稳定。后来我把它们设为不同优先级,用抢占式调度,问题就解决了。
3.3 任务状态切换
RTOS中的任务有四种状态:
| 状态 | 说明 | 典型场景 |
|---|---|---|
| 运行态 | 正在使用CPU | 当前正在执行的任务 |
| 就绪态 | 可以运行,但CPU被别的任务占着 | 等待高优先级任务释放CPU |
| 阻塞态 | 等待某个事件(延时、信号量、队列等) | vTaskDelay()、等待消息 |
| 挂起态 | 被vTaskSuspend()暂停,不参与调度 | 调试时暂停某个任务 |
状态切换图(用SVG绘制):
这张图我建议你记在脑子里。调试RTOS问题时,90%的bug都能通过分析任务状态切换找到原因。
3.4 空闲任务与钩子函数
空闲任务是RTOS自动创建的,优先级最低。当所有其他任务都阻塞或挂起时,空闲任务运行。
空闲任务能干啥?
- 统计CPU使用率
- 进入低功耗模式
- 做一些后台清理工作
钩子函数(Hook Function)是RTOS留给用户的“后门”。你可以注册一个函数,在空闲任务每次循环时被调用。
// 空闲任务钩子函数
void vApplicationIdleHook(void) {
// 进入低功耗模式
__WFI(); // Wait For Interrupt
}
// 在FreeRTOSConfig.h中启用
#define configUSE_IDLE_HOOK 1
我的经验:在运动控制系统中,我经常在空闲钩子里做两件事:
- 统计CPU负载——如果空闲任务运行时间占比低于10%,说明CPU快满了,需要优化
- 喂看门狗——确保系统不会因为任务卡死而复位
警告:不要在空闲钩子里调用任何可能阻塞的API(如vTaskDelay()、等待信号量等)。因为空闲任务本身不能阻塞,否则系统就“死”了。我曾经犯过这个错,结果系统一启动就卡死,查了半天才发现是空闲钩子里调了vTaskDelay()。
3.5 实战中的调度策略选择
我总结了一个简单的选择原则:
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 电流环(10-50kHz) | 最高优先级,抢占式 | 实时性要求极高,不能被打断 |
| 位置环(1-10kHz) | 次高优先级,抢占式 | 需要及时响应,但可容忍少量延迟 |
| 通信任务(CAN/以太网) | 中等优先级,抢占式 | 避免丢包,但可缓冲 |
| 日志打印 | 低优先级,时间片 | 不着急,慢慢来 |
| 人机界面 | 最低优先级,时间片 | 用户操作慢,不敏感 |
嗯,这一章的内容就到这里。任务管理是RTOS的基石,搞懂了它,后面的同步、通信、中断管理才能顺利展开。记住:优先级不是越高越好,合理分配才是王道。
公众号:蓝海资料掘金营,微信deep3321