2、RTOS基础概念:任务、调度器、临界区、同步与通信机制概述

好,咱们正式开始聊RTOS的核心概念。说实话,很多初学者一上来就被各种术语砸晕了——任务、调度器、临界区、信号量、消息队列……感觉像在听天书。别急,我当年也一样。

我记得第一次接触RTOS是在一个电机控制项目上。那时候我还在用裸机跑循环,结果一个中断没处理好,电机直接抖成了筛子。后来老工程师丢给我一句话:「去学学RTOS,别再用你的大循环糟蹋硬件了。」嗯,从那以后,我才真正开始理解这些东西。

今天这一章,咱们就把这些基础概念捋清楚。不搞虚的,全是干活用的。

2.1 任务——RTOS里的「线程」

任务是什么?说白了,就是一段独立执行的代码。在裸机里,你只有一个main函数在那死循环。但在RTOS里,你可以创建多个任务,它们看起来像是「同时」在跑。

每个任务都有自己的栈空间、优先级和状态。我习惯把任务想象成一个独立的小工人——每个工人干自己的活,互不干扰。

任务的核心属性:

  • 任务控制块(TCB):内核用来管理任务的数据结构。里面存了栈指针、优先级、状态等信息。
  • 任务栈:每个任务独享的栈空间。大小要算好,我见过太多因为栈溢出导致的诡异bug。
  • 优先级:数字越小优先级越高(还是越大?不同RTOS定义不同,看手册!)。

任务有几种状态:运行态、就绪态、阻塞态、挂起态。你想想看,一个任务不可能永远在跑——它得等资源、等时间、等信号。这时候它就进入阻塞态,把CPU让给别人。

我的经验:任务栈大小别拍脑袋定。我一般先给个保守值(比如512字节),然后用工具监控栈使用峰值,再调整。曾经有个同事直接给了个2KB的栈,结果内存爆了,查了两天才发现是栈浪费太多。

2.2 调度器——谁来决定谁跑?

调度器就是RTOS的大脑。它决定下一个该哪个任务上CPU跑。常见的调度策略有两种:

  • 抢占式调度:高优先级任务就绪了,立马抢走CPU。实时性要求高的场景必须用这个。
  • 时间片轮转:同优先级的任务轮流跑,每人分一个时间片。适合不需要严格实时的任务。

FreeRTOS默认是抢占式调度。uC/OS也是。为什么?因为工业控制里,你绝不能让一个低优先级的任务耽误了紧急响应。

调度器是怎么工作的?每次系统节拍(SysTick)中断来了,调度器就检查一下:有没有更高优先级的任务就绪了?有的话,立马切换。这个过程叫上下文切换——保存当前任务的寄存器,恢复新任务的寄存器。

注意:上下文切换是有开销的。我测过,在Cortex-M4上,一次切换大概几十到上百个时钟周期。如果你的系统节拍设得太快(比如1ms),切换太频繁,CPU都花在切换上了,实际干活的时间反而少了。我一般设5ms或10ms,看应用场景。

2.3 临界区——别让中断搞乱你的数据

临界区,说白了就是一段「不允许被打断」的代码。为什么需要它?因为多个任务或中断可能会访问同一个全局变量。如果不加保护,数据就乱套了。

举个例子:一个任务在更新传感器数据,另一个中断突然插进来也要读这个数据。你想想看,读到一半的数据,能准吗?

RTOS里保护临界区有两种常见方式:

  • 关中断:进入临界区前关掉所有中断,出来再打开。简单粗暴,但关中断时间不能太长,否则影响实时性。
  • 使用调度锁:禁止任务切换,但中断仍然可以响应。比关中断温和一些。

FreeRTOS中的临界区示例:

// 进入临界区
taskENTER_CRITICAL();

// 访问共享资源
shared_data = sensor_value;

// 退出临界区
taskEXIT_CRITICAL();

避坑指南:我曾经在一个项目中,临界区里放了个延时函数。结果呢?整个系统卡死了。因为关中断期间,系统节拍中断也进不来,调度器没法工作。记住:临界区里别干耗时的事,越快越好。

2.4 同步与通信机制——任务之间怎么聊天?

任务之间不能各干各的,总得互相配合。比如一个任务采集数据,另一个任务处理数据,第三个任务发送数据。它们之间怎么协调?这就需要同步和通信机制。

2.4.1 信号量——「好了,你可以干了」

信号量就是个计数器。任务A释放一个信号量(计数值+1),任务B获取这个信号量(计数值-1)。如果计数值为0,任务B就阻塞等待。

信号量有两种:

  • 二值信号量:只有0和1,像一把钥匙。常用于中断与任务之间的同步。
  • 计数信号量:可以计数,比如管理一个资源池,有5个缓冲区可用。

我习惯用二值信号量来通知任务:「数据准备好了,快来取!」

2.4.2 互斥量——「这个资源我先用」

互斥量是信号量的变种,但它有优先级继承机制。什么意思?如果一个低优先级任务占着互斥量,高优先级任务来等它,低优先级任务会临时提升优先级,尽快释放资源。这能防止优先级反转。

优先级反转是什么?简单说:低优先级任务占着资源不放,中优先级任务抢了CPU,高优先级任务反而被堵死了。我早期一个项目就栽在这上面,后来加了互斥量才解决。

2.4.3 消息队列——「给你传个数据」

消息队列是任务间传递数据的常用方式。一个任务往队列里发消息,另一个任务从队列里取消息。队列是FIFO(先进先出)的,当然也支持紧急消息插队。

消息队列的大小要规划好。太小了容易丢消息,太大了浪费内存。我一般根据数据产生速率和处理速率来算,再留20%的余量。

FreeRTOS消息队列示例:

// 创建队列,每个消息10字节,队列深度5
QueueHandle_t xQueue = xQueueCreate(5, 10);

// 发送消息
int data = 100;
xQueueSend(xQueue, &data, portMAX_DELAY);

// 接收消息
int received;
xQueueReceive(xQueue, &received, portMAX_DELAY);

2.4.4 事件标志组——「多个条件都满足才执行」

有时候一个任务需要等好几个条件都成立才能继续。比如:传感器1数据就绪、传感器2数据就绪、定时器超时。这时候用事件标志组最合适。

每个位代表一个事件,任务可以等「与」条件(所有位都置1)或「或」条件(任意位置1)。

2.5 总结一下

这一章咱们聊了RTOS的五个核心概念:

概念 一句话总结
任务 独立执行的代码单元,有自己的栈和优先级
调度器 决定哪个任务上CPU跑,抢占式还是时间片
临界区 保护共享数据,关中断或加锁
信号量/互斥量 同步与互斥,防止资源竞争
消息队列/事件组 任务间传数据或等条件

这些概念是RTOS的基石。你想想看,没有它们,多个任务就像一群没纪律的士兵,各跑各的,迟早乱套。有了这些机制,任务才能有序协作。

下一章咱们会深入FreeRTOS的源码,看看这些机制在Cortex-M上到底是怎么实现的。到时候我会带着你一行一行读代码,别怕,没那么难。

课后小练习:打开你的开发板,创建一个简单的FreeRTOS工程。创建两个任务:一个每500ms翻转LED,另一个每1秒打印一次「Hello RTOS」。试试看,能不能跑起来?