2. 实时操作系统(RTOS)基础:任务、调度器、上下文切换、中断管理
好,我们进入第二章。这一章是RTOS的核心地基。你想想看,心脏起搏器这种设备,每一毫秒都关乎生命。如果操作系统不稳定,后果不堪设想。所以,搞懂RTOS的这几个基本概念,是咱们做医疗固件的基本功。
2.1 任务:不仅仅是函数
在裸机编程里,我们写一个main函数,里面一个大循环。但在RTOS里,我们把整个程序拆成一个个独立的任务。每个任务,说白了就是一个无限循环的函数,但它有自己的“灵魂”——也就是任务控制块。
任务控制块(TCB)是RTOS用来管理任务的数据结构。它里面存了什么?我列一下:
- 任务栈指针(SP):每个任务都有自己的栈,用来保存局部变量和函数调用信息。
- 任务状态:就绪、运行、阻塞、挂起等。
- 任务优先级:数字越小,优先级越高(或者反过来,看具体RTOS实现)。
- 任务ID:唯一标识。
我个人习惯,在起搏器项目中,每个任务都分配独立的栈空间。为什么?因为一旦任务栈溢出,可能破坏其他任务的数据。我见过一个案例,同事把两个任务共用一个栈,结果一个任务跑飞,另一个任务也跟着崩溃。嗯,这种坑踩过一次就够了。
关键点:任务不是函数调用,它是RTOS调度的基本单位。每个任务都有自己的“生命线”——栈空间。
2.2 调度器:谁来决定谁跑?
调度器是RTOS的大脑。它决定哪个任务获得CPU的使用权。在心脏起搏器里,调度策略直接决定了系统能否及时响应心率异常。
常见的调度策略有几种:
| 调度策略 | 特点 | 起搏器适用场景 |
|---|---|---|
| 优先级抢占式 | 高优先级任务随时抢占低优先级任务 | 心率检测任务必须抢占显示任务 |
| 时间片轮转 | 同优先级任务轮流执行固定时间片 | 非关键性任务,如日志记录 |
| 协作式 | 任务主动让出CPU | 几乎不用,因为不可预测 |
我建议在起搏器项目中,核心任务(如感知、起搏)使用最高优先级,并且采用抢占式调度。你想想看,如果检测到室颤,却还要等一个显示任务跑完时间片,那病人就危险了。
避坑指南:我曾经在一个项目中,把两个关键任务设为相同优先级,结果它们互相抢占,导致心跳检测出现微秒级的抖动。后来我调整了优先级,并给高优先级任务加了临界区保护,问题才解决。
2.3 上下文切换:代价有多大?
上下文切换,就是从一个任务切换到另一个任务的过程。这个过程需要保存当前任务的寄存器、栈指针等,然后恢复新任务的这些信息。
上下文切换的代价,说白了就是时间。每次切换,CPU都要花几十到几百个时钟周期。在起搏器这种硬实时系统里,这个时间必须精确可控。
我举个例子:
// 伪代码:上下文切换过程
void PendSV_Handler(void) {
// 保存当前任务上下文
__asm volatile("MRS R0, PSP");
__asm volatile("STMDB R0!, {R4-R11}");
// 更新当前任务TCB中的栈指针
// 找到下一个要运行的任务
// 恢复新任务上下文
__asm volatile("LDMIA R0!, {R4-R11}");
__asm volatile("MSR PSP, R0");
__asm volatile("ORR LR, LR, #0x04");
__asm volatile("BX LR");
}
这段代码看起来简单,但每个指令的执行时间都要算清楚。我记得在调试一个起搏器原型时,发现上下文切换偶尔会多出几个微秒。查了半天,原来是中断嵌套导致栈指针错乱。嗯,从那以后,我每次写上下文切换代码,都会仔细检查栈对齐。
注意:上下文切换期间,必须关中断。否则,如果在保存寄存器时来了一个高优先级中断,可能导致数据不一致。我曾经见过一个bug,就是因为在切换过程中开了中断,结果任务栈被破坏,系统直接复位。
2.4 中断管理:RTOS的“紧急通道”
中断是RTOS处理紧急事件的机制。在心脏起搏器里,中断用来响应心率信号、定时器溢出等。中断管理做得好不好,直接决定了系统的实时性。
RTOS中的中断管理有几个关键点:
- 中断优先级:RTOS通常允许设置中断优先级,高优先级中断可以抢占低优先级中断。
- 中断服务程序(ISR):ISR要短小精悍,只做最必要的事。比如,只读取硬件寄存器,然后通过消息队列或信号量通知任务处理。
- 临界区保护:在ISR中访问共享资源时,必须关中断或使用锁。
我建议在起搏器项目中,把心率检测中断设为最高优先级。为什么?因为这是生命信号。其他中断,比如按键、通信,可以设低一些。
核心原则:ISR里不要做复杂运算,不要调用可能阻塞的RTOS API。把工作交给任务去做。ISR只负责“通知”,任务负责“处理”。
举个例子,一个典型的心率检测ISR:
void Heartbeat_ISR(void) {
// 读取硬件寄存器,获取心率数据
uint32_t heart_rate = READ_REG(HEART_RATE_REG);
// 通过消息队列发送给心率处理任务
xQueueSendFromISR(heartbeat_queue, &heart_rate, NULL);
// 清除中断标志
CLEAR_REG(INT_FLAG_REG);
}
你看,ISR里只做了三件事:读数据、发消息、清标志。真正的分析、决策,都在任务里完成。这样既保证了实时性,又避免了ISR占用太多CPU时间。
个人经验:我曾经在调试一个起搏器时,发现ISR里调用了printf函数。结果每次中断都卡死。后来才意识到,printf是阻塞的,而且可能触发其他中断。从那以后,我规定ISR里绝对不允许调用任何可能阻塞的函数。
好了,这一章的内容就到这里。任务、调度器、上下文切换、中断管理,这四个概念是RTOS的基石。你把这些搞懂了,后面讲任务同步、通信、优先级反转这些,就轻松多了。下一章,我们聊聊任务之间的“沟通”——消息队列和信号量。