2. 任务管理与调度:任务状态机、任务创建与删除、优先级调度、时间片轮转

大家好,我是你们的老朋友。今天咱们聊聊RTOS里最核心的东西——任务管理与调度。说实话,我做了十几年运动控制,见过太多系统因为任务调度没搞好,电机跑着跑着就抽风了。你想想看,一个伺服驱动器里同时要处理电流环、速度环、位置环,还得兼顾通讯和故障保护,这活儿要是分不好,机器就得罢工。

2.1 任务状态机:任务的一生

每个RTOS任务,说白了就是一个无限循环的函数。但它不是一直在跑的,它有生命周期。我习惯把任务状态机比作一个人的一天:

  • 就绪态(Ready):任务已经准备好,就等CPU来宠幸它。就像你早上起床洗漱完毕,等着出门上班。
  • 运行态(Running):CPU正在执行这个任务。这就是你在工位上干活的状态。
  • 阻塞态(Blocked):任务在等某个事件,比如等一个信号量、等消息队列。就像你等公交车,车不来你只能干等着。
  • 挂起态(Suspended):任务被强制暂停,不参与调度。嗯,就像你被老板叫去喝茶,暂时不用干活了。

核心要点:任务状态切换是RTOS调度的基础。只有就绪态的任务才能被调度器选中运行。

我在项目中遇到过一个问题:有个同事把任务里加了个死循环while(1),但忘了加任何阻塞操作。结果这个任务一直占着CPU,其他任务全饿死了。电机直接飞车,吓得我赶紧按了急停。所以记住:任务里一定要有让出CPU的机制,比如延时、等待事件。

下面这张图展示了任务状态机的完整流转:

就绪态 Ready 运行态 Running 阻塞态 Blocked 挂起态 Suspended 调度器选中 等待事件 强制挂起 事件到达 恢复 时间片耗尽 图:RTOS任务状态机流转图

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