任务与调度器:让单片机学会“多线程”思考
各位同学,欢迎来到第三章。前两章我们搭好了RTOS的骨架——临界区和链表。现在,是时候给它注入灵魂了。
这一章,我们要聊的是RTOS最核心的两个概念:任务和调度器。说白了,就是让单片机看起来能“同时”干好几件事。你想想看,一个单核CPU,怎么做到一边刷屏、一边读按键、一边处理串口数据?嗯,答案就在任务切换里。
任务到底是什么?——TCB、栈、优先级
在RTOS的世界里,任务不是一个函数,而是一个“执行上下文”。我习惯把任务想象成一个独立的小人,他有自己的状态、自己的记忆、自己的优先级。
每个任务背后,站着三个核心数据结构:
- TCB(任务控制块):任务的身份证。里面存了栈指针、优先级、状态、名字等。调度器就是靠TCB来管理所有任务的。
- 任务栈:任务的私人记事本。函数调用、局部变量、中断现场,全记在这里。栈大小设错了,系统就崩了。我在项目中遇到过,栈设小了,任务跑着跑着就莫名其妙死机,查了两天才发现是栈溢出。
- 优先级:任务的“插队权”。数值越小,优先级越高(有些RTOS相反,看具体实现)。高优先级的任务先跑,跑完了才轮到低优先级的。
核心要点:没有TCB,调度器就不知道任务的存在。没有栈,任务就没法恢复执行。没有优先级,调度器就不知道先服务谁。
调度器的工作原理——谁先跑,谁后跑?
调度器,就是RTOS的大脑。它决定下一个该执行哪个任务。常见的调度策略有两种:
1. 时间片轮转(Round-Robin)
每个任务轮流跑一小段时间,时间到了就换下一个。大家机会均等,谁也不占便宜。适合那些“大家差不多重要”的场景。
但说实话,我在实际项目中很少只用时间片轮转。因为嵌入式系统里,任务的重要性往往不一样。比如,电机控制任务比LED闪烁任务重要得多。你让它们平分时间?电机可能会抖。
2. 抢占式调度(Preemptive Scheduling)
这才是RTOS的杀手锏。高优先级的任务可以随时“抢”低优先级任务的CPU使用权。低优先级的任务?乖乖等着吧。
举个例子:你正在写一个低优先级的日志打印任务,突然来了一个高优先级的按键中断。抢占式调度会立刻暂停日志任务,让按键任务先跑。等按键任务处理完了,再回来继续打印日志。
避坑指南:我曾经在一个项目中,把两个任务的优先级设反了。结果高优先级的任务一直占着CPU,低优先级的任务永远得不到执行。系统看起来没死,但该干的事一件没干。嗯,这就是传说中的“优先级反转”的雏形。
创建第一个RTOS任务——vTaskCreate
理论说完了,咱们来点实际的。在FreeRTOS中,创建任务的API是vTaskCreate。它的原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名字(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度(单位是字,不是字节)
void *pvParameters, // 任务参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t *pxCreatedTask // 任务句柄(输出参数)
);
参数看着多,其实用起来很简单。我给你写个例子:
void vTask1(void *pvParameters)
{
while(1)
{
// 任务代码
printf("Task 1 is running\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1秒
}
}
void vTask2(void *pvParameters)
{
while(1)
{
printf("Task 2 is running\n");
vTaskDelay(pdMS_TO_TICKS(500)); // 延时0.5秒
}
}
void main(void)
{
// 初始化硬件
// ...
// 创建两个任务
xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
xTaskCreate(vTask2, "Task2", 128, NULL, 2, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
while(1);
}
你看,创建任务就这么简单。两个任务,一个优先级1,一个优先级2。优先级2的任务会抢占优先级1的任务。运行起来,你会看到Task2打印的频率比Task1快一倍。
个人经验:我建议新手在创建任务时,先给每个任务分配128字的栈。跑起来后,用uxTaskGetStackHighWaterMark()看看实际用了多少。然后根据实际情况调整。别一开始就给很大,浪费内存。
体验多任务并发——眼见为实
理论再漂亮,不如亲眼看到效果。我建议你拿一块开发板,把上面的代码烧进去。打开串口助手,你会看到:
- Task1每隔1秒打印一次
- Task2每隔0.5秒打印一次
- 两个任务“同时”在跑,互不干扰
这就是多任务并发的魅力。虽然底层是CPU在快速切换,但看起来就像有两个独立的“大脑”在干活。
你可能会问:“那如果两个任务同时要访问同一个全局变量怎么办?”嗯,这个问题问得好。这就是我们下一章要讲的——同步与互斥。到时候我会告诉你,怎么用信号量和互斥锁来避免数据打架。
好了,这一章的内容就到这里。记住:任务和调度器是RTOS的基石。把这两个概念吃透了,后面的内容就会轻松很多。下一章,我们聊聊任务状态和切换的细节。