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
- 写回内存
如果任务A执行到第2步时被任务B抢占了,会发生什么?
任务B也读到了旧值,加1,写回。然后任务A继续,也加1,写回。结果呢?本来应该加2,实际只加了1。这就是经典的「竞态条件」。
保护临界区的常用方法:
| 方法 | 说明 | 适用场景 |
|---|---|---|
| 关中断 | 进入临界区前关中断,出来再开 | 极短的操作,如修改一个变量 |
| 信号量 | 任务获取信号量才能访问资源 | 保护一段代码或外设 |
| 互斥量 | 类似信号量,但支持优先级继承 | 有优先级反转风险的场景 |
| 任务通知 | 轻量级的同步机制 | 一对一的任务同步 |
核心原则:临界区要短!要快!别在临界区里做延时、打印、复杂计算。否则整个系统的实时性就毁了。
我见过有人把整个printf放在临界区里,结果串口打印一次要几十毫秒,这段时间所有中断都被关了...系统直接「假死」。嗯,这种问题查起来特别痛苦,因为不是每次都复现。
我的建议:能用关中断解决的,就别用信号量。能用信号量的,就别用互斥量。越简单的机制,出问题的概率越小。别为了「看起来很专业」而过度设计。
好了,第一章的内容就到这里。咱们把RTOS的底子打牢了,后面讲任务通信、中断管理、内存管理这些硬核内容时,你才不会懵。
下一章,咱们聊聊「任务创建与删除」——怎么让你的任务真正跑起来。