第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的汇编代码时,头都大了。后来画了个流程图,才搞明白整个流程。
看明白了吗?核心就三步:保存现场 → 选任务 → 恢复现场。其中选任务这一步,FreeRTOS用的是优先级就绪位图,O(1)复杂度,效率极高。
1.5 实际项目中的调度策略选择
我做了十年嵌入式,总结出几条经验:
| 场景 | 推荐调度方式 | 原因 |
|---|---|---|
| 实时控制(电机、PID) | 抢占式 + 高优先级 | 必须保证控制周期不被打断 |
| 数据采集(传感器) | 抢占式 + 中等优先级 | 采集窗口有严格时间要求 |
| 人机交互(按键、显示) | 时间片轮转 | 响应慢点没关系,别卡死就行 |
| 后台任务(日志、统计) | 最低优先级 + 空闲钩子 | 有空就跑,没空拉倒 |
个人经验:我一般把实时性要求高的任务优先级设在3-5,普通任务设在1-2,空闲任务优先级0。这样既保证实时性,又不会让低优先级任务饿死。
嗯,调度机制这块就聊这么多。记住一句话:抢占式保证实时性,时间片保证公平性。两者结合,才能写出健壮的嵌入式系统。