4、嵌入式软件架构设计:分层架构(HAL/驱动/中间件/应用)、模块化设计原则、状态机设计(呼吸模式切换)
好,我们进入第四章。这一章,我个人认为是整个嵌入式系统设计的「骨架」。你想想看,呼吸机这种设备,代码量动辄几十万行,如果没有一个清晰的架构,后期维护简直就是噩梦。我见过不少项目,前期图快,代码揉成一团,结果到了认证阶段,改一个 bug 引出三个新 bug,那叫一个痛苦。
今天我们就来聊聊,怎么搭这个骨架。核心就三块:分层架构、模块化设计、状态机。这三样东西,是呼吸机软件通过认证的基石。
4.1 分层架构:把复杂问题拆成几层
分层架构,说白了就是「各司其职」。每一层只关心自己的事,不越界。我习惯把呼吸机的嵌入式软件分成四层:硬件抽象层(HAL)、驱动层、中间件层、应用层。
| 层级 | 职责 | 典型内容 |
|---|---|---|
| 应用层 | 业务逻辑、模式切换、报警策略 | 呼吸模式管理、参数设置、用户界面 |
| 中间件层 | 协议解析、数据缓冲、任务调度 | RTOS、文件系统、通信协议栈 |
| 驱动层 | 直接操作外设寄存器 | ADC、DAC、PWM、UART、SPI 驱动 |
| 硬件抽象层 | 屏蔽硬件差异,提供统一接口 | HAL_GPIO_WritePin、HAL_TIM_PWM_Start |
为什么要这么分?我举个例子。你在应用层写了一个「切换呼吸模式」的函数,它不应该直接去操作某个 GPIO 引脚。万一以后换了 MCU,引脚号变了,你难道要改应用层的代码?那认证就得重来。有了 HAL 层,你只需要改 HAL 层的映射,应用层纹丝不动。
核心原则:上层只能调用下层,不能跨层调用。下层不能反向依赖上层。这是铁律。
4.2 模块化设计原则:高内聚,低耦合
模块化,我理解就是「把大象装进冰箱」—— 每个模块只做一件事,并且做好。在呼吸机里,一个典型的模块划分是这样的:
- 传感器模块:负责采集流量、压力、氧浓度数据。
- 执行器模块:控制涡轮、比例阀、呼气阀。
- 报警模块:监测异常,触发声光报警。
- 通信模块:处理与上位机、护士站的数据交换。
- 日志模块:记录事件,用于事后追溯。
每个模块对外只暴露必要的接口。比如传感器模块,只提供 GetPressure()、GetFlow() 这样的函数。至于它内部是用 I2C 还是 SPI 读的,其他模块不需要知道。
我的一个小习惯:每个模块都配一个 _cfg.h 文件,里面放所有可配置的宏。比如采样频率、滤波系数、超时时间。这样调试时改参数,不用翻遍整个代码。
我曾经在一个项目里,看到有人把报警阈值写死在传感器模块的 .c 文件里。后来要改阈值,得重新编译整个传感器模块。这其实违反了模块化原则。正确的做法是:阈值由应用层通过接口传入,或者从配置文件中读取。
4.3 状态机设计:呼吸模式切换
呼吸机最核心的逻辑,就是呼吸模式切换。比如从「容量控制通气(VCV)」切换到「压力支持通气(PSV)」。这个过程必须安全、可靠、无抖动。
我强烈建议用有限状态机(FSM)来实现。别用一堆 if-else 去判断模式,那代码会变成一团乱麻。
一个典型的呼吸模式状态机,至少包含这几个状态:
- IDLE(空闲):待机状态,不送气。
- INSPIRATION(吸气):正在送气。
- EXPIRATION(呼气):正在呼气。
- HOLD(屏气):吸气末或呼气末的暂停。
- ALARM(报警):检测到异常,进入安全模式。
状态之间的转换,由事件触发。比如:
- 从 IDLE 到 INSPIRATION,触发事件是「启动通气」。
- 从 INSPIRATION 到 EXPIRATION,触发事件是「吸气时间到」或「压力/容量达到预设值」。
- 任何状态都可以跳转到 ALARM,触发事件是「严重故障」。
代码实现上,我推荐用「查表法」而不是「switch-case 嵌套」。查表法更清晰,也更容易做形式化验证。看个例子:
// 状态转换表
typedef struct {
State currentState;
Event triggerEvent;
State nextState;
Action action;
} Transition;
// 定义所有可能的转换
const Transition stateTable[] = {
{IDLE, EVENT_START, INSPIRATION, ActionStartInspiration},
{INSPIRATION, EVENT_TIME_UP, EXPIRATION, ActionStartExpiration},
{INSPIRATION, EVENT_PRESSURE_HIGH, EXPIRATION, ActionStartExpiration},
{EXPIRATION, EVENT_TIME_UP, IDLE, ActionStopVentilation},
// ... 其他转换
};
// 状态机执行函数
State currentState = IDLE;
void ProcessEvent(Event event) {
for (int i = 0; i < sizeof(stateTable)/sizeof(Transition); i++) {
if (stateTable[i].currentState == currentState &&
stateTable[i].triggerEvent == event) {
// 执行动作
stateTable[i].action();
// 切换状态
currentState = stateTable[i].nextState;
break;
}
}
}
避坑指南:我曾经犯过一个错,在状态转换的动作里直接调用了延时函数。结果延时期间,状态机被阻塞,无法响应新的报警事件。记住:状态机的动作必须是「非阻塞」的。如果需要等待,用定时器或状态机内部的子状态来实现。
另外,呼吸模式切换时,一定要做「安全过渡」。比如从 VCV 切换到 PSV,你不能瞬间把气道压力从 20 cmH₂O 降到 5 cmH₂O。那会触发病人的保护性反射。正确的做法是:在切换过程中,先进入一个「过渡状态」,逐步调整参数,直到稳定后再进入目标模式。
嗯,这里还要提一句。状态机的设计文档,是医疗器械认证的必审项。你需要把每个状态、每个事件、每个转换都画成图,并且用自然语言描述清楚。我建议用 UML 状态图来画,认证老师看着也舒服。
好了,这一章的内容就这些。分层架构让你代码不乱,模块化让你好维护,状态机让你逻辑清晰。这三板斧砍下去,呼吸机的嵌入式软件架构基本就稳了。下一章,我们聊聊实时性和任务调度,那又是另一个有意思的话题。