从示波法原理到STM32实战:自制智能血压计完整指南 我先把丑话说在前面做这个项目之前我也以为血压计是医疗器械里面肯定有很复杂的算法和高精度的传感器离爱好者很远。直到我自己查了一圈资料、把板子焊起来、跑通数据之后才发现智能血压表的核心原理其实就一句话——通过袖带压力变化捕捉脉搏波的幅度包络再按比例系数算出收缩压和舒张压。原理简单但要做到“测得准、重复性好”需要抠的细节非常多。这篇文章就把我从零开始做智能血压表的完整思路、选型、电路、算法、代码和实测过程全部摊开来写适合完全没接触过传感器信号处理、刚入门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 mmHg5V供电输出0.5V~4.5V的模拟电压内部集成了放大调理电路。到手不用额外搭复杂放大电路直接接ADC即可非常适合新手。XGZP6847同样是带放大输出的压力传感器量程也有0~100 kPa可选I2C或模拟输出都有灵敏度一致性不错国内采购方便。换算关系要弄清楚传感器的电压输出和压力是线性关系校准公式是P (Vout - Vzero) / (Vfull - Vzero) * FullScale以MPS3110为例5V供电时Vzero约0.5VVfull约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或ADS1115RAM只有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-MOSAO3400驱动基极串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[i1] target) { // 线性插值 float frac (target - amp[i]) / (amp[i1] - amp[i]); return pressure[i] frac * (pressure[i1] - 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触发ADC1500Hz采样率 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 RAMSTC增强型装800个采样点问题不大每个点用两个字节存原始值和一个字节存基线差压缩一下够用。如果RAM实在不够可以把采样率降到250 Hz然后缩短测量时间窗。5.2 STM32版用DMAADC去解放CPUSTM32版我强烈建议直接用ADCDMA因为这是整个项目里最能体现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 人机交互与数据存储第一个人机交互我建议别贪多一个OLEDSSD13060.96寸加上两个按键就够了。屏幕每屏显示三个数值收缩压、舒张压、脉率大字排版这样测试的时候一眼就能看到结果。数据存储方面51版就存到EEPROM如AT24C02或者STC单片机内部的EEPROM每次测量后写一个JSON格式或自定义协议的数据块。STM32版直接操作内部FlashF103的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控速、蓝牙数据上传、心率变异性分析这些方向延伸每一步都能学到大量实用知识。