第一章:实时系统基础——麻醉机对实时性的要求,任务优先级与调度策略

各位同行,咱们直接切入正题。

麻醉机这东西,说白了就是病人的“呼吸守护神”。你想想看,病人躺在手术台上,自主呼吸被抑制了,全靠机器给他送气。这时候代码要是卡顿一下,或者任务调度出了岔子,那可不是蓝屏重启那么简单的事。我入行那会儿,带我的老师傅说过一句话,我一直记着:“麻醉机的代码,每一行都连着一条命。”

1.1 麻醉机到底有多“实时”?

很多人觉得实时系统就是“快”。其实不完全对。实时系统的核心是“确定性”——你得保证某个任务在规定的截止时间之前必须完成。

拿麻醉机来说,有几个硬指标:

  • 吸气相到呼气相的切换:这个切换必须在毫秒级完成。病人吸气结束,你阀门晚关 50ms,病人就可能憋气。
  • 压力采样与闭环控制:气道压力传感器数据,通常需要每 1ms 甚至更短时间采集一次。控制算法必须在下一个采样点到来之前算完。
  • 报警响应:气道压力过高、氧浓度异常,这些报警必须在 100ms 内触发。我见过一个项目,报警任务优先级设低了,结果被通信任务抢了 CPU,报警灯亮了但蜂鸣器没响——这要是真出了事,后果不堪设想。

核心结论:麻醉机属于“硬实时系统”。错过截止时间,就是安全事故。

1.2 任务优先级怎么分?

我个人习惯把麻醉机的任务分成三个梯队:

优先级 任务类型 典型任务 截止时间
最高(0-1) 安全关键任务 压力检测、过压保护、氧浓度监测 < 10ms
中等(2-3) 控制与采样任务 吸气阀/呼气阀控制、流量传感器采样 1-5ms
较低(4-5) 非实时任务 UI 刷新、数据记录、通信 100ms 以上

嗯,这里要注意:优先级不是越高越好。你把所有任务都设成最高优先级,那跟没有优先级有什么区别?我曾经在一个项目里看到有人把 UI 刷新任务设成了最高优先级,结果压力控制任务被活活饿死,病人波形直接变成一条直线——还好是在测试阶段发现的。

1.3 调度策略怎么选?

市面上常见的 RTOS,比如 FreeRTOS、uC/OS-III,都支持两种调度策略:

  • 抢占式调度(Preemptive):高优先级任务来了,低优先级任务立刻让路。这是麻醉机的首选。
  • 时间片轮转(Round-Robin):同等优先级的任务轮流跑。这个在麻醉机里要慎用。

为什么?你想想看,压力控制任务和 UI 刷新任务如果同优先级,压力控制跑了一半,时间片到了,被迫切出去跑 UI——那压力控制就断了。我建议的做法是:

我的经验:安全关键任务用抢占式,并且优先级隔离。非实时任务可以用时间片轮转,但一定要放在最低优先级组里。

1.4 一个典型的任务调度配置

拿 FreeRTOS 举例,我一般这样配:

// 任务优先级定义
#define PRIO_SAFETY     5   // 安全监测任务(最高)
#define PRIO_CONTROL    4   // 控制任务
#define PRIO_SENSOR     3   // 传感器采样
#define PRIO_COMM       2   // 通信任务
#define PRIO_UI         1   // 界面刷新(最低)

// 任务创建
xTaskCreate(vSafetyMonitorTask, "Safety", 256, NULL, PRIO_SAFETY, NULL);
xTaskCreate(vControlTask,       "Ctrl",   512, NULL, PRIO_CONTROL, NULL);
xTaskCreate(vSensorTask,        "Sensor", 256, NULL, PRIO_SENSOR, NULL);
xTaskCreate(vCommTask,          "Comm",   384, NULL, PRIO_COMM, NULL);
xTaskCreate(vUITask,            "UI",     1024, NULL, PRIO_UI, NULL);

注意看,安全监测任务优先级最高,但它的栈空间反而小——因为它只做简单的比较和报警,不干重活。控制任务栈空间大,因为它要跑 PID 算法。

避坑指南:我曾经犯过一个错——把传感器采样任务和控制任务设成同一个优先级。结果采样任务被控制任务阻塞,数据更新不及时,控制算法用的全是旧数据,导致压力过冲。后来我把采样任务优先级调高了一级,问题就解决了。

1.5 中断与任务的配合

实时系统里,中断是“快刀”,任务是“慢工”。

我的原则是:中断里只做最轻量的事。比如压力传感器触发中断,我就在中断里读一下寄存器,然后发个信号量给控制任务。真正的 PID 计算和阀门控制,放到任务里做。

为什么?中断里不能调用阻塞函数,不能做浮点运算,更不能等信号量。你想想看,中断里跑个 printf 会怎样?整个系统直接挂掉。

// 中断服务函数——只做最轻量的事
void TIM1_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    // 读取传感器数据
    g_u16PressureADC = ADC1->DR;
    
    // 通知控制任务
    vTaskNotifyGiveFromISR(xControlTaskHandle, &xHigherPriorityTaskWoken);
    
    // 退出中断前做一次上下文切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

1.6 死锁与优先级反转——两个要命的坑

说到实时调度,有两个问题必须提:

  • 死锁:任务 A 等任务 B 的资源,任务 B 等任务 A 的资源,两个都卡死。麻醉机里要是出现死锁,病人就危险了。
  • 优先级反转:低优先级任务拿着锁,高优先级任务等锁,结果中等优先级任务抢了 CPU,高优先级任务反而被“饿死”。

我建议的做法:

  • 能用信号量就别用互斥锁。信号量没有优先级继承,但简单可靠。
  • 如果非要用互斥锁,一定要开启优先级继承(Priority Inheritance)。FreeRTOS 的互斥锁默认就支持。
  • 尽量减少锁的持有时间。拿到锁,干完活,立刻释放。

一句话总结:麻醉机的实时性,不是靠硬件堆出来的,是靠合理的任务划分、优先级设计和调度策略“算”出来的。你把这些基础打牢了,后面的代码优化才有意义。

好,第一章就聊到这儿。下一章咱们深入讲讲“任务间通信与同步”——信号量、消息队列、事件标志组,这些东西在麻醉机里怎么用才不出事。