简介:基于STM32的电子血压计设计是一份可运行复现的嵌入式实践项目,面向电子竞赛、毕业设计及单片机入门学习者,用于解决血压测量系统的软硬件实现问题,帮助掌握STM32外设编程与传感器数据采集处理流程。资源压缩包共包含195个文件,大小仅3.39MB,以C源文件(35个)与H头文件(34个)为核心,辅以Keil工程配置、编译输出、链接脚本及备份文件等,层次清晰便于导入工程后直接阅读和修改。目前已有38人学习浏览,虽属小众资源,但内容经过严格测试,具备较好的完整性。项目中包含完整源码、工程文件与说明文档,覆盖了从外设初始化到数据处理的多个模块。既可用于毕业设计、课程设计的方案参考,也能作为电子竞赛的实际作品基础,方便在此基础上扩展无线传输、云端存储等更多功能。
1. 基于STM32的电子血压计:先搞懂示波法再动手
基于STM32的电子血压计,难的不是读气压,是怎么把藏在袖带压力波里的脉搏波抠出来。示波法(也叫振荡法)的测量逻辑是:给袖带充气到 160~180 mmHg 再缓慢放气,压力传感器采集到的是一路缓慢下降的基线压力,叠加在基线之上的小幅振荡就是每一拍心跳的脉搏波。脉搏波振幅从增到减的包络曲线,才是收缩压和舒张压的来源。这套设计用 STM32F103C8T6、模拟输出压力传感器、微型气泵和电磁阀就能完整搭起来,适合做毕业设计、嵌入式进阶练习,也适合想把家用血压计原理真正吃透的从业者。下面给的电路参数、滤波系数和判定阈值,都按可直接复现的口径来写。
2. 硬件链路:压力传感器、气泵与电磁阀怎么配
2.1 压力传感器怎么选:模拟输出与数字输出的取舍
示波法血压计对压力传感器的要求有两个:量程要覆盖 0~300 mmHg,动态响应要能跟上脉搏波。选型时我建议优先用模拟输出的压阻式传感器,比如 XGZP6847 或 MPX5050GP,量程 0~40 kPa(约 300 mmHg),满量程输出 4.5 V 左右,配合运放或者电阻分压后送入 STM32 的 ADC。数字输出的 BMP280 之类虽然 IIC 接口接起来省事,但量程只有 110 kPa 且响应偏慢,不适合测动态脉动压力,这是第一个要避开的坑。
供电和信号调理上有个细节:传感器用 5 V 供电,输出范围接近 0.5~4.5 V,而 STM32 的 ADC 输入范围是 0~3.3 V,直接接会超量程。我一般用两个电阻分压把信号拉到 0~3.0 V 以内,再串一个 100 Ω 电阻加 100 nF 电容到地做低通。分压电阻用 1% 精度,否则标定出来的斜率每一台都不一样。ADC 参考电压直接用 VDDA,如果板子上 3.3 V 纹波大,就需要单独加一颗 LDO 给模拟部分供电。
下面是一份常用的引脚分配表,可以直接对照接线:
| 功能 | STM32 引脚 | 说明 |
|---|---|---|
| 压力传感器 ADC 输入 | PA0 / ADC1_IN0 | 分压后接入,采样率 100 Hz |
| 气泵 PWM 驱动 | PA1 / TIM2_CH2 | 20 kHz PWM,控制充气速度 |
| 电磁阀控制 | PB0 | 推挽输出,高电平打开快放阀 |
| OLED / LCD 显示 | PB10~PB15 | SPI 接口,具体按屏的引脚定义 |
| 按键启动 | PB1 | 内部上拉,按下为低电平 |
| 蜂鸣器 | PB2 | 测量结束提示音 |
| 串口调试 | PA9 / PA10 | USART1,用于导出波形和标定数据 |
2.2 气泵与电磁阀:充气要快、放气要稳
充气部分用微型隔膜泵,额定 5 V 或 6 V,电流 300~500 mA,不能直接接在 STM32 的 GPIO 上,需要一颗 NPN 三极管或者 AO3400 MOS 管做开关。气泵启动瞬间电流大,会在电源线上拉出明显纹波,如果压力传感器和泵共用一根电源线,ADC 读数会跟着跳。常见的做法是气泵单独走一路电源,或者至少在泵的电源引脚并一颗 470 μF 电解电容。
放气是示波法成败的关键。电磁阀选常闭型两位两通,断电时关断,通电时快速放气。但要注意,示波法测量阶段需要的是 2~5 mmHg/s 的缓慢均匀放气,直接用电磁阀做慢放很难控制,因为电磁阀的流通口径太大,PWM 占空比稍微一动放气速率就突变。我习惯在气路里串一个 0.6~0.8 mm 的机械节流嘴做慢放,电磁阀只负责测量结束后的快速放气,这样气路简单且重复性好。
充气目标压力不用太高,成年人上臂测量建议 160~170 mmHg,儿童或瘦弱人群可以降到 140 mmHg。判断是否到目标压力,直接读传感器气压值就行,不需要定时器估算。充气阶段只控制气泵,不采集脉搏波数据,这一点后面避坑章节会细说。
2.3 STM32 外设分配:一套够用的引脚划分
主控选 STM32F103C8T6 就够跑完整套逻辑,原因很简单:一颗 ADC 采集压力,一颗定时器输出 PWM 驱动气泵,一颗定时器做 100 Hz 采样节拍,再加几根 GPIO 控制电磁阀、按键和蜂鸣器,资源完全没有压力。代码工程我习惯直接用标准库新建工程,HAL 库也可以,流程没有本质差别,只是外设初始化写法不同。
ADC 采集这里有个重要细节:如果同一个 ADC 还要采集电池电压或者温度,切换通道之后不能立刻读数。ADC 的采样电容需要时间充电,切换通道后至少要等几十微秒,或者丢弃第一次采样值。我在采样函数里固定做一次无效转换再取有效值,能省掉很多莫名其妙的跳变问题。
定时器安排上,TIM3 做 100 Hz 采样中断,每次中断读一次 ADC 并更新滤波缓冲区;TIM2 的 PWM 通道给气泵调速。串口调试用 USART1,引脚定义是 PA9/PA10,调试阶段每秒输出一次压力值和脉搏波幅值,方便用串口绘图工具看波形。
3. 示波法算法落地:从气压波到收缩压与舒张压
3.1 先标定:把 ADC 值换算成毫米汞柱
算法开始前必须先做标定。压力传感器经过分压后进入 ADC,ADC 读数和实际气压之间是线性关系,标定的目的就是求出斜率和零点。最笨但可靠的办法是:压力传感器不接袖带时读一个零点,再用一个已知压力的气源(比如水银血压计或者精密气压表)读一个高点,两点确定一条直线。
标定公式我写成这样:
#define ADC_ZERO 1280 /* 零点 ADC 值,实测得到 */ #define ADC_FULL 2860 /* 满量程点 ADC 值 */ #define PRESSURE_FULL 300.0f /* 满量程对应 300 mmHg */ float adc_to_pressure(uint16_t adc) { float p; /* 线性插值:先算当前 ADC 在零点与满量程之间的比例 */ p = (float)(adc - ADC_ZERO) / (float)(ADC_FULL - ADC_ZERO); /* 再按量程换算成 mmHg,负值截断为 0 */ p *= PRESSURE_FULL; if (p < 0.0f) p = 0.0f; return p; }这段代码的核心是线性插值,ADC_ZERO 和 ADC_FULL 必须换成你自己板子上实测的值。怎么测?把传感器接到大气压,读到的 ADC 平均值就是 ADC_ZERO;接一个校好的 300 mmHg 气源,读到的就是 ADC_FULL。没有标准气源时,可以用水银血压计打压,记录几个不同压力点,用最小二乘法拟合出斜率。
这里容易翻车的地方是零点漂移。传感器上电后前几秒输出会有缓慢漂移,我的做法是每次测量启动前先读一次当前 ADC 作为零点,而不是把固定值写死在程序里。零点漂移会造成收缩压、舒张压整体偏移,属于系统性误差,后面避坑章节会再提到。
3.2 分离基线压力与脉搏波:滑动平均做的事情
袖带放气过程中,压力传感器读到的原始信号由两部分组成:一路从 170 mmHg 缓慢下降的基线压力,以及叠加在基线上、幅度 1~5 mmHg 的脉搏波。要计算血压,必须先把基线去掉,留下干净的脉搏波。
去掉基线最常见的方法是滑动平均。采样率 100 Hz,基线用 1 秒窗口(100 个点)滑动平均,得到的就是跟随袖带压力的慢变基线,原始值减基线就是脉搏波:
#define SAMPLE_RATE 100 /* 采样率 100 Hz */ #define BASE_WINDOW 100 /* 基线窗口 100 个点,即 1 秒 */ #define PRES_RAW_BUF 128 /* 原始压力环形缓冲区 */ float pres_raw_buf[PRES_RAW_BUF]; uint8_t pres_head = 0; float pres_base = 0.0f; float pres_osc = 0.0f; /* 采样中断里每 10ms 调用一次 */ void pressure_process(float pres_raw) { static float sum = 0.0f; static uint8_t cnt = 0; /* 先把新值写入环形缓冲区,覆盖最老的值 */ sum -= pres_raw_buf[pres_head]; pres_raw_buf[pres_head] = pres_raw; sum += pres_raw_buf[pres_head]; pres_head = (pres_head + 1) & (PRES_RAW_BUF - 1); if (cnt < BASE_WINDOW) cnt++; /* 基线 = 窗口内平均值,脉搏波 = 原始值 - 基线 */ if (cnt == BASE_WINDOW) { pres_base = sum / BASE_WINDOW; pres_osc = pres_raw - pres_base; } }逻辑上需要注意:窗口选 1 秒,是为了让基线既能跟得上袖带压力的缓慢下降,又能把 0.5~5 Hz 的脉搏波完整保留下来。窗口太短,比如 200 ms,基线会跟着脉搏波起伏,把真实的脉搏波振幅吃掉一部分;窗口太长,比如 2 秒,放气较快时基线跟不上压力下降,脉搏波会叠加一个斜坡分量。
实际使用中,窗口长度要和你设定的放气速率匹配。放气速率 3 mmHg/s 时,1 秒内基线只变化 3 mmHg,窗口内的波形近似水平,减出来的脉搏波最干净。如果放气速率调到了 5 mmHg/s,窗口建议缩短到 80 个点,否则基线滞后会引入误差。
3.3 包络与特征点:收缩压、舒张压从哪两个位置读出来
干净的脉搏波序列拿到后,下一步是找包络。每拍心跳对应一个脉搏波,波峰减波谷就是这一拍的振荡幅度。放气过程中,振荡幅度会先增大后减小,包络的峰值位置对应的袖带压力就是平均压 MAP,而收缩压和舒张压分别位于包络上升段和下降段的特定比例位置。
我在工程里用的判定比例是 0.55 和 0.75:包络从峰值往前找,幅度降到峰值 0.55 倍时对应的压力是收缩压;包络从峰值往后找,幅度降到 0.75 倍时对应的压力是舒张压。具体代码如下:
#define SBP_RATIO 0.55f /* 收缩压对应包络上升段的幅度比例 */ #define DBP_RATIO 0.75f /* 舒张压对应包络下降段的幅度比例 */ typedef struct { float pressure; /* 袖带压力 */ float amplitude; /* 当前拍脉搏波幅度 */ } beat_info_t; #define MAX_BEATS 128 beat_info_t beats[MAX_BEATS]; uint8_t beat_cnt = 0; /* 在放气阶段,每检测到一拍完整脉搏波就存储一次 */ int find_blood_pressure(float *sbp, float *dbp, float *map) { uint8_t i, peak_idx = 0; float max_amp = 0.0f, ratio; if (beat_cnt < 10) return -1; /* 数据太少,判定失败 */ /* 找包络峰值,峰值处压力就是平均压 */ for (i = 0; i < beat_cnt; i++) { if (beats[i].amplitude > max_amp) { max_amp = beats[i].amplitude; peak_idx = i; } } *map = beats[peak_idx].pressure; /* 从峰值往前找收缩压点 */ *sbp = beats[0].pressure; for (i = peak_idx; i > 0; i--) { ratio = beats[i].amplitude / max_amp; if (ratio <= SBP_RATIO) { /* 线性插值,避免只取离散点造成读数跳变 */ float r0 = beats[i].amplitude / max_amp; float r1 = beats[i + 1].amplitude / max_amp; float t = (SBP_RATIO - r0) / (r1 - r0); *sbp = beats[i].pressure + t * (beats[i + 1].pressure - beats[i].pressure); break; } } /* 从峰值往后找舒张压点 */ *dbp = beats[beat_cnt - 1].pressure; for (i = peak_idx; i < beat_cnt - 1; i++) { ratio = beats[i].amplitude / max_amp; if (ratio <= DBP_RATIO) { float r0 = beats[i].amplitude / max_amp; float r1 = beats[i + 1].amplitude / max_amp; float t = (DBP_RATIO - r0) / (r1 - r0); *dbp = beats[i].pressure + t * (beats[i + 1].pressure - beats[i].pressure); break; } } return 0; }这两个比例值不是拍脑袋定的。0.55 和 0.75 来自示波法血压计多年积累的经验范围:收缩压通常在包络上升到峰值的 45%~57% 位置,舒张压通常在下降到峰值的 72%~82% 位置。不同厂家的算法差异也主要在这两个系数上。
讲几个调参方向:如果你用同一套设备和自己对比,收缩压偏高,就把 SBP_RATIO 调大一点,比如从 0.55 调到 0.60;舒张压偏低,就把 DBP_RATIO 从 0.75 调到 0.70。调参必须有数据支撑,每改一次系数,至少做三组对照测量再决定。
3.4 心率计算与无效数据剔除
脉搏波除了用来判血压,还能直接算心率。计算方法是统计相邻两拍波峰之间的时间间隔,再换算成每分钟次数。100 Hz 采样率下,如果连续三拍的间隔都在 600~1500 ms 范围内(对应 40~100 次/分),就取平均值作为心率,超出范围直接丢弃,不做加权。
无效数据剔除我放在包络存储之前。如果某一拍脉搏波幅度小于前几拍平均幅度的 30%,基本可以判定是身体移动或者咳嗽造成的干扰,这一拍不存进 beats 数组。如果放气过程中出现连续三拍幅度异常,直接终止测量,触发重测提示,而不是硬算一个血压值出来。这个保护逻辑很重要,不然测量结果会经常出现收缩压 90、舒张压 140 这种明显错误。
4. 状态机与驱动:仪器靠什么按顺序干完一件事
4.1 主状态机:五个状态走完一次测量
血压计不能把充气、测量、放气混在一起乱写,状态机是这套程序的主干。我把测量流程分成五个状态:空闲、充气、测量、放气、完成。空闲时只有按键能触发启动,充气阶段只加压不采集脉搏波,测量阶段停泵并依靠节流嘴缓慢放气,放气阶段全开电磁阀快速泄压,完成后显示结果并进入待机。
typedef enum { BP_IDLE = 0, BP_INFLATE, BP_MEASURE, BP_DEFLATE, BP_DONE } bp_state_t; bp_state_t bp_state = BP_IDLE; uint8_t key_pressed = 0; void bp_state_machine(void) { switch (bp_state) { case BP_IDLE: /* 按键消抖确认后启动,打开气泵 */ if (key_pressed) { pump_on(); valve_close(); bp_state = BP_INFLATE; key_pressed = 0; } break; case BP_INFLATE: /* 压力到 170 mmHg 停泵,开始测量 */ if (pressure_now() >= 170.0f) { pump_off(); bp_state = BP_MEASURE; beat_cnt = 0; /* 清空上一轮包络数据 */ } break; case BP_MEASURE: /* 测量阶段采集脉搏波,直到压力降到 60 mmHg 以下 */ if (pressure_now() < 60.0f) { valve_open(); bp_state = BP_DEFLATE; } break; case BP_DEFLATE: /* 快速放气,压力归零后回到空闲 */ if (pressure_now() < 5.0f) { valve_close(); bp_state = BP_DONE; } break; default: break; } }状态机的执行频率放在主循环里,每次 10 ms 跑一次。注意充气阶段的判断条件用的是当前压力而不是充气时间,这是因为气泵个体差异大,同一款泵在不同电压下的充气速度能差 30%,用定时器预估压力非常不靠谱。放气阶段的结束条件是压力降到 60 mmHg 以下,此时包络已经明显消失,再继续采集没有意义。
按键消抖我直接用了 20 ms 延时轮询,简单可靠。如果你用的是 SysTick 做延时,注意不要在中断服务函数里调 delay 函数,否则会出现 stm32 延时函数 delay 卡死的典型问题。
4.2 气泵与电磁阀驱动:PWM 参数怎么给
气泵驱动我用的 20 kHz PWM,占空比分两档:充气前期 100% 全速加压,接近目标压力时降到 60% 防止过冲。判断是否接近目标压力,留了 10 mmHg 的提前量,比如目标 170 mmHg,压力到 160 mmHg 时就要切到低占空比。这样做是为了避免惯性过冲,气泵断电后气路内压力还会惯性上升几毫米汞柱。
电磁阀控制没有用 PWM,直接推挽输出高电平打开、低电平关闭。测量阶段电磁阀必须保持关闭,此时如果从串口看到电磁阀发烫,多半是阀体选型有问题,正常常闭阀断电状态不通电不应该发热。只有在进入 BP_DEFLATE 状态时才打开电磁阀,并且当压力降到 5 mmHg 以下就立刻关闭,避免长时间通电。
充气阶段和测量阶段的切换要干净利落。我遇到过气泵刚断电那几十毫秒,压力传感器读数明显抖动,这是泵体振动传导到气路造成的。解决办法是等 200 ms 再开始采集脉搏波,给气路一个稳定时间。这段延时放在状态切换处,不影响整体测量时长。
4.3 显示与按键:OLED 绘制曲线、启动测量
显示部分我建议用 0.96 寸 OLED,SPI 接口,刷新速度足够画实时压力曲线。工程里显示三块内容:当前袖带压力、实时脉搏波波形、测量结束后的收缩压/舒张压/心率。OLED 刷一屏全幅数据大约 30 ms,放在状态机之外单独跑,不占用采样中断的时间。
如果你用的是 ILI9341 这类 SPI 彩屏,初始化第一步先读屏幕 ID,读回来是 0xA1A1 大概率是接线反了或者 SPI 时序不对,这是排查屏不亮的黄金入口。屏幕显示中文时,字模工具生成的是 GBK 编码,Keil 工程里写中文字符串前先把编码统一成 UTF-8 或者直接在取模阶段转好,否则屏幕上会出乱码。
按键我接在 PB1 上,内部上拉,按下为低。消抖逻辑是检测到低电平后延时 20 ms 再确认一次,两次都是低才认为有效。按键响应后蜂鸣器响一声作为反馈,测量结束再响三声作为提示。整套交互不需要复杂菜单,一个按键长按 1 秒开机测量,测量完成自动待机,做设备交互时够用。
5. 避坑笔记:气压检测与气路里最常见的五个问题
5.1 血压值忽高忽低,换个人就不准
现象:同一个人连续测两次,收缩压能差 15 mmHg 以上;换个人测,读数整体偏高或偏低。
原因:最常见的是气路漏气,袖带接口和管路接头没拧紧,或者袖带本身老化。漏气会让放气速率不稳定,包络波形变形。另一个原因是零点漂移没处理,每次上电传感器零点都不一样,固定的 ADC_ZERO 会让所有读数整体偏移。
解决:每个气路接口用快插接头加密封圈,上电后先做零点校准再进测量流程。袖带绑缚时统一松紧度,以能塞进一根手指为准,否则不同人之间的测量结果没有可比性。
5.2 ADC 采集乱跳,压力曲线像锯齿
现象:串口导出的压力数据在静止状态下波动超过 ±3 mmHg,脉搏波完全淹没在噪声里。
原因:气泵和传感器共地,泵启动瞬间电流拉低电源电压,导致 ADC 参考电压不稳。也可能是传感器分压电阻过大,ADC 输入阻抗不够,信号驱动能力不足。
解决:气泵电源单独走线,模拟地和功率地在主电容处单点汇合。传感器输出串联 100 Ω 电阻,并联 100 nF 电容做低通。同一颗 ADC 切换通道时,切换后丢弃第一次采样结果,等采样电容稳定后再取有效值。
5.3 放气太快,包络只有两三个点没法判
现象:测量阶段压力从 170 mmHg 掉到 60 mmHg 只用了几秒,beats 数组里只存了两三拍数据,收缩压和舒张压判定直接失败。
原因:机械节流嘴孔径选大了,放气速率达到 10 mmHg/s 以上。示波法要求放气速率控制在 2~5 mmHg/s,太快会导致包络采样点过稀,特征点定位误差成倍放大。
解决:换 0.5~0.8 mm 内径的节流嘴,并在调试阶段用串口实测放气曲线,确保压力线性下降。不要用电磁阀 PWM 去硬凑慢放速率,电磁阀在小占空比下流量非线性严重,一致性很差。
5.4 脉搏波在充气阶段被泵振动淹没
现象:整个测量过程都在采脉搏波,充气阶段波形乱跳,包络峰值找不准。
原因:气泵的机械振动通过气路传到压力传感器,振动频率和脉搏波频段重叠,滤波器很难完全分离。
解决:充气阶段只做压力控制,不采集包络数据,停泵后等 200 ms 再开始测量。这是我一开始没注意的坑,后来看串口波形才发现充气阶段的“脉搏波”其实是泵振动。状态机里把采集窗口严格限定在 BP_MEASURE 状态,问题立刻消失。
5.5 电磁阀一开就烫手,测完一次还漏气
现象:电磁阀在测量过程中发热明显,放完气后阀体关闭不严,压力缓慢往下掉。
原因:误用了 12 V 电磁阀却在 5 V 供电下长时间通电,或者电磁阀类型选错,选成了常开型。阀体发热还会导致密封圈变形,关闭后漏气。
解决:选 5 V 常闭型电磁阀,只在快速放气阶段通电,压力归零立即断电。如果必须长时间保持打开状态,用 20 Hz、30% 占空比的 PWM 驱动降低维持电流,但一般情况下不该出现这种需求。
6. 验证方法:和标准血压计对比的一次完整流程
6.1 对照表:和标准血压计记同一组数据
设备运行稳定后,最关键的验证是和经过校准的水银血压计或医用电子血压计做对比。测试流程我固定成这样:受测者静坐 5 分钟,同一只手臂先用水银计测一次,等 1 分钟再用自制的电子血压计测一次,交替共测 5 组。记录表格如下:
| 组号 | 设备收缩压 mmHg | 设备舒张压 mmHg | 标准收缩压 mmHg | 标准舒张压 mmHg |
|---|---|---|---|---|
| 1 | 128 | 82 | 126 | 80 |
| 2 | 131 | 85 | 128 | 82 |
| 3 | 127 | 80 | 127 | 81 |
| 4 | 133 | 84 | 130 | 83 |
| 5 | 129 | 81 | 128 | 81 |
评估时看两个口径:平均误差要在 ±5 mmHg 以内,标准差不超过 8 mmHg,这是业内常用的血压计准确性参考口径。平均误差反映系统偏差,标准差反映重复性。如果平均误差超限,优先检查零点标定和两个比例系数;如果标准差超限,优先检查气路漏气和放气速率稳定性。
6.2 串口把原始波形导出来,逐拍检查
对比测试不过关时,光看最终血压值很难定位问题。我的习惯是把原始压力、基线和脉搏波通过串口同时导出,每秒打 100 行,用串口绘图工具直接看曲线。USART1 重定向 printf 到串口助手,波特率 115200,输出格式用逗号分隔,方便导入 Excel 或 SerialPlot 绘图。
检查波形时重点看三处:放气阶段压力是否线性下降,脉搏波是否清晰且没有削顶,包络包峰值是否出现在压力下降到 MAP 附近的位置。如果脉搏波在放气初期被削顶,说明滤波窗口太短或放气速率太快;如果包络峰值偏到很后面,说明 DBP_RATIO 需要调整。每改一个参数,就重新走一遍上面的对比流程,波形和表格都存档,方便回溯是哪一次改动把指标改坏了。
从那以后我每次调完算法或者换气路零件,都会强制走一遍串口导出波形加 5 组对比测试的流程,先看波形再下结论,不看数据不调参。这套习惯帮我避开了至少半打“感觉合理但实际是错”的改动,希望帮到你。
本文还有配套的精品资源,点击获取