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的大脑。它决定「下一个该谁跑」。常见的调度策略有两种:
- 抢占式调度:高优先级任务就绪时,立即打断低优先级任务。我用的都是这种。
- 协作式调度:任务主动让出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
- 调度器决定谁跑,优先级决定谁先跑
- 临界区保护共享资源,但越小越好
下一章我会讲任务状态机设计——制氧机里那些「待机→启动→运行→报警→停机」的状态切换,怎么用状态机优雅地实现。到时候我会拿我踩过的坑当反面教材,保证你们印象深刻。