第1章:任务调度机制——抢占式调度与时间片轮转

大家好,我是老李。做嵌入式这些年,我见过太多人在任务调度上栽跟头。说白了,调度就是操作系统决定「谁先跑、谁后跑」的规则。今天咱们就把FreeRTOS的调度机制彻底聊透。

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

抢占式调度,我习惯叫它「霸道总裁模式」。高优先级的任务一旦就绪,立马抢走CPU。低优先级的任务?乖乖等着吧。

我在项目中遇到过这么个事:一个数据采集任务优先级设低了,结果被通信任务一直抢占,采集窗口都过了还没轮到它跑。嗯,这就是典型的优先级设计翻车。

核心规则:

  • 高优先级任务就绪 → 立即抢占低优先级任务
  • 同优先级任务之间,按时间片轮转
  • 优先级数值越小,优先级越低(FreeRTOS默认)

你想想看,如果两个任务优先级不同,高优先级的任务永远不阻塞,低优先级的任务就永远没机会跑。这就是所谓的「饥饿」问题。我曾经调试过一个系统,LED闪烁任务就是被高优先级的传感器任务饿死的,灯死活不闪。

1.2 时间片轮转:公平的艺术

同优先级任务怎么分CPU?FreeRTOS用时间片轮转。每个任务跑一个tick,然后换下一个。这个tick长度由configTICK_RATE_HZ决定,默认是1000Hz,也就是1ms。

我个人习惯把时间片设成5ms或10ms。太短了切换开销大,太长了响应变慢。这个得根据实际需求调,没有银弹。

// 在FreeRTOSConfig.h中配置
#define configTICK_RATE_HZ      ( ( TickType_t ) 1000 )  // 1ms一个tick
#define configUSE_TIME_SLICING  1                        // 开启时间片轮转

避坑指南:我曾经把时间片设成0.5ms,结果任务切换太频繁,CPU一半时间都在做上下文切换。后来改成5ms,系统瞬间稳了。

1.3 调度器启动与挂起

调度器启动前,所有任务都是「待命」状态。调用vTaskStartScheduler()后,调度器才开始工作。这个函数不会返回,除非系统内存不够。

挂起调度器呢?用vTaskSuspendAll()。我一般在做批量数据更新时挂起调度器,防止任务切换导致数据不一致。但注意,挂起时间不能太长,否则实时性就没了。

void vTaskStartScheduler( void );   // 启动调度器
void vTaskSuspendAll( void );       // 挂起调度器
BaseType_t xTaskResumeAll( void );  // 恢复调度器

警告:挂起调度器期间,不能调用任何可能导致阻塞的API(如vTaskDelay、xQueueSend等)。我吃过这个亏,挂起后调了vTaskDelay,系统直接死机。

1.4 任务切换的底层原理

任务切换,说白了就是保存当前任务的上下文,加载下一个任务的上下文。上下文包括:CPU寄存器、堆栈指针、任务状态等。

FreeRTOS用PendSV异常来实现任务切换。为什么用PendSV?因为它优先级最低,可以等所有高优先级中断处理完再切换。这样就不会打断关键中断服务程序。

我记得第一次看PendSV的汇编代码时,头都大了。后来画了个流程图,才搞明白整个流程。

任务切换流程图 触发PendSV异常 保存当前任务上下文(入栈) 选择下一个要运行的任务 任务是否相同? 跳过切换 加载新任务上下文(出栈)

看明白了吗?核心就三步:保存现场 → 选任务 → 恢复现场。其中选任务这一步,FreeRTOS用的是优先级就绪位图,O(1)复杂度,效率极高。

1.5 实际项目中的调度策略选择

我做了十年嵌入式,总结出几条经验:

场景 推荐调度方式 原因
实时控制(电机、PID) 抢占式 + 高优先级 必须保证控制周期不被打断
数据采集(传感器) 抢占式 + 中等优先级 采集窗口有严格时间要求
人机交互(按键、显示) 时间片轮转 响应慢点没关系,别卡死就行
后台任务(日志、统计) 最低优先级 + 空闲钩子 有空就跑,没空拉倒

个人经验:我一般把实时性要求高的任务优先级设在3-5,普通任务设在1-2,空闲任务优先级0。这样既保证实时性,又不会让低优先级任务饿死。

嗯,调度机制这块就聊这么多。记住一句话:抢占式保证实时性,时间片保证公平性。两者结合,才能写出健壮的嵌入式系统。

专注资料整理