1. RTOS基础概念:什么是RTOS、任务与调度、任务状态机、临界区与资源保护
大家好,欢迎来到这门课的第一章。
说实话,很多工程师一上来就撸电机控制代码,结果发现系统一复杂,逻辑就乱成一锅粥。我当年也踩过这个坑。后来才明白,搞电机控制,尤其是多任务协同的场景,RTOS 是绕不开的基石。这一章,我们就先把地基打牢。
1.1 什么是RTOS?
RTOS,全称 Real-Time Operating System,实时操作系统。你想想看,它跟咱们电脑上的 Windows 或者 Linux 最大的区别在哪?
Windows 追求的是“平均性能好”,你点个鼠标,它可能几十毫秒后才响应,但你感觉不到。但 RTOS 追求的是“确定性”——它必须保证在给定的时间内,一定能把事情做完。说白了,就是“deadline 必须满足”。
我在做无刷电机 FOC 控制时,电流环的周期是 50 微秒。如果系统偶尔“卡”一下,电机就会抖,甚至失控。这时候,RTOS 的实时性就派上用场了。
- 通用OS(GPOS): 追求吞吐量,响应时间不确定。
- 实时OS(RTOS): 追求确定性,响应时间可预测。
RTOS 不是万能的。它不会让你的代码跑得更快,但它能让你的代码跑得更稳、更可控。
1.2 任务与调度
任务(Task),在 RTOS 里就是一段独立的程序,有自己的栈空间和优先级。你可以把它想象成一个“小工人”,专门负责干一件事。
比如在电机控制项目里,我通常会拆成这几个任务:
- 电流环任务: 最高优先级,50us 执行一次。
- 速度环任务: 中等优先级,1ms 执行一次。
- 通信任务: 低优先级,处理上位机指令。
- 看门狗喂狗任务: 最低优先级,闲着没事就喂一下。
调度器就是那个“包工头”,它决定哪个“小工人”该干活了。常见的调度策略有两种:
| 调度策略 | 特点 | 适用场景 |
|---|---|---|
| 抢占式调度 | 高优先级任务可以打断低优先级任务 | 电机控制等强实时场景 |
| 时间片轮转 | 同优先级任务轮流执行固定时间 | 人机交互、后台处理等 |
我个人习惯在电机控制里只用抢占式调度。为什么?因为电流环的优先级必须最高,它不能被任何东西打断。一旦被打断,电流波形就会畸变。
1.3 任务状态机
任务不是一直“活着”的。它有自己的生命周期。RTOS 里的任务状态机,说白了就是任务在不同状态之间跳来跳去。
常见的状态有四个:
- 就绪态: 任务准备好了,随时可以运行,但调度器还没选它。
- 运行态: 任务正在占用 CPU 执行代码。
- 阻塞态: 任务在等某个事件(比如延时、信号量、消息队列)。
- 挂起态: 任务被强制暂停,不参与调度。
嗯,这里要注意:很多初学者会把“阻塞”和“挂起”搞混。我简单解释一下:
- 阻塞: 任务主动“睡觉”,等闹钟响(延时)或等快递(信号量)。
- 挂起: 任务被“打晕”了,别人叫它它才醒。
举个例子,在电机控制里,速度环任务通常会这样跑:
void SpeedControlTask(void *param)
{
while(1)
{
// 1. 读取当前速度
speed = GetCurrentSpeed();
// 2. 计算新的电流指令
current_ref = PID_Calc(speed, target_speed);
// 3. 阻塞等待 1ms(让出CPU给其他任务)
vTaskDelay(pdMS_TO_TICKS(1));
}
}
你看,任务执行完一次计算后,就调用 vTaskDelay 主动进入阻塞态。这时候调度器就会去运行其他就绪的任务。1ms 后,闹钟响了,任务又回到就绪态,等待调度器再次选中它。
1.4 临界区与资源保护
临界区,听起来很高大上,其实说白了就是“不能被打断的代码段”。
你想想看,如果两个任务同时访问同一个全局变量,会发生什么?比如一个任务在写,另一个任务在读,读到的数据可能是一半新的、一半旧的——这就是“数据竞争”。
我举个真实的例子。在电机控制里,我们经常用一个全局变量来保存目标转速:
// 全局变量
int32_t g_target_speed = 0;
// 通信任务:更新目标转速
void CommTask(void *param)
{
while(1)
{
// 假设从串口收到新指令
g_target_speed = 1000; // 写入
vTaskDelay(10);
}
}
// 速度环任务:读取目标转速
void SpeedTask(void *param)
{
while(1)
{
int32_t speed = g_target_speed; // 读取
// ... 计算
vTaskDelay(1);
}
}
如果 g_target_speed 是 32 位的,在 8 位或 16 位单片机上,写入操作可能不是原子的。通信任务写到一半,速度环任务突然插进来读取,读到的就是一个“半残”的值。电机就会突然抽风一下。
怎么解决?用临界区保护:
// 通信任务
void CommTask(void *param)
{
while(1)
{
taskENTER_CRITICAL(); // 进入临界区,关中断
g_target_speed = 1000;
taskEXIT_CRITICAL(); // 退出临界区,开中断
vTaskDelay(10);
}
}
// 速度环任务
void SpeedTask(void *param)
{
while(1)
{
int32_t speed;
taskENTER_CRITICAL();
speed = g_target_speed;
taskEXIT_CRITICAL();
// ... 计算
vTaskDelay(1);
}
}
- 临界区本质上是“关中断”,所以执行时间必须极短(微秒级)。
- 不要在临界区里调用延时函数或阻塞操作!否则整个系统都会停摆。
- 除了关中断,还可以用互斥量(Mutex)来保护资源,但开销比临界区大。
我个人习惯:对于简单的全局变量读写,直接用临界区。对于复杂的共享数据结构(比如环形缓冲区),才用互斥量或信号量。
小结
这一章我们聊了 RTOS 的四个核心概念:
- RTOS 是什么: 追求确定性的实时系统。
- 任务与调度: 抢占式调度是电机控制的首选。
- 任务状态机: 就绪、运行、阻塞、挂起,搞清它们的区别。
- 临界区与资源保护: 保护共享数据,但别在里面干“重活”。
下一章,我们会深入 FreeRTOS 的具体 API,手把手教你创建第一个电机控制任务。到时候,咱们再聊。