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 避坑指南
做电机控制这么多年,我总结了几条调度相关的坑:
- 优先级反转:低优先级任务持有锁,高优先级任务等锁,中间优先级任务趁机运行。结果高优先级任务反而被低优先级任务「拖死」。解决办法是使用优先级继承协议。
- 死锁:两个任务互相等待对方释放资源。我曾经在调试双轴同步时遇到过,两个任务各持一把锁等另一把,系统直接卡死。后来统一了锁的获取顺序才解决。
- 临界区过长:关中断时间超过10μs,就可能影响电机控制的PWM周期。我建议临界区里只做赋值操作,别做计算。
总结一下:
- 抢占式调度:高优先级说了算,适合实时任务
- 时间片轮转:同优先级公平分配,适合非实时任务
- 空闲任务:CPU摸鱼时间,用来省电或统计
- 调度锁:禁止任务切换,保护共享数据
- 临界区:关中断,保护中断与任务共享的数据
好了,调度机制这块就讲到这里。下一章咱们聊聊任务间通信,信号量和消息队列怎么用。到时候我会拿电机控制中的实际案例来讲,保证你听完就能用上。