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的基石。你把这些搞懂了,后面讲任务同步、通信、优先级反转这些,就轻松多了。下一章,我们聊聊任务之间的“沟通”——消息队列和信号量。