我先把丑话说在前面:做这个项目之前,我也以为血压计是医疗器械,里面肯定有很复杂的算法和高精度的传感器,离爱好者很远。直到我自己查了一圈资料、把板子焊起来、跑通数据之后才发现,智能血压表的核心原理其实就一句话——通过袖带压力变化,捕捉脉搏波的幅度包络,再按比例系数算出收缩压和舒张压。原理简单,但要做到“测得准、重复性好”,需要抠的细节非常多。这篇文章就把我从零开始做智能血压表的完整思路、选型、电路、算法、代码和实测过程全部摊开来写,适合完全没接触过传感器信号处理、刚入门STM32或51单片机的朋友照着做。你要做的不是照抄一个固件,而是理解一条从物理信号到数字判定的完整链路。
1. 先搞懂血压计是怎么测血压的:示波法原理
1.1 不是直接“测压力”,是“看脉搏”
很多人有个误解,觉得血压计是把袖带勒紧,压力传感器直接读出血管里的压力。实际上医学级电子血压计普遍用的是示波法(Oscillometric Method),并不是直接测血压,而是测“袖带里的气压波动”。
完整过程分三步:
- 充气:气泵把袖带充到收缩压以上,一般设定在180~220 mmHg,这时袖带完全压闭动脉,血液暂时被“截住”,所以测得的压力是袖带气压,不是血压。
- 逐步放气:电磁阀以2~5 mmHg/s的速度缓慢放气。随着袖带压力下降,血管从完全压闭到逐渐恢复通畅,每一次心跳都会让袖带内的气体产生一个微小的压力脉动——这就是脉搏波。
- 同步采集:压力传感器同时测两路信息:一个是袖带的静态气压(缓慢下降的直流分量),一个是叠加在静态气压上的微小振荡波(交流分量,幅度只有1~3 mmHg,非常微弱)。
关键来了:脉搏波的幅度不是始终一样的。在袖带压力远高于收缩压时,血管完全压闭,脉搏波几乎为零;随着袖带放气接近收缩压,脉搏波开始出现;当袖带压力等于平均压(MAP)时,血管壁处于“受压和回弹”最灵敏的状态,脉搏波幅度达到最大;之后继续放气,脉搏波幅度又越来越小。
把每个心跳对应的脉搏波幅度连起来,会得到一个近似钟形的包络。这个包络的形状里,藏着收缩压、平均压和舒张压的全部信息。
1.2 三条判定规则决定测量结果的准确性
包络拿到之后,怎么从里面“读出”三个血压值?业内用得最多的有三类规则:
最大幅度法(Maximum Amplitude Algorithm,简称MAA):把包络上幅度最大的点对应的袖带压力记为平均压(MAP)。这是最稳定、最容易实现的判定点,因为最大峰值非常明显,抗噪能力相对强。
固定比例法(K值判定法):在包络的上升沿和下降沿按最大幅度的比例取点。不同厂商、不同文献采用的比例有差异,比较常见的做法是收缩压取上升沿幅度达到最大幅度的0.45~0.60倍的位置,舒张压取下降沿幅度衰减到最大幅度的0.70~0.80倍的位置。也有不少算法直接用0.5和0.75。比例选取的差异,是不同品牌血压计测量结果有偏差的重要原因之一。
斜率特征法:根据包络一阶导数的变化率来判断特征点。这种方法对稳定性的要求更高,通常用于算法调优,初学阶段不推荐直接上。
你现在不用纠结哪个比例“最正确”,初版代码用固定比例法就能跑通。真正的工程难点在于:包络必须够干净,也就是说每个脉搏波的峰值得要准。
1.3 一个简单的数学表达
用公式概括整个流程就是:
- 采集原始压力序列:
P_raw(t) - 从中提取基线压力:
P_base(t)(代表袖带静态气压) - 提取振荡波:
P_osc(t) = P_raw(t) - P_base(t) - 找出前100~300ms窗口内的最大峰峰值,得到幅度序列
A(i) - 寻找最大幅度
A_max,其对应压力=MAP - 在上升沿找
A(i) >= R1 * A_max的最小压力,记为SBP - 在下降沿找
A(i) <= R2 * A_max的最大压力,记为DBP
整个系统的精度,取决于P_raw(t)采得准不准、P_osc(t)提取得干不干净,以及A(i)峰峰值检测是否稳定。下面每个章节其实都是在解决这三个问题。
2. 核心器件选型:压力传感器、气泵、电磁阀与主控
2.1 压力传感器选型:带放大输出的最省事
做血压计最关键的就是压力传感器。市面常见的有两类:
- MPS3110:量程0~100 kPa(约750 mmHg),5V供电,输出0.5V~4.5V的模拟电压,内部集成了放大调理电路。到手不用额外搭复杂放大电路,直接接ADC即可,非常适合新手。
- XGZP6847:同样是带放大输出的压力传感器,量程也有0~100 kPa可选,I2C或模拟输出都有,灵敏度一致性不错,国内采购方便。
换算关系要弄清楚:传感器的电压输出和压力是线性关系,校准公式是:
P = (Vout - Vzero) / (Vfull - Vzero) * FullScale
以MPS3110为例,5V供电时Vzero约0.5V,Vfull约4.5V,所以:
P_mmHg = (Vout - 0.5) / 4.0 * 750
实测时,零点电压可能因为供电误差漂移,比如实际零点在0.512V而不是0.5V,这会导致全量程误差约十几mmHg,所以每次测量前建议做一个零点校准:先测量大气压下的输出电压,作为当次测量的零点基准。我做的第一版没有这个步骤,上电直接测,结果静态压力普遍偏高8~12 mmHg,后来加了开机零点校准才解决。
提示:如果选用的是4G/5G传感器这类需要外部放大的型号,务必把放大电路设计好,否则信号很容易被噪声吞掉。新手初版强烈建议选带放大输出的传感器,节省大量调试时间。
2.2 气泵和电磁阀:降压速度是决定成败的点
气泵负责充气,要能在几秒内把袖带充到180 mmHg以上。选择标准就三条:额定电压、最大压力、噪声。
我用的泵是5V/6V微型隔膜气泵,额定6V,实测5V供电时最大输出压力约300 mmHg,充气到180 mmHg大约5秒,电流约300~500 mA。泵的电流不能从单片机IO直接取,必须用NPN三极管或MOS管驱动,并且并联一个反向续流二极管,否则关断瞬间的反向电动势很容易打坏驱动管。
电磁阀负责放气。对于示波法,电磁阀的作用不仅仅是“放气”,而是要精确控制降压速度。理想状态是每秒钟下降2~5 mmHg。初学阶段可以不用PID闭环控制,用PWM配合固定节流孔也能达到近似效果:
- 放气阀全开时降压速度太快,包络采样点太少,判定误差大;
- 放气阀关闭时又完全不降压;
- 解决办法是用PWM控制电磁阀占空比,典型值:充气结束后阀关1秒,然后以20%~40%占空比间歇放气,具体占空比取决于阀的口径和袖带容腔大小,需要实测调整。
这里我有一个踩坑经历:第一次测试时电磁阀的通断直接产生了几十毫伏的电压跳变,叠加到压力信号上,导致包络上出现大量毛刺。后来在电磁阀两端并联了RC吸收电路(100Ω电阻串联100nF电容),同时把电磁阀的地线单点接地,问题明显缓解。
2.3 主控怎么选:51单片机做原型,STM32做产品
主控选择是这个项目最纠结的问题。标题里同时写了STM32和51,我的建议是:用51入门理解流程,用STM32实现更完整的产品。
51单片机的优势是简单、逻辑清晰,尤其适合初学阶段理解整个测量状态机。但受限也很明显:很多经典的51单片机(比如STC89C52)没有内置ADC,需要外挂ADC0832或ADS1115;RAM只有256字节,存储不了多少采样点;主频12MHz,定时器中断做采样时,CPU几乎没法同时做其他事。
如果你要用51做,建议选STC12C5A60S2这类增强型51:内置10位ADC、1KB以上RAM、支持双串口,还有硬件PWM,比老款89C52省掉大量外围芯片。10位ADC在0~5V范围内理论分辨率约4.9 mV,对应约0.73 mmHg,对于测量静态气压和提取脉搏波已经可用了。
STM32的优势则是采样率、精度和扩展性都大幅提升。我用的是STM32F103C8T6这个经典芯片:
- 12位ADC,分辨率约1.22 mV,对应约0.18 mmHg,精度直接上一个台阶;
- 支持定时器触发ADC + DMA传输,可以做到采样不占CPU;
- 后来我做的版本用DMA连续采8000个点(4 kHz采样率,采2秒),CPU同时去处理显示和按键响应,流畅多了。
简单总结:51适合搭原理验证机,STM32适合做“能日常用”的完整设备。下面各章节我会分别给出两边的关键代码思路。
2.4 供电与电机驱动电路
整机供电我建议用两套电源:数字部分(单片机、传感器、显示屏)用低压稳压,电机部分(气泵、电磁阀)单独直供。原因是气泵启动电流大,会引起电源电压跌落,ADC采集到的电压会跟着抖动,直接影响测量精度。
我实际使用的电源方案是:外部5V/2A DC输入,经AMS1117-3.3给STM32和OLED屏幕供电;气泵和电磁阀直接接5V,但电源线加粗,并联470μF电解电容减少启动压降。单片机与电机驱动之间用光耦隔离的话会更干净,初学版至少要做到电机独立走线、分开接地。
参考电路连接:
- 气泵驱动:NPN三极管(S8050)或N-MOS(AO3400)驱动,基极串1kΩ电阻接单片机PA4,并联1N4007续流二极管。
- 电磁阀驱动:同样用N-MOS驱动,PWM口接PA5。
- 压力传感器输出:接单片机的ADC输入引脚,供电5V。
3. 信号链路的细节:从传感器到ADC的每个坑
3.1 放大器增益设计
如果用了带放大输出的集成传感器,这节可以跳过。如果用差分输出的裸传感器(如压力桥式传感器),需要在传感器和ADC之间加一级仪表放大器(如AD620、AD8237)或者用运放搭差分放大电路。
设计增益的逻辑很简单:传感器的满量程差分输出一般只有几十毫伏,而ADC的满量程输入是0~3.3V或0~5V,必须把微弱信号放大到合适范围。比如满量程50 mV,输入范围3.3V,理论增益约66倍。但注意:血压测量的最大压力是250 mmHg,约占满量程1/3,增益可以适当降低,给动态范围留余量。我一般把增益设在50~100倍之间,然后在校准步骤里做软件修正。
还有一个容易忽略的问题:传感器零点失调电压。差分配对的传感器在零压时输出并不严格为0V,可能是2~10 mV,放大之后就变成0.2~1V的偏移。这个偏移如果没处理,会挤占ADC的可用动态范围。方法有两个:硬件上调零电位器,或者软件里做零点扣除。我建议用软件方法,每次测量前先把袖带完全泄压,采样一次作为零点,后面所有采样值减去零点。
3.2 滤波与采样节奏
传感器原始信号是叠加了工频干扰、气泵震动、电磁阀噪声的高频毛刺信号。脉搏波的频带主要集中在0.5~5 Hz,也就是心跳频率30~300次/分,正常心率范围60~120次/分换算成频率就是1~2 Hz。所以:
- 采样率不需要特别高,200 Hz~1 kHz足够。我最终用500 Hz。
- 软件要做一个带通滤波,把低频漂移和高频噪声都滤掉。
初学阶段不用上复杂的IIR/FIR设计,一个滑动平均+差分的组合就能达到可用效果:
// 滑动平均滤波,窗口大小从ADC采样率计算 #define FILTER_N 8 // 500Hz采样时,对应16ms时间窗 static uint16_t filter_buf[FILTER_N]; static uint8_t filter_idx = 0; static uint32_t filter_sum = 0; uint16_t smooth_filter(uint16_t new_sample) { filter_sum -= filter_buf[filter_idx]; filter_sum += new_sample; filter_buf[filter_idx] = new_sample; filter_idx = (filter_idx + 1) % FILTER_N; return filter_sum / FILTER_N; }第一版的滤波我用了50点滑动平均,结果包络被抹得太平,峰值识别不出来。后来改成8点窗口,效果明显改善。滤波窗口不是越大越好,窗口太大会把真正的波峰波谷削平,这个度需要你根据自己采样率实测。
3.3 压力值与脉搏波的“分离术”
把这个讲清楚,你就理解了整个血压算法的一半。原始采样序列P_raw是“基线压力 + 脉搏波”的叠加,要做的是把两者拆开。
做法一:低通滤波提取基线。对原始序列做很重的低通滤波(比如超出脉搏波频段的0.2 Hz截止频率),得到平滑的基线压力序列。然后用原始序列减去基线,就得到脉搏波序列。
做法二:逐拍分离。把原始序列按单个脉搏周期切开,每个脉搏周期内的最大值减去最小值,就是该拍的峰峰值。基线压力取该周期内的平均值。这种方法对实时性要求高,适合STM32这种性能较强的平台。
我实际用的是做法一的变体:先对原始序列做轻度滑动平均减少白噪声,然后用一个二次函数对每个脉搏波的局部区间做拟合,拟合值作为基线,残差作为脉搏波。实测效果不错,但代码略复杂。新手从做法一开始就行了,代码量最小,效果完全够用。
4. 算法落地:用最朴素的代码算出收缩压和舒张压
4.1 幅度包络的提取
所有算法都建立在“每个心跳对应一个脉搏波幅度”的基础上,所以首先要做波峰检测。
经典做法是阈值+滞回。设定一个阈值TH,当脉搏波信号从下往上穿过TH时记录一个候选峰,然后在其后的一段窗口内查找最大值作为波峰。为了避免噪声导致的误触发,阈值要设成动态的,比如取当前基线压力下历史脉搏波幅度的30%。
伪代码如下:
bool detect_peak(float current_value, float last_peak, float threshold) { static bool crossing = false; // 上穿阈值,标记开始寻找峰值 if (current_value > threshold && !crossing) { crossing = true; } // 已标记,且当前值小于阈值,说明一个脉搏波结束 if (current_value < threshold && crossing) { crossing = false; return true; // 当前周期结束,峰值取该周期内最大值 } return false; }把检测到的每个脉搏波的峰峰值和该周期对应的基线压力存到数组里,放气过程结束后,就得到一组「压力-幅度」序列。这个序列在坐标纸上描出来就是钟形包络。
4.2 最大振幅与K值判定
得到包络数组后,算法就进到后处理阶段:
- 找到最大幅度的下标
i_max,对应压力记为MAP。 - 从
i_max往前(上升沿)找第一个幅度大于0.50 * Amax的点,其压力记为SBP。为了避免单点毛刺,我建议用线性插值:找到相邻两个点一个小于0.50 * Amax一个大于,然后在这两点之间用线性插值求更精确的压力值。 - 从
i_max往后(下降沿)找第一个幅度小于0.75 * Amax的点,其压力记为DBP。
这里有两处需要特别注意的空值边界:
- 如果放气初始压力远高于收缩压,上升沿起始段幅度非常小,一旦出现第一个超过阈值的点,它就是收缩压候选。
- 如果袖带没有绑紧,或者放气太快导致包络采样点太少,下降沿可能还没来得及衰减到0.75倍就已经放完气了,舒张压就会明显偏高。
代码中用到的判定:
float find_sbp(int peak_index, float ratio, float amax) { float target = ratio * amax; for (int i = peak_index - 1; i >= 0; i--) { if (amp[i] <= target && amp[i+1] > target) { // 线性插值 float frac = (target - amp[i]) / (amp[i+1] - amp[i]); return pressure[i] + frac * (pressure[i+1] - pressure[i]); } } return pressure[0]; // 兜底 }4.3 代码骨架(51版 / STM32版)
51版(STC12C5A60S2)关键思路:
// 主循环为状态机:IDLE -> CHARGING -> MEASURING -> RELEASING -> RESULT void timer0_isr() interrupt 1 { static uint16_t adc_tick = 0; adc_tick++; // 以500Hz进入采样 if (measure_on) { uint16_t raw = ADC_GetValue(); // 10位ADC raw = smooth_filter(raw); // 滑动平均 extract_pulse(raw); // 提取脉搏波和基线 if (peak_detected) { store_amplitude(); } } // 用PWM控制电磁阀降低压速度 valve_control(); if (pressure < 30) { // 低于30mmHg结束测量 measurement_done = 1; } }51版要特别注意定时器中断不能做太多事,所以ADC读取、滤波、峰值检测都尽可能精简。我在51上把提取脉搏波的逻辑都放在中断里,主循环只负责保存数据和状态切换。8 MHz主频下,每次中断任务量控制在30μs以内,实测没有出现卡顿丢数据的情况。
STM32版关键思路:
// 定时器触发ADC采样,DMA搬运,无需CPU干预 void ADC_DMA_Init(void) { // TIM2触发ADC1,500Hz采样率 TIM2_Period = 1400-1; // 72MHz/500 = 144000 -> 1400经过分频得到500Hz ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2; ADC_DMACmd(ADC1, ENABLE); DMA_InitStructure.DMA_BufferSize = 1024; // 每1KB触发一次DMA中断 // ... } void DMA1_Channel1_IRQHandler(void) { // 数据就绪,对DMA缓冲区做处理 // 数据是连续压力序列,执行滤波、基线提取、脉搏波检测 // 清中断标志 }STM32版最大的好处就是处理能力充足,可以在DMA中断里做更多数据处理,主循环还能跑屏幕刷新。数据采集完之后包络数组在内存里都是连续的,后处理算法直接并行计算,体验顺畅很多。
5. 从51到STM32移植和扩展
5.1 51原型机时的定时器采样节奏
用51做原型时,最容易出问题的是采样节奏不稳。因为主循环里要处理按键扫描、LCD1602显示、泵阀控制,一旦某个耗时间的操作(比如向LCD写一长串字符串)插进主循环,定时器中断的响应仍然及时,但主循环对测量状态的管理会出现混乱。
解决办法是:把测量状态机的推进全部放在中断里,主循环只做最小事。我的做法:
- 定时器0以2ms周期中断,每次中断做ADC采样、滤波、脉搏波检测,并向一个字节数组写入结果。
- 主循环只在检测到
measure_done标志后,一次性把结果数组读出并计算血压。LCD刷新仅在非测量状态下执行。
这样主循环即使偶尔卡顿,中断采样也不会丢。51的2KB RAM(STC增强型)装800个采样点问题不大:每个点用两个字节存原始值和一个字节存基线差,压缩一下够用。如果RAM实在不够,可以把采样率降到250 Hz,然后缩短测量时间窗。
5.2 STM32版用DMA+ADC去解放CPU
STM32版我强烈建议直接用ADC+DMA,因为这是整个项目里最能体现STM32相比51优势的地方。
配置要点:
// 关键参数:500Hz采样率,DMA缓存1024点 // 每次DMA半传输完成中断处理前半段,传输完成中断处理后半段 // 这样保证了数据不丢失,CPU也有时间做其他事实测F103在72 MHz主频下,12位ADC + 4 kHz采样率连续采10秒,DMA搬运的数据量约40 KB,占整个DMA缓存很大一部分,但CPU占用率极低,主循环还能处理菜单和OLED动画。我在这个平台上加了历史记录存储,把最近20次测量结果存到内部Flash里,掉电不丢。
如果你要用STM32G0系列或者HAL库,思路完全一样,只是函数名有差异。F103的资料最多最全,建议新手选这个芯片。
5.3 人机交互与数据存储
第一个人机交互我建议别贪多,一个OLED(SSD1306,0.96寸)加上两个按键就够了。屏幕每屏显示三个数值:收缩压、舒张压、脉率,大字排版,这样测试的时候一眼就能看到结果。
数据存储方面:
- 51版就存到EEPROM(如AT24C02)或者STC单片机内部的EEPROM,每次测量后写一个JSON格式或自定义协议的数据块。
- STM32版直接操作内部Flash,F103的Flash是2KB一页,留一页专门存血压数据,写之前需要先擦除。注意Flash擦写寿命有限(约1万次),在家用场景完全没问题。
按键的处理逻辑很简单:一个“开始”键,一个“电源/停止”键。如果做进阶版,可以加蓝牙模块(HC-05或JDY-31)把数据发给手机App,但这不是必需的,先把基础测量做稳定比什么都重要。
6. 实测校准与误差分析
6.1 标定基础:血压计的一致性检查
刚做完硬件时,我拿它和水银血压计对比了一下,结果收缩压差了20多mmHg,舒张压差了10多mmHg,第一反应是“算法写错了”。排查了三天,最后发现是压力传感器的校准公式里零点漂移没有更新。所以提醒各位:拿到传感器后,第一件事就是校零。
具体做法:
- 开机后先把电磁阀完全打开,让袖带处于大气压状态。
- 采样100次取平均,记录为
pressure_zero。 - 后续所有压力计算都用
P = (Vout - pressure_zero) / (4.0) * 750。
这样修正后,静态压力误差能控制在±3 mmHg以内。
6.2 误差源分析
列一下我在测试中遇到的主要误差来源,方便你排查:
| 误差来源 | 现象 | 解决办法 |
|---|---|---|
| 零点漂移 | 所有读数整体偏高/偏低 | 每次开机校零 |
| 袖带尺寸不符 | 收缩压偏低而舒张压偏高 | 选用与手臂周长匹配的袖带 |
| 放气速度过快 | 包络采样点太少,判定码变动 | PWM放气控制在2~5mmHg/s |
| 气泵震动干扰 | 包络上出现高频毛刺 | 软硬件滤波 + 加橡胶减震垫 |
| 电源压降 | 传感器供电不稳导致ADC读数漂移 | 电机独立供电,加大电容 |
这里面最坑的是气泵震动干扰。传感器对压力变化极其敏感,气泵工作时的机械震动会通过外壳传到袖带里,产生一种“类脉搏”的周期信号,算法会误判成心跳。我在PCB上加了一段缓冲海绵,并把气泵用双面胶悬空固定,震动干扰才降下来。
6.3 实测中常见的伪差和处理
给你分享几个我第一次实测时搞得头大的场景:
场景一:袖带没绑紧。袖带缠得太松,充气时袖带往外鼓,脉搏波幅度很小但基线压力上升很快,最后包络形状完全失真。解决办法是绑袖带时保证能塞进两根手指为准。
场景二:测量过程中说话或移动。任何肌肉动作都会产生压力伪差。实测时我边看手机边测,结果连续三次数据都飘,后来老老实实坐着不说话,数据就稳定了。这不是算法问题,是生理反馈。
场景三:手臂位置不正确。测量时手臂要放在与心脏平齐的高度,如果手臂垂在桌面下面,静水压会额外加上约10 mmHg。这个误差是物理性的,算法再准也修正不了。
场景四:放气阀太小或太大。我第一版用的电磁阀口径偏大,放气速度达到每秒10 mmHg以上,包络上每个心跳之间几乎没有重叠的采样点,收缩压和舒张压都测不准。后来把PWM占空比下调,降压速度稳定在3 mmHg/s左右,测量结果跟参考血压计已经很接近了。
在校准阶段,我建议你找一台经过计量校准的电子血压计(或者去药店用水银血压计)做对照。连续测三组,每次间隔1分钟以上,记录自行数值和参考数值的差值。如果差值恒定,可以在软件里做一个线性修正:比如自测值普遍偏高8 mmHg,就在最终结果里减去8;如果差值随压力变化,就做两点修正,分别在高值和低值附近校正。
我还记得第一次把完整的电子血压表拿给家人测试时,屏幕显示出收缩压和舒张压的那一刻,心里特别有成就感——虽然它的精度离医用级还有距离,但作为学习项目,它把传感器采集、信号调理、滤波、特征提取、状态机和软硬件协同设计从头到尾整个链路都打通了,比单纯点灯、跑个串口外设学到的东西多太多。最后提醒一句:自制血压计只用于学习、实验和开发验证,不能作为医疗诊断设备。你要是想认真改进这个项目,可以往PID控速、蓝牙数据上传、心率变异性分析这些方向延伸,每一步都能学到大量实用知识。