1. RTOS基础概念:什么是RTOS、前后台系统与RTOS对比、任务与调度器、临界区与资源共享

大家好,我是你们的制氧机固件讲师。今天咱们聊聊RTOS最基础的东西。说实话,很多工程师做了好几年嵌入式,对RTOS的理解还停留在「用了FreeRTOS就是RTOS」这个层面。嗯,这其实远远不够。

1.1 什么是RTOS?

RTOS,全称Real-Time Operating System,实时操作系统。注意这个「实时」两个字——不是指跑得快,而是指响应时间可预测

我打个比方:你按下制氧机的启动按钮,系统必须在100毫秒内启动风机、打开电磁阀。如果有时候50毫秒响应,有时候500毫秒才响应,那就不叫实时。RTOS要保证的是:最坏情况下的响应时间,也是确定的

核心要点:RTOS ≠ 跑得快,RTOS = 响应时间可预测

我在做第一代便携制氧机时,用的就是前后台系统。结果呢?有一次用户把机器放在颠簸的车上,按键抖动处理加上LCD刷新,把风机PWM的响应硬生生拖慢了200毫秒。氧浓度直接从93%掉到85%。从那以后,我所有医疗级产品都上了RTOS。

1.2 前后台系统 vs RTOS

前后台系统,也叫超级循环(Super Loop)。结构很简单:

void main(void)
{
    while(1)
    {
        // 前台:处理按键
        if(Key_Scan())
        {
            Handle_Key();
        }
        
        // 前台:处理传感器
        Sensor_Read();
        
        // 前台:更新显示
        LCD_Update();
        
        // 后台:中断服务
        // 比如定时器中断、UART中断
    }
}

你看,所有任务排着队执行。如果某个任务卡住了,后面的全得等着。我遇到过最典型的问题:制氧机的流量传感器用I2C读取,I2C通信偶尔会因为噪声卡住几百微秒。就这几百微秒,风机的PID控制周期就错过了,氧浓度开始波动。

RTOS怎么解决?把每个功能拆成独立任务:

// 任务1:风机PID控制(优先级最高)
void Task_FanControl(void *pvParameters)
{
    while(1)
    {
        PID_Calculate();
        Fan_SetDuty();
        vTaskDelay(10);  // 10ms周期
    }
}

// 任务2:传感器采集(优先级中等)
void Task_SensorRead(void *pvParameters)
{
    while(1)
    {
        Flow_Read();
        Pressure_Read();
        vTaskDelay(50);  // 50ms周期
    }
}

// 任务3:人机交互(优先级最低)
void Task_HMI(void *pvParameters)
{
    while(1)
    {
        Key_Process();
        LCD_Update();
        vTaskDelay(100); // 100ms周期
    }
}

我习惯把关键任务(风机控制、氧浓度监测)放在高优先级,非关键任务(显示、日志)放低优先级。这样即使传感器偶尔卡住,风机控制也不会受影响。

对比项 前后台系统 RTOS
任务隔离 无,所有代码共享CPU 有,每个任务独立栈空间
响应时间 不确定,取决于循环长度 确定,由优先级决定
代码复杂度 低,适合简单系统 中高,需要学习成本
调试难度 低,单线程思维 中,需要处理竞态
适用场景 LED闪烁、简单遥控器 制氧机、医疗设备、工业控制

我的建议:如果你的系统只有3-5个功能,且对实时性要求不高,前后台完全够用。但制氧机涉及生命安全,我建议直接上RTOS。

1.3 任务与调度器

任务(Task),在RTOS里就是一段无限循环的函数。每个任务有自己的栈空间、优先级、状态。

任务有四种状态:

  • 运行态:正在使用CPU
  • 就绪态:可以运行,但CPU被别的任务占着
  • 阻塞态:等待某个事件(延时、信号量、队列)
  • 挂起态:被其他任务或中断挂起

调度器(Scheduler)是RTOS的大脑。它决定「下一个该谁跑」。常见的调度策略有两种:

  1. 抢占式调度:高优先级任务就绪时,立即打断低优先级任务。我用的都是这种。
  2. 协作式调度:任务主动让出CPU。嗯,这个在制氧机上我从来不用——万一哪个任务忘了让出,系统就卡死了。

举个例子,制氧机里三个任务同时就绪:

优先级: 风机控制(5) > 传感器采集(3) > 人机交互(1)

时间轴:
t=0: 三个任务都就绪
t=0~10ms: 风机控制运行(优先级最高)
t=10ms: 风机控制调用vTaskDelay(10),进入阻塞
t=10~60ms: 传感器采集运行(次高优先级)
t=60ms: 传感器采集调用vTaskDelay(50),进入阻塞
t=60~160ms: 人机交互运行(最低优先级)
t=160ms: 三个任务都重新就绪,风机控制再次抢占

你看,高优先级的任务永远先跑。这就是抢占式调度的核心思想。

避坑指南:我曾经犯过一个错误——把LCD刷新任务优先级设得太高。结果LCD刷新时占用了大量CPU时间,导致风机PID控制周期抖动。记住:优先级越高,不代表越重要,而是代表对实时性要求越高

1.4 临界区与资源共享

临界区(Critical Section),说白了就是「这段代码不能被中断」。为什么需要?因为多个任务可能同时访问同一个资源。

举个制氧机里的真实例子:

// 全局变量,记录累计供氧时间
uint32_t g_totalRunTime = 0;

// 任务A:每1秒更新一次
void Task_Timer(void *pvParameters)
{
    while(1)
    {
        g_totalRunTime++;
        vTaskDelay(1000);
    }
}

// 任务B:用户查询时显示
void Task_Display(void *pvParameters)
{
    while(1)
    {
        if(Query_Button_Pressed())
        {
            // 这里读取g_totalRunTime
            uint32_t time = g_totalRunTime;
            LCD_ShowNumber(time);
        }
        vTaskDelay(100);
    }
}

问题在哪?如果Task_Timer正在执行g_totalRunTime++(这其实是三步:读-加-写),Task_Display突然抢占了CPU,读到的就是错误的值。

解决方案:用临界区保护共享资源。

// 方法1:关中断(最快,但影响实时性)
void Task_Timer(void *pvParameters)
{
    while(1)
    {
        taskENTER_CRITICAL();
        g_totalRunTime++;
        taskEXIT_CRITICAL();
        vTaskDelay(1000);
    }
}

// 方法2:用信号量(更优雅)
SemaphoreHandle_t xRunTimeSem;

void Task_Timer(void *pvParameters)
{
    while(1)
    {
        xSemaphoreTake(xRunTimeSem, portMAX_DELAY);
        g_totalRunTime++;
        xSemaphoreGive(xRunTimeSem);
        vTaskDelay(1000);
    }
}

我个人习惯:能用信号量就别关中断。关中断会影响系统实时性,特别是对定时器精度要求高的场合。制氧机里PWM周期是微秒级的,关中断超过10微秒就可能出问题。

经验之谈:临界区越小越好。我见过有人把整个任务函数都包在临界区里——那还不如用前后台系统呢!临界区只保护「读-改-写」那几行代码就够了。

资源共享还有几个常见机制:

  • 互斥量(Mutex):带优先级继承的信号量,防止优先级反转
  • 队列(Queue):任务间传递数据,我常用它传递传感器数据
  • 事件组(Event Group):多个条件同时满足时触发任务

举个例子,制氧机里传感器数据传递:

// 创建队列,存放10个传感器数据
QueueHandle_t xSensorQueue = xQueueCreate(10, sizeof(SensorData_t));

// 采集任务:发送数据
void Task_Sensor(void *pvParameters)
{
    SensorData_t data;
    while(1)
    {
        data.flow = Flow_Read();
        data.pressure = Pressure_Read();
        xQueueSend(xSensorQueue, &data, 0);
        vTaskDelay(50);
    }
}

// 控制任务:接收数据
void Task_Control(void *pvParameters)
{
    SensorData_t data;
    while(1)
    {
        if(xQueueReceive(xSensorQueue, &data, 100))
        {
            PID_Update(data.flow, data.pressure);
        }
        vTaskDelay(10);
    }
}

这样设计的好处是:采集任务和控制任务完全解耦。即使采集偶尔卡顿,控制任务也能从队列里拿到最近的数据。

总结一下今天的内容:

  • RTOS的核心是「响应时间可预测」,不是「跑得快」
  • 前后台系统适合简单场景,制氧机这种医疗设备必须上RTOS
  • 调度器决定谁跑,优先级决定谁先跑
  • 临界区保护共享资源,但越小越好

下一章我会讲任务状态机设计——制氧机里那些「待机→启动→运行→报警→停机」的状态切换,怎么用状态机优雅地实现。到时候我会拿我踩过的坑当反面教材,保证你们印象深刻。