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 后,闹钟响了,任务又回到就绪态,等待调度器再次选中它。

注意: 千万不要在阻塞态里做耗时操作!我曾经见过有人直接在阻塞态里写了个 while 循环等待标志位,结果整个系统都卡死了。阻塞态就是用来“等”的,不是用来“忙”的。

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)来保护资源,但开销比临界区大。

我个人习惯:对于简单的全局变量读写,直接用临界区。对于复杂的共享数据结构(比如环形缓冲区),才用互斥量或信号量。

避坑指南: 我曾经在一个项目里,为了图省事,在临界区里调用了 printf 打印调试信息。结果 printf 本身耗时很长,导致中断被长时间关闭,电机控制周期严重超时。从那以后,我养成了一个习惯:临界区里只做“读”或“写”操作,绝不干别的。

小结

这一章我们聊了 RTOS 的四个核心概念:

  • RTOS 是什么: 追求确定性的实时系统。
  • 任务与调度: 抢占式调度是电机控制的首选。
  • 任务状态机: 就绪、运行、阻塞、挂起,搞清它们的区别。
  • 临界区与资源保护: 保护共享数据,但别在里面干“重活”。

下一章,我们会深入 FreeRTOS 的具体 API,手把手教你创建第一个电机控制任务。到时候,咱们再聊。