第三章 任务调度机制:抢占式调度原理、时间片轮转、调度器启动与挂起、空闲任务与钩子函数
各位同学,咱们今天聊聊FreeRTOS的核心——任务调度。说白了,调度器就是操作系统的“大脑”,它决定哪个任务在什么时候运行。我做了这么多年嵌入式,见过太多因为调度没搞明白而翻车的项目。嗯,咱们今天就把这块彻底吃透。
3.1 抢占式调度原理
抢占式调度,这个名字听起来挺唬人。其实说白了就是:高优先级的任务可以随时打断低优先级任务的执行。你想想看,就像你在写代码,老板突然来了个紧急需求,你得立刻放下手头的工作去处理。这就是“抢占”。
在FreeRTOS里,每个任务都有一个优先级。数值越大,优先级越高。我习惯把优先级范围控制在0到configMAX_PRIORITIES-1之间。注意,0是最低优先级,一般留给空闲任务用。
核心机制:
- 每次系统节拍中断(SysTick)到来时,调度器都会检查是否有更高优先级的任务就绪
- 如果有,立即保存当前任务的上下文,切换到高优先级任务
- 高优先级任务运行直到主动阻塞(比如等待信号量、延时等),或者被更高优先级的任务抢占
我在项目中遇到过一个问题:两个任务共享一个串口,高优先级任务疯狂打印,低优先级任务根本拿不到CPU。这就是典型的“优先级反转”前兆。解决办法?要么给串口加互斥量,要么调整优先级设计。
3.2 时间片轮转
时间片轮转,是给相同优先级的任务用的。你想想看,如果两个任务优先级一样,谁先跑?FreeRTOS的做法是:每人分一个时间片,轮流来。
时间片长度由configTICK_RATE_HZ决定。比如系统节拍是1000Hz,那一个时间片就是1ms。我个人习惯把这个值设在100到1000之间,太短了切换开销大,太长了实时性差。
// 创建两个相同优先级的任务
xTaskCreate(vTask1, "Task1", 1000, NULL, 2, NULL);
xTaskCreate(vTask2, "Task2", 1000, NULL, 2, NULL);
void vTask1(void *pvParameters) {
for(;;) {
// 每个时间片执行一次
vTaskDelay(1); // 主动让出CPU
}
}
注意,时间片轮转只在同优先级任务之间生效。如果有一个更高优先级的任务就绪,它会立刻抢走CPU。嗯,这里要注意:不要指望时间片轮转来做精确的时序控制,它只能保证公平性。
我的经验:在电机控制中,我通常把控制任务设为最高优先级,通信任务次之,显示任务最低。这样能保证控制环的实时性,不会被其他任务干扰。
3.3 调度器启动与挂起
调度器的启动,是系统从“单任务模式”切换到“多任务模式”的关键一步。调用vTaskStartScheduler()后,FreeRTOS会创建空闲任务,然后开始调度。
我曾经犯过一个低级错误:在启动调度器之前就调用了vTaskDelay()。结果呢?系统直接卡死。因为调度器没启动,没有节拍中断,延时函数根本不会返回。
int main(void) {
// 硬件初始化
prvSetupHardware();
// 创建任务
xTaskCreate(vControlTask, "Control", 200, NULL, 3, NULL);
xTaskCreate(vCommTask, "Comm", 200, NULL, 2, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
for(;;);
}
调度器挂起,用vTaskSuspendAll()。这个函数会暂停所有任务调度,但不会停止中断。我一般在做临界区保护时用它,比如批量修改多个任务的状态。
注意:调度器挂起期间,不能调用任何可能导致任务切换的API,比如vTaskDelay()、xQueueSend()等。否则会触发断言失败。我曾经因为这个排查了一整天,最后发现是在挂起期间调用了vTaskDelay()。
3.4 空闲任务与钩子函数
空闲任务是FreeRTOS自动创建的,优先级为0。当没有其他任务就绪时,系统就运行空闲任务。它的主要工作是:回收被删除任务的资源。
空闲任务钩子函数,是一个可选的回调函数。你可以在空闲任务里干点“杂活”,比如:
- 统计CPU利用率
- 进入低功耗模式
- 喂看门狗
- 做一些后台维护工作
// 启用空闲任务钩子
// 在FreeRTOSConfig.h中设置:
#define configUSE_IDLE_HOOK 1
// 实现钩子函数
void vApplicationIdleHook(void) {
// 进入低功耗模式
__WFI(); // Wait For Interrupt
}
我习惯在空闲钩子里做CPU利用率统计。方法很简单:用一个计数器,在空闲钩子里累加,然后在某个任务里读取这个值。这样就能知道系统有多“闲”。
避坑指南:
- 空闲钩子函数里不能调用任何可能导致任务阻塞的API
- 钩子函数执行时间要尽量短,否则会影响系统响应
- 如果使用了低功耗模式,要确保中断能唤醒系统
我曾经在一个电池供电的项目里,把空闲钩子用来做深度睡眠。系统空闲时自动进入低功耗模式,中断来了再唤醒。这样电池续航从原来的8小时延长到了48小时。嗯,效果很明显。
3.5 知识体系总览
下面这张图,是我梳理的本章知识结构。你可以把它当作一个“调度机制速查表”。
这张图把四个核心模块的关系理清楚了。你想想看,抢占式调度决定了“谁先跑”,时间片轮转决定了“同优先级怎么跑”,调度器启动挂起控制了“什么时候跑”,空闲任务则负责“没人跑时干什么”。四个模块配合起来,就构成了FreeRTOS完整的任务调度体系。
最后说一句:调度机制是操作系统的灵魂。我建议你在实际项目中,先用简单的任务测试调度行为,确认符合预期后再上复杂逻辑。别问我怎么知道的——都是血泪教训。
公众号:蓝海资料掘金营,微信deep3321