3、血压测量算法:示波法原理、特征点提取(收缩压/舒张压)、脉搏波滤波算法(移动平均/中值滤波)、测量状态机设计
各位同学,咱们今天聊点硬核的——血压测量算法。说实话,很多做蓝牙通信的工程师,往往把算法这块外包给传感器厂商。但我个人觉得,不懂算法的蓝牙工程师,就像只会拧螺丝的装配工。你想想看,数据都从你手里过,你不知道这数据是怎么来的,怎么保证传输的可靠性?
我在做第一版血压计原型时,就踩过这个坑。传感器数据直接拿来用,结果蓝牙传上去的血压值忽高忽低,被测试部门骂得狗血淋头。后来我才明白,算法才是整个系统的灵魂。
3.1 示波法原理:你必须要懂的底层逻辑
示波法,说白了就是通过袖带压力振荡波来推算血压。它的核心思想很简单:
- 袖带充气:阻断动脉血流
- 缓慢放气:记录袖带压力信号
- 提取脉搏波:从压力信号中分离出心跳引起的振荡波
- 特征点定位:找到收缩压和舒张压对应的位置
嗯,这里要注意一个关键点:示波法测的不是直接血压,而是通过振荡波的幅度变化来推算。为什么能这么干?因为当袖带压力等于平均压时,动脉壁处于最松弛状态,此时脉搏波振幅最大。这个现象我在实验室验证过无数次,确实很稳定。
核心公式(简化版):
收缩压 ≈ 平均压 + 系数 × (最大振幅对应的袖带压 - 平均压)
舒张压 ≈ 平均压 - 系数 × (平均压 - 最小振幅对应的袖带压)
其中系数通常取0.45~0.55,具体值需要根据临床校准
3.2 特征点提取:收缩压和舒张压到底在哪?
这是整个算法中最容易出问题的地方。我记得第一次做特征点提取时,直接拿最大振幅的百分比去算,结果测出来血压值跟水银柱差了20多mmHg。后来查资料才发现,不同人群的脉搏波形态差异很大。
我建议采用以下步骤:
- 找平均压点:脉搏波振幅最大的位置,对应平均压
- 找收缩压点:从平均压点往前找,振幅下降到某个比例(通常0.45~0.55)的位置
- 找舒张压点:从平均压点往后找,振幅下降到某个比例(通常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:错误状态,处理异常情况(如袖带漏气、患者移动)
为什么要有这么复杂的状态机?说白了就是为了处理各种边界情况。我曾经遇到过充气过程中患者突然咳嗽,导致压力瞬间飙升——如果没有错误状态处理,整个系统就死机了。
我建议的状态机实现要点:
- 状态切换用定时器驱动:不要用死循环等待,否则会阻塞蓝牙通信
- 每个状态设置超时保护:比如充气超过10秒还没到目标压力,直接进入错误状态
- 测量状态要实时保存数据:我一般用环形缓冲区,一边采集一边处理
- 计算状态完成后自动放气:这是安全要求,不能忘了
状态机伪代码示例:
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上,以及如何优化内存和计算速度。咱们下节课见。