3、血压计状态机顶层设计:系统级状态划分

好,咱们今天聊聊血压计最顶层的状态机设计。说白了,就是整个系统怎么“跑”起来的。

我刚开始做血压计项目时,第一版代码写得特别“直”——一个 while 大循环,里面塞满了 if-else。结果呢?充气还没完成,按键响应就乱了;测量中途用户拔掉袖带,系统直接死机。嗯,那叫一个惨。

后来我学乖了。顶层状态机,必须先把“系统级状态”划分清楚。这是整个架构的骨架,骨架歪了,后面怎么填肉都别扭。

3.1 六大核心状态

我个人习惯,把血压计的工作流程拆成六个状态。你想想看,从用户按下“开始”键,到最终看到结果,中间经历了什么?

  • 空闲(IDLE):待机,等待用户操作。
  • 充气(INFLATE):气泵工作,袖带加压。
  • 测量(MEASURE):压力稳定,采集脉搏波信号。
  • 放气(DEFLATE):测量完成,缓慢或快速放气。
  • 结果显示(RESULT):在屏幕上展示收缩压、舒张压、心率。
  • 错误处理(ERROR):任何异常,比如袖带漏气、用户乱动。

这六个状态,覆盖了血压计99%的工作场景。剩下的1%,比如固件升级、自检,可以单独处理,不放在主状态机里。

3.2 状态转换图(核心逻辑)

状态之间怎么跳?我画了一张表,你一看就明白:

当前状态 触发事件 下一状态 备注
空闲 用户按下“开始”键 充气 同时检查袖带是否连接
充气 压力达到预设阈值(如160mmHg) 测量 阈值可自适应调整
充气 压力异常(如超过安全值) 错误处理 立即停止充气,快速放气
测量 脉搏波采集完成 放气 通常采集30-40个心跳周期
测量 信号质量差(如用户移动) 错误处理 提示“测量失败,请重试”
放气 压力降至安全值以下 结果显示 放气速度需控制,避免用户不适
结果显示 用户确认或超时(如30秒) 空闲 自动返回待机
错误处理 错误已排除(如重新连接袖带) 空闲 或直接进入充气重试

这张表,就是状态机的“宪法”。所有代码逻辑,都得按这个来。

3.3 代码骨架(C语言实现)

我习惯用枚举定义状态,用 switch-case 实现状态机。简单、清晰、好调试。

// 状态枚举
typedef enum {
    STATE_IDLE,
    STATE_INFLATE,
    STATE_MEASURE,
    STATE_DEFLATE,
    STATE_RESULT,
    STATE_ERROR
} sys_state_t;

// 全局状态变量
sys_state_t current_state = STATE_IDLE;

// 状态机主循环(简化版)
void state_machine_run(void) {
    switch (current_state) {
        case STATE_IDLE:
            if (button_start_pressed()) {
                // 检查袖带连接
                if (cuff_connected()) {
                    current_state = STATE_INFLATE;
                    start_pump();
                } else {
                    show_error("请连接袖带");
                }
            }
            break;

        case STATE_INFLATE:
            if (pressure_reached_target()) {
                current_state = STATE_MEASURE;
                stop_pump();
                start_sampling();
            } else if (pressure_too_high()) {
                current_state = STATE_ERROR;
                emergency_release();
            }
            break;

        case STATE_MEASURE:
            if (sampling_complete()) {
                current_state = STATE_DEFLATE;
                start_deflation();
            } else if (signal_noise_detected()) {
                current_state = STATE_ERROR;
                show_error("信号干扰");
            }
            break;

        // ... 其他状态类似处理
    }
}

你看,每个 case 里只做两件事:判断条件,然后跳转。千万别在 case 里写复杂的业务逻辑,否则状态机就变成“面条机”了。

核心原则: 状态机只负责“状态跳转”,不负责“具体干活”。具体干活(比如充气、采样)交给独立的模块去处理。

3.4 避坑指南

我曾经踩过一个坑:在充气状态里,直接调用了放气函数。结果呢?气泵还在转,放气阀也开了,两边打架,压力忽高忽低。用户还以为血压计要爆炸了。

所以,我后来强制规定:每个状态只能操作自己的硬件资源。进入新状态前,必须释放上一个状态的资源。比如:

  • 进入“测量”前,确保气泵已关闭。
  • 进入“放气”前,确保采样已停止。
  • 进入“空闲”前,确保所有外设已断电。
警告: 千万不要在中断服务函数里直接修改状态机的状态!我曾经这么干过,结果状态跳了一半,主循环还没反应过来,系统就乱了。正确的做法是:中断里只设置标志位,主循环里再处理状态跳转。

3.5 为什么这样划分?

你可能会问:为什么非要把“充气”和“测量”分开?合在一起不行吗?

嗯,这里要注意。充气阶段,气泵噪音大,压力不稳定,这时候采集脉搏波,信号质量极差。强行合在一起,算法复杂度会翻倍,而且容易误判。分开之后,每个阶段的任务单一,代码好写,调试也方便。

我记得有一次,客户要求增加“快速测量”模式。如果状态机是耦合的,我得改一大片代码。但因为我们划分得清晰,只需要在“充气”状态里加一个“快速充气”分支,其他状态完全不用动。这就是顶层设计带来的好处。

3.6 小结

顶层状态划分,说白了就是“分而治之”。把一个大问题,切成六个小问题,每个小问题单独解决。你想想看,是不是这个理?

下一章,咱们会深入“充气状态”的细节,聊聊怎么控制气泵、怎么检测压力异常。到时候,我会分享一个我当年调试气泵时遇到的“灵异事件”——嗯,保证让你印象深刻。