第一章:实时系统基础——麻醉机对实时性的要求,任务优先级与调度策略
各位同行,咱们直接切入正题。
麻醉机这东西,说白了就是病人的“呼吸守护神”。你想想看,病人躺在手术台上,自主呼吸被抑制了,全靠机器给他送气。这时候代码要是卡顿一下,或者任务调度出了岔子,那可不是蓝屏重启那么简单的事。我入行那会儿,带我的老师傅说过一句话,我一直记着:“麻醉机的代码,每一行都连着一条命。”
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 的互斥锁默认就支持。
- 尽量减少锁的持有时间。拿到锁,干完活,立刻释放。
一句话总结:麻醉机的实时性,不是靠硬件堆出来的,是靠合理的任务划分、优先级设计和调度策略“算”出来的。你把这些基础打牢了,后面的代码优化才有意义。
好,第一章就聊到这儿。下一章咱们深入讲讲“任务间通信与同步”——信号量、消息队列、事件标志组,这些东西在麻醉机里怎么用才不出事。