2. 任务管理与调度:任务状态机、任务创建与删除、优先级调度、时间片轮转
大家好,我是你们的老朋友。今天咱们聊聊RTOS里最核心的东西——任务管理与调度。说实话,我做了十几年运动控制,见过太多系统因为任务调度没搞好,电机跑着跑着就抽风了。你想想看,一个伺服驱动器里同时要处理电流环、速度环、位置环,还得兼顾通讯和故障保护,这活儿要是分不好,机器就得罢工。
2.1 任务状态机:任务的一生
每个RTOS任务,说白了就是一个无限循环的函数。但它不是一直在跑的,它有生命周期。我习惯把任务状态机比作一个人的一天:
- 就绪态(Ready):任务已经准备好,就等CPU来宠幸它。就像你早上起床洗漱完毕,等着出门上班。
- 运行态(Running):CPU正在执行这个任务。这就是你在工位上干活的状态。
- 阻塞态(Blocked):任务在等某个事件,比如等一个信号量、等消息队列。就像你等公交车,车不来你只能干等着。
- 挂起态(Suspended):任务被强制暂停,不参与调度。嗯,就像你被老板叫去喝茶,暂时不用干活了。
核心要点:任务状态切换是RTOS调度的基础。只有就绪态的任务才能被调度器选中运行。
我在项目中遇到过一个问题:有个同事把任务里加了个死循环while(1),但忘了加任何阻塞操作。结果这个任务一直占着CPU,其他任务全饿死了。电机直接飞车,吓得我赶紧按了急停。所以记住:任务里一定要有让出CPU的机制,比如延时、等待事件。
下面这张图展示了任务状态机的完整流转:
2.2 任务创建与删除
任务创建,说白了就是告诉RTOS:「嘿,我这有个函数,你帮我管着它。」在FreeRTOS里,我们用xTaskCreate()。在uC/OS里,用OSTaskCreate()。接口不同,但核心参数差不多:
// FreeRTOS 任务创建示例
TaskHandle_t motorControlTaskHandle = NULL;
void vMotorControlTask(void *pvParameters)
{
// 任务初始化
motor_init();
while(1)
{
// 执行电流环控制
current_loop();
// 执行速度环控制
speed_loop();
// 执行位置环控制
position_loop();
// 让出CPU,等待下一个控制周期
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms周期
}
}
// 在main函数中创建任务
xTaskCreate(
vMotorControlTask, // 任务函数
"Motor Control", // 任务名称
512, // 栈深度(字)
NULL, // 参数
5, // 优先级(0-31,数值越大优先级越高)
&motorControlTaskHandle // 任务句柄
);
我的经验:栈大小一定要留够余量。我一般会多给30%。曾经有个项目,栈大小算得刚刚好,结果加了个调试打印,栈溢出了,系统随机死机。查了三天才找到原因,血的教训啊。
任务删除呢?用vTaskDelete()。但要注意:千万别在中断里删除任务。为什么?因为删除任务要释放资源,中断上下文干不了这活。我曾经见过有人这么干,结果系统直接hard fault。
警告:任务删除后,它占用的堆栈不会自动回收。如果你频繁创建删除任务,小心内存碎片。运动控制系统中,我建议任务一旦创建,就让它一直活着,别折腾。
2.3 优先级调度
优先级调度,这是RTOS的看家本领。每个任务有个优先级,数值越小优先级越低(或者反过来,看具体RTOS)。调度器永远选择最高优先级的就绪任务来运行。
在运动控制里,优先级怎么分?我一般这么干:
| 优先级 | 任务类型 | 说明 |
|---|---|---|
| 最高(如15) | 电流环控制 | 微秒级响应,必须最高优先级 |
| 高(如10) | 速度环/位置环 | 毫秒级响应,仅次于电流环 |
| 中(如5) | 通讯任务(CAN/EtherCAT) | 实时性要求中等 |
| 低(如2) | 状态监测/日志 | 非实时,有空才跑 |
| 最低(如0) | 空闲任务 | 系统空闲时运行 |
这里有个坑:优先级反转。低优先级任务拿了锁,高优先级任务等锁,结果中优先级任务插进来把低优先级任务抢走了。高优先级任务反而被中优先级任务阻塞了。这问题在运动控制里很要命,会导致控制周期抖动。
解决方案:使用优先级继承协议,或者干脆用无锁编程。我在伺服驱动项目里,控制环任务从来不用锁,全靠消息队列和信号量来同步。
2.4 时间片轮转
时间片轮转,说白了就是让同优先级的任务轮流用CPU。每个任务分到一个时间片(比如10ms),时间到了就换下一个。这玩意儿在运动控制里用得不多,因为控制任务通常优先级不同。
但有一种情况会用:多个非实时任务共享CPU。比如你同时跑一个UI刷新任务和一个数据记录任务,优先级都是3。没有时间片轮转的话,一个任务可能一直占着CPU,另一个饿死。
// FreeRTOS 配置时间片轮转
// 在 FreeRTOSConfig.h 中
#define configUSE_TIME_SLICING 1 // 启用时间片轮转
#define configTIME_SLICE_TICKS 2 // 每个时间片2个tick(通常1 tick = 1ms)
// 创建两个同优先级任务
xTaskCreate(vTaskA, "TaskA", 256, NULL, 3, NULL);
xTaskCreate(vTaskB, "TaskB", 256, NULL, 3, NULL);
// 这两个任务会轮流执行,每个任务最多运行2ms
我的建议:时间片别设太短。太短的话,上下文切换开销占比太大。我一般设5-10ms。太长的话,交互响应会变慢。你想想看,一个任务跑50ms才切换,用户按个按钮得等半秒才有反应,这体验能好吗?
嗯,说到这里,我想起一个项目。当时做多轴同步控制,三个轴各有一个控制任务,优先级都是5。我开了时间片轮转,结果发现轴间同步误差很大。后来一查,原来是时间片切换导致控制周期抖动。最后我把三个控制任务合并成一个,用状态机来分时处理三个轴,问题就解决了。所以啊,时间片轮转不是万能的,在硬实时场景下要慎用。
2.5 总结一下
任务管理与调度,是RTOS的基石。你得搞清楚:
- 任务状态怎么切换?别让任务死占CPU。
- 优先级怎么分?控制环任务必须最高优先级。
- 时间片轮转什么时候用?同优先级非实时任务共享CPU时用。
最后送大家一句话:调度策略选对了,系统就成功了一半。另一半?嗯,那是中断管理和资源同步的事,咱们后面再聊。
公众号:蓝海资料掘金营,微信deep3321