3、血压测量算法:示波法原理、特征点提取(收缩压/舒张压)、脉搏波滤波算法(移动平均/中值滤波)、测量状态机设计

各位同学,咱们今天聊点硬核的——血压测量算法。说实话,很多做蓝牙通信的工程师,往往把算法这块外包给传感器厂商。但我个人觉得,不懂算法的蓝牙工程师,就像只会拧螺丝的装配工。你想想看,数据都从你手里过,你不知道这数据是怎么来的,怎么保证传输的可靠性?

我在做第一版血压计原型时,就踩过这个坑。传感器数据直接拿来用,结果蓝牙传上去的血压值忽高忽低,被测试部门骂得狗血淋头。后来我才明白,算法才是整个系统的灵魂。

3.1 示波法原理:你必须要懂的底层逻辑

示波法,说白了就是通过袖带压力振荡波来推算血压。它的核心思想很简单:

  • 袖带充气:阻断动脉血流
  • 缓慢放气:记录袖带压力信号
  • 提取脉搏波:从压力信号中分离出心跳引起的振荡波
  • 特征点定位:找到收缩压和舒张压对应的位置

嗯,这里要注意一个关键点:示波法测的不是直接血压,而是通过振荡波的幅度变化来推算。为什么能这么干?因为当袖带压力等于平均压时,动脉壁处于最松弛状态,此时脉搏波振幅最大。这个现象我在实验室验证过无数次,确实很稳定。

核心公式(简化版):

收缩压 ≈ 平均压 + 系数 × (最大振幅对应的袖带压 - 平均压)

舒张压 ≈ 平均压 - 系数 × (平均压 - 最小振幅对应的袖带压)

其中系数通常取0.45~0.55,具体值需要根据临床校准

3.2 特征点提取:收缩压和舒张压到底在哪?

这是整个算法中最容易出问题的地方。我记得第一次做特征点提取时,直接拿最大振幅的百分比去算,结果测出来血压值跟水银柱差了20多mmHg。后来查资料才发现,不同人群的脉搏波形态差异很大。

我建议采用以下步骤:

  1. 找平均压点:脉搏波振幅最大的位置,对应平均压
  2. 找收缩压点:从平均压点往前找,振幅下降到某个比例(通常0.45~0.55)的位置
  3. 找舒张压点:从平均压点往后找,振幅下降到某个比例(通常0.55~0.65)的位置

这里有个避坑指南:我曾经遇到过老年患者血管硬化严重,脉搏波振幅变化很平缓,用固定比例根本找不到特征点。后来我改用自适应阈值法——根据前几个脉搏波的振幅动态调整比例系数,效果好了很多。

警告:特征点提取时,一定要做异常值剔除。如果某个脉搏波振幅突然跳变(比如患者移动手臂),直接用它计算会得到荒谬的结果。我一般会加一个滑动窗口,剔除超过均值±3倍标准差的异常点。

3.3 脉搏波滤波算法:移动平均 vs 中值滤波

原始脉搏波信号里全是噪声——工频干扰、肌电干扰、运动伪迹。不滤波的话,特征点提取就是扯淡。我常用的两种方法:

滤波方法 原理 优点 缺点 适用场景
移动平均 取窗口内N个点的平均值 计算快,平滑效果好 会模糊波峰波谷 基线漂移校正
中值滤波 取窗口内N个点的中位数 抗脉冲噪声能力强 计算稍慢,边缘保持一般 运动伪迹去除

我个人习惯的做法是:先用移动平均去除基线漂移,再用中值滤波去除尖峰噪声。你想想看,移动平均就像用抹布擦桌子——大面积抹平;中值滤波就像用镊子挑刺——精准去除异常点。两者配合使用,效果最好。

// 移动平均滤波示例(窗口大小=5)
uint16_t moving_average(uint16_t *buffer, uint8_t size) {
    uint32_t sum = 0;
    for(uint8_t i = 0; i < size; i++) {
        sum += buffer[i];
    }
    return (uint16_t)(sum / size);
}

// 中值滤波示例(窗口大小=5,冒泡排序)
uint16_t median_filter(uint16_t *buffer, uint8_t size) {
    uint16_t temp[5];
    memcpy(temp, buffer, size * sizeof(uint16_t));
    // 冒泡排序
    for(uint8_t i = 0; i < size-1; i++) {
        for(uint8_t j = 0; j < size-i-1; j++) {
            if(temp[j] > temp[j+1]) {
                uint16_t t = temp[j];
                temp[j] = temp[j+1];
                temp[j+1] = t;
            }
        }
    }
    return temp[size/2];  // 取中位数
}

小技巧:窗口大小的选择很关键。窗口太小,滤波效果差;窗口太大,信号失真。我一般根据采样率来定——如果采样率是100Hz,窗口取5~7个点比较合适。你可以用示波器先看看原始信号的噪声频率,再反推窗口大小。

3.4 测量状态机设计:让算法有条不紊地跑起来

血压测量不是一锤子买卖,它是个完整的状态流转过程。我设计的状态机一般包含以下状态:

  • IDLE:空闲状态,等待启动命令
  • INFLATE:充气状态,袖带加压到180mmHg左右
  • DEFLATE:放气状态,以2~3mmHg/s的速度缓慢放气
  • MEASURE:测量状态,实时采集并处理脉搏波
  • CALCULATE:计算状态,提取特征点并计算血压值
  • RESULT:结果状态,输出血压值并准备放气
  • ERROR:错误状态,处理异常情况(如袖带漏气、患者移动)

为什么要有这么复杂的状态机?说白了就是为了处理各种边界情况。我曾经遇到过充气过程中患者突然咳嗽,导致压力瞬间飙升——如果没有错误状态处理,整个系统就死机了。

我建议的状态机实现要点:

  1. 状态切换用定时器驱动:不要用死循环等待,否则会阻塞蓝牙通信
  2. 每个状态设置超时保护:比如充气超过10秒还没到目标压力,直接进入错误状态
  3. 测量状态要实时保存数据:我一般用环形缓冲区,一边采集一边处理
  4. 计算状态完成后自动放气:这是安全要求,不能忘了

状态机伪代码示例:

void bp_state_machine(void) {
    switch(current_state) {
        case IDLE:
            if(start_measure_flag) {
                start_pump();
                current_state = INFLATE;
                timeout_counter = 0;
            }
            break;
            
        case INFLATE:
            if(pressure >= 180) {
                stop_pump();
                start_valve_slow();
                current_state = DEFLATE;
            }
            if(timeout_counter++ > 100) {
                current_state = ERROR;  // 充气超时
            }
            break;
            
        case DEFLATE:
            if(pressure < 60) {
                current_state = CALCULATE;
            }
            // 采集脉搏波数据
            collect_pulse_wave();
            break;
            
        case CALCULATE:
            calculate_bp();
            current_state = RESULT;
            break;
            
        case RESULT:
            open_valve_fast();  // 快速放气
            send_result_via_ble();
            current_state = IDLE;
            break;
            
        case ERROR:
            open_valve_fast();
            send_error_code();
            current_state = IDLE;
            break;
    }
}

嗯,这里要特别提醒一下:状态机里的超时保护一定要做。我见过一个产品,因为充气阀门卡住,袖带一直加压到300多mmHg,把患者胳膊都勒紫了。这种安全事故,咱们做嵌入式的一定要防住。

好了,这一章的内容就到这里。示波法原理、特征点提取、滤波算法、状态机设计——这四个知识点是血压计算法的四大支柱。下一章我会讲怎么把这些算法移植到具体的MCU上,以及如何优化内存和计算速度。咱们下节课见。