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

1.1 什么是RTOS?

RTOS,全称是Real-Time Operating System,实时操作系统。说白了,它就是一个能保证在确定时间内完成特定任务的系统。

我经常跟刚入行的朋友说:RTOS不是跑得快,而是跑得准。普通Linux追求的是平均响应快,RTOS追求的是最坏情况下的响应时间可控。举个例子,你按下温室灌溉按钮,RTOS能保证在5毫秒内启动水泵,不管系统当时有多忙。

RTOS的核心特征:

  • 确定性(Determinism):任务响应时间可预测,有上限
  • 多任务并发:看起来多个任务同时运行,实际上是快速切换
  • 优先级驱动:高优先级任务优先获得CPU使用权
  • 抢占式调度:高优先级任务可以打断低优先级任务

我在一个温室项目中遇到过这样的场景:温度传感器每100ms采集一次数据,同时还要处理按键输入和LCD刷新。如果用前后台系统,按键响应延迟可能达到几百毫秒。换成RTOS后,我把温度采集设为最高优先级,按键处理次之,LCD刷新最低。嗯,问题就解决了。

1.2 前后台系统 vs RTOS

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

void main(void)
{
    // 硬件初始化
    System_Init();
    
    // 后台:无限循环
    while(1)
    {
        // 轮询各个任务
        Task_Temperature();   // 温度采集
        Task_KeyScan();       // 按键扫描
        Task_LCD_Update();    // LCD刷新
        Task_Valve_Control(); // 阀门控制
    }
}

你想想看,这个循环里如果某个任务执行时间长了,后面的任务就得等着。比如温度采集函数里有个延时等待传感器稳定,那按键扫描就被堵住了。

我刚开始做嵌入式开发时,就吃过这个亏。一个温室控制器,按键响应延迟有时候达到2秒,客户投诉说按了没反应。查了半天,原来是温度传感器初始化时有个500ms的延时,刚好卡在按键扫描前面。

对比项 前后台系统 RTOS
任务调度 顺序轮询,无优先级 抢占式,支持优先级
响应时间 不确定,依赖循环长度 确定,可预测
代码复杂度 简单,适合小系统 中等,需要学习成本
资源开销 极低,几乎无额外开销 需要RAM/ROM存放任务栈和内核
任务间通信 全局变量,容易出错 队列、信号量、互斥量等
适用场景 简单控制,任务少于5个 复杂系统,任务多,实时性要求高

我的建议:如果项目只有3-5个简单任务,且实时性要求不高,前后台系统完全够用。但一旦任务超过5个,或者有严格的时序要求,直接上RTOS。别犹豫,犹豫就会败北。

1.3 任务与调度器

任务(Task),在RTOS里也叫线程(Thread)。它是一个无限循环的函数,有自己的栈空间和优先级。

// 一个典型的RTOS任务
void Task_Temperature(void *param)
{
    while(1)
    {
        // 读取温度传感器
        uint16_t adc_val = ADC_Read(CHANNEL_TEMP);
        float temp = ADC_To_Temperature(adc_val);
        
        // 判断是否需要报警
        if(temp > TEMP_THRESHOLD)
        {
            Alarm_Set(ALARM_OVERHEAT);
        }
        
        // 延时100ms,让出CPU
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

调度器(Scheduler)是RTOS的大脑。它决定哪个任务该运行,运行多久。常见的调度策略有两种:

  • 抢占式调度:高优先级任务就绪时,立即抢占低优先级任务的CPU。这是最常用的方式。
  • 时间片轮转:同优先级任务轮流运行,每个任务分到一个时间片。

为什么会这样设计?你想想看,温室里温度传感器采集数据是最高优先级的,因为温度失控可能导致作物死亡。而LCD显示刷新可以慢一点,优先级低一些。如果不用抢占式,温度采集任务可能被其他任务阻塞,那就危险了。

注意:我曾经在一个项目中把两个任务的优先级设反了——把LCD刷新设成了最高优先级,温度采集反而低了。结果温度传感器数据采集不及时,导致加热器过冲了5度。虽然没造成大问题,但那次教训让我记住了:优先级设计要基于任务的实时性需求,而不是代码的复杂程度

1.4 临界区与资源保护

临界区(Critical Section),就是一段不能被打断的代码。比如你在更新一个全局变量时,如果被另一个任务打断,数据就可能出错。

看这个例子:

// 共享资源:温室温度设定值
int g_target_temp = 25;

// 任务1:用户通过按键调整温度
void Task_Key_Control(void *param)
{
    while(1)
    {
        if(Key_Get(KEY_UP))
        {
            // 这里可能被任务2打断
            g_target_temp++;
        }
        vTaskDelay(10);
    }
}

// 任务2:PID控制器读取温度设定值
void Task_PID_Control(void *param)
{
    while(1)
    {
        // 这里可能被任务1打断
        int temp = g_target_temp;
        PID_Calculate(temp);
        vTaskDelay(50);
    }
}

问题在哪?如果任务1执行到一半(刚读完g_target_temp,还没写回),任务2抢占了CPU,读取了旧值。等任务1恢复执行,写回新值。这时候任务2拿到的就是错误数据。

解决办法就是临界区保护。RTOS提供了几种机制:

  • 关中断:最简单粗暴,但会影响系统实时性
  • 互斥量(Mutex):专门保护共享资源,支持优先级继承
  • 信号量(Semaphore):更通用的同步机制
  • 任务通知(Task Notification):轻量级同步,适合简单场景
// 使用互斥量保护共享资源
SemaphoreHandle_t xMutex;

void Task_Key_Control(void *param)
{
    while(1)
    {
        if(Key_Get(KEY_UP))
        {
            // 获取互斥量
            if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE)
            {
                g_target_temp++;  // 安全操作
                xSemaphoreGive(xMutex);  // 释放互斥量
            }
        }
        vTaskDelay(10);
    }
}

避坑指南:我曾经在临界区里调用了vTaskDelay(),结果系统直接死锁了。记住:临界区里不要做任何可能引起任务切换的操作,比如延时、等待信号量、打印日志等。临界区代码要短、快、准。

嗯,到这里RTOS的基础概念就讲完了。总结一下:RTOS的核心是确定性,任务调度靠优先级,资源保护靠临界区。这些概念看起来简单,但实际项目中踩坑的案例比比皆是。下一章我们聊聊任务创建和管理的具体实现,到时候我会分享更多实战经验。