1. RTOS基础概念

大家好,我是你们的讲师。今天咱们开始聊RTOS。

说实话,我第一次接触RTOS是在做一款便携式医疗设备的时候。那时候项目催得紧,前后台系统跑得我焦头烂额——按键响应不及时,屏幕刷新卡顿,还时不时死机。后来换了RTOS,问题迎刃而解。嗯,从那时起我就知道,这玩意儿值得好好学。

1.1 什么是RTOS

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

你可能会问:「普通操作系统不也能完成任务吗?」

区别在于「确定性」。Windows、Linux这些通用操作系统,你点一下鼠标,系统什么时候响应?不确定。可能几毫秒,也可能几百毫秒。但RTOS不一样——它拍着胸脯保证:「这个任务,最多5毫秒内给你搞定。」

RTOS的核心特征:

  • 确定性:任务响应时间可预测、可计算
  • 实时性:满足硬实时或软实时要求
  • 多任务:看似同时运行多个任务
  • 抢占式调度:高优先级任务可以打断低优先级任务

我在项目中遇到过不少新手,一上来就问:「FreeRTOS和Linux哪个好?」其实这俩不是一个赛道的东西。Linux追求吞吐量,RTOS追求实时性。你拿Linux去做心脏起搏器?那病人可能等不到系统响应就...嗯,你懂的。

1.2 前后台系统与RTOS对比

讲RTOS之前,咱们先看看它的「前任」——前后台系统。

前后台系统长什么样?

就是一个大循环(后台),加上中断服务程序(前台)。

void main(void)
{
    // 初始化硬件
    init_all();
    
    while(1)
    {
        // 后台:轮询各种标志
        if(flag_key)    handle_key();
        if(flag_sensor) handle_sensor();
        if(flag_display) refresh_display();
        // ... 还有一堆if
    }
}

// 前台:中断服务
void TIM_IRQHandler(void)
{
    flag_sensor = 1;  // 置个标志就跑
}

这种架构简单,但问题也明显。我早期做的一个温控项目就吃过亏——传感器采集任务跑得慢,导致PID计算延迟,温度超调得一塌糊涂。

对比项 前后台系统 RTOS
任务管理 手动轮询,代码耦合 独立任务,松耦合
实时性 差,受主循环影响 好,可抢占调度
代码维护 随着功能增加越来越乱 模块化,清晰
资源开销 极低 需要额外RAM/ROM
适用场景 极简系统、任务极少 多任务、有实时要求

你想想看,如果你的项目只有「每隔1秒闪个灯」这种任务,前后台完全够用。但一旦超过3-5个任务,还涉及优先级、同步、通信...我建议你直接上RTOS,别犹豫。

1.3 任务与调度器

任务(Task),在RTOS里就是一段独立的代码,有自己的栈空间和优先级。每个任务看起来就像「独占CPU」一样。

任务的状态机:

  • 运行态:正在使用CPU
  • 就绪态:万事俱备,只差CPU
  • 阻塞态:在等某个事件(延时、信号量、队列等)
  • 挂起态:被暂停了,需要别人唤醒

调度器(Scheduler)是RTOS的大脑。它决定「下一个该谁跑」。

我个人习惯:把调度器想象成一个「交通警察」。CPU就是一条路,任务就是路上的车。调度器决定哪辆车先走、走多久、什么时候靠边让行。

常见的调度策略:

  • 抢占式调度:高优先级任务随时可以抢CPU。这是最常用的。
  • 时间片轮转:同优先级任务轮流用CPU,每人分一个时间片。
  • 协作式调度:任务主动让出CPU,否则一直跑。现在用得少了。
// 一个典型的RTOS任务示例(FreeRTOS风格)
void vTask_LED(void *pvParameters)
{
    while(1)
    {
        GPIO_Toggle(LED_PIN);
        vTaskDelay(pdMS_TO_TICKS(500));  // 阻塞500ms,让出CPU
    }
}

void vTask_KeyScan(void *pvParameters)
{
    while(1)
    {
        if(KEY_Pressed())
        {
            xQueueSend(xKeyQueue, &key_val, 0);
        }
        vTaskDelay(pdMS_TO_TICKS(10));   // 每10ms扫描一次
    }
}

注意看,每个任务里都有个vTaskDelay。这就是任务主动「睡觉」,把CPU让给别人。没有这个,高优先级任务就会一直霸占CPU,低优先级任务永远没机会跑。

我曾经踩过的坑: 有个任务忘了加延时,结果这个任务占用了99%的CPU时间,其他任务全部饿死。系统看起来还在跑,但按键没反应,屏幕不刷新。排查了两天才发现——一个while(1)里忘了放阻塞函数。

1.4 临界区与资源保护

临界区(Critical Section),就是一段「不能被中断」的代码。为什么需要它?因为共享资源。

举个例子:

// 两个任务都要修改这个全局变量
int g_counter = 0;

// 任务A
void task_A(void)
{
    g_counter++;  // 读-改-写三步
}

// 任务B
void task_B(void)
{
    g_counter++;  // 同样三步
}

看起来没问题?实际上g_counter++在汇编层面是三条指令:

  1. 从内存读到寄存器
  2. 寄存器加1
  3. 写回内存

如果任务A执行到第2步时被任务B抢占了,会发生什么?

任务B也读到了旧值,加1,写回。然后任务A继续,也加1,写回。结果呢?本来应该加2,实际只加了1。这就是经典的「竞态条件」。

保护临界区的常用方法:

方法 说明 适用场景
关中断 进入临界区前关中断,出来再开 极短的操作,如修改一个变量
信号量 任务获取信号量才能访问资源 保护一段代码或外设
互斥量 类似信号量,但支持优先级继承 有优先级反转风险的场景
任务通知 轻量级的同步机制 一对一的任务同步

核心原则:临界区要短!要快!别在临界区里做延时、打印、复杂计算。否则整个系统的实时性就毁了。

我见过有人把整个printf放在临界区里,结果串口打印一次要几十毫秒,这段时间所有中断都被关了...系统直接「假死」。嗯,这种问题查起来特别痛苦,因为不是每次都复现。

我的建议:能用关中断解决的,就别用信号量。能用信号量的,就别用互斥量。越简单的机制,出问题的概率越小。别为了「看起来很专业」而过度设计。

好了,第一章的内容就到这里。咱们把RTOS的底子打牢了,后面讲任务通信、中断管理、内存管理这些硬核内容时,你才不会懵。

下一章,咱们聊聊「任务创建与删除」——怎么让你的任务真正跑起来。