4、任务调度机制:抢占式调度、时间片轮转、空闲任务、调度锁与临界区

各位同学,咱们今天聊聊RTOS里最核心的玩意儿——任务调度。说白了,调度就是操作系统决定「接下来该谁干活」的规则。你想想看,一个单片机里跑着好几个任务,CPU只有一个核,谁先跑、谁后跑、跑多久,总得有个说法吧?

我在做电机控制项目时,就遇到过调度不当导致电机抖动的惨痛教训。嗯,今天我把这块掰开揉碎了讲清楚。

4.1 抢占式调度:优先级说了算

抢占式调度,是RTOS最常用的调度方式。它的核心思想很简单:高优先级的任务可以打断低优先级任务的执行

举个例子。你正在写一个电机FOC控制任务,优先级设为5。同时还有一个LED闪烁任务,优先级设为2。当FOC任务就绪时,哪怕LED任务正跑到一半,CPU也得立刻停下来去执行FOC任务。这就是「抢占」。

关键点:抢占发生在任务切换点,比如系统滴答中断、任务主动挂起、或者更高优先级任务就绪时。

我个人习惯把电机控制这类实时性要求高的任务设为最高优先级。但要注意,优先级不要滥用。我曾经见过一个项目,把所有任务都设成同一个优先级,结果抢占式调度直接退化成协作式调度,系统响应一塌糊涂。

4.2 时间片轮转:公平是相对的

如果多个任务优先级相同,谁先跑?这时候就需要时间片轮转。

时间片轮转,说白了就是「每人分一个时间片,轮流来」。每个任务跑完一个时间片(比如1ms),就自动切换到下一个同优先级的任务。这样能保证同优先级任务都能得到CPU时间。

调度方式 适用场景 特点
抢占式 实时性要求高的任务 高优先级打断低优先级
时间片轮转 同优先级任务 公平分配CPU时间

我在做多轴电机同步控制时,三个轴的电流环任务优先级相同,就用时间片轮转来分配。每个轴跑200μs,轮流切换,效果还不错。但要注意,时间片太短会导致频繁切换,浪费CPU在上下文切换上;太长又会让其他任务等太久。

小技巧:时间片一般设为系统滴答周期的1~5倍。比如系统滴答是1ms,时间片可以设成1ms或2ms。

4.3 空闲任务:CPU的「摸鱼时间」

当所有任务都处于阻塞或挂起状态时,CPU总不能空转吧?这时候空闲任务就登场了。

空闲任务是RTOS自动创建的一个最低优先级任务。它永远处于就绪状态,只有当没有其他任务可运行时,才会执行空闲任务。

空闲任务能干什么?

  • 进入低功耗模式:比如STM32的WFI指令,让CPU休眠省电
  • 统计CPU利用率:通过计算空闲任务运行时间占比,反推CPU负载
  • 系统自检:做一些后台的检查工作

我记得有一次做电池供电的电机控制器,客户要求待机功耗低于1mA。我就是在空闲任务里调用了WFI指令,配合外设时钟门控,硬是把功耗从5mA降到了0.8mA。嗯,这里要注意,空闲任务里不要做耗时操作,否则会影响系统响应。

4.4 调度锁与临界区:保护共享资源

调度锁和临界区,是RTOS里用来保护共享资源的机制。说白了,就是防止多个任务同时访问同一块数据,导致数据错乱。

4.4.1 调度锁

调度锁的作用是禁止任务切换。一旦上了调度锁,当前任务就会一直运行,直到解锁。其他任务即使优先级更高,也无法抢占。

// 示例:使用调度锁保护共享数据
taskENTER_CRITICAL();  // 进入临界区(关中断)
// 操作共享变量,比如电机速度设定值
motor_speed_setpoint = 1000;
taskEXIT_CRITICAL();   // 退出临界区(开中断)

警告:调度锁内不要做耗时操作!我曾经在调度锁里做了一次ADC采样,结果导致高优先级的中断响应延迟了200μs,电机直接失步。血的教训。

4.4.2 临界区

临界区比调度锁更狠——它直接关中断。在临界区内,连中断服务程序都无法执行。这通常用于保护极短的操作,比如修改一个全局变量。

我个人习惯:

  • 保护普通共享变量:用调度锁(不影响中断响应)
  • 保护中断与任务共享的变量:用临界区(必须关中断)

举个例子。电机控制中,PWM中断会更新电流采样值,而控制任务会读取这个值。如果不用临界区保护,任务读到一半时中断来了,数据就乱了。

// 中断中更新数据
void PWM_IRQHandler(void) {
    current_measure = adc_value;  // 中断中直接写
}

// 任务中读取数据
void motor_control_task(void) {
    uint32_t temp;
    taskENTER_CRITICAL();
    temp = current_measure;  // 关中断读取,防止被中断打断
    taskEXIT_CRITICAL();
    // 使用temp进行计算
}

4.5 避坑指南

做电机控制这么多年,我总结了几条调度相关的坑:

  1. 优先级反转:低优先级任务持有锁,高优先级任务等锁,中间优先级任务趁机运行。结果高优先级任务反而被低优先级任务「拖死」。解决办法是使用优先级继承协议。
  2. 死锁:两个任务互相等待对方释放资源。我曾经在调试双轴同步时遇到过,两个任务各持一把锁等另一把,系统直接卡死。后来统一了锁的获取顺序才解决。
  3. 临界区过长:关中断时间超过10μs,就可能影响电机控制的PWM周期。我建议临界区里只做赋值操作,别做计算。

总结一下:

  • 抢占式调度:高优先级说了算,适合实时任务
  • 时间片轮转:同优先级公平分配,适合非实时任务
  • 空闲任务:CPU摸鱼时间,用来省电或统计
  • 调度锁:禁止任务切换,保护共享数据
  • 临界区:关中断,保护中断与任务共享的数据

好了,调度机制这块就讲到这里。下一章咱们聊聊任务间通信,信号量和消息队列怎么用。到时候我会拿电机控制中的实际案例来讲,保证你听完就能用上。