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 状态图来画,认证老师看着也舒服。

好了,这一章的内容就这些。分层架构让你代码不乱,模块化让你好维护,状态机让你逻辑清晰。这三板斧砍下去,呼吸机的嵌入式软件架构基本就稳了。下一章,我们聊聊实时性和任务调度,那又是另一个有意思的话题。