简介:基于STM32设计的电子血压计完整项目包,面向嵌入式方向的学生与开发者,适用于课程设计、毕业设计、工程实训及学科竞赛等场景。资源已经过严格测试可直接运行,包含完整源码、工程文件及使用说明,可帮助快速复刻血压计功能并与实际硬件联调,也适合在此基础上扩展物联网或显示交互功能。压缩包内含161个文件,以C源文件、H头文件为主,辅以编译配置文件、链接脚本、Hex烧录文件等,整体仅2.21MB,结构清晰,便于查阅和移植。目前已有114人学习参考,作者具备丰富的单片机开发经验,如遇问题可在CSDN站内联系交流;对不熟悉PCB设计的小白,也可按引脚定义改用面包板、杜邦线与模块方式完成搭建,下载源码烧录后即可体验完整功能,是兼具学习价值与工程参考意义的嵌入式优质项目。
1. 很多毕设方案卡在“看不到脉搏波”这一步
拿到“基于STM32的电子血压计”这套工程文件,最容易踩的误区是先做界面、先调屏幕,最后才接压力传感器,然后发现采集到的数值忽大忽小,甚至根本测不出来。这不是传感器坏了,而是电子血压计本质上不是“测气压的工具”,它要在袖带放气过程中,从压力信号里剥离出一个很微弱的脉搏波动,再根据波动幅值的变化反推收缩压和舒张压。这个思路叫示波法,也是目前绝大多数电子血压计产品采用的方法。整套系统的技术分水岭不在 UI,而在 ADC 采样链路、脉搏波提取、包络判定和放气控制这几个环节。这篇文字就按这个顺序,把从传感器选型到标定验证的完整路径捋一遍,适合做毕设、课设、竞赛和实训项目的读者对照着实现。
2. 从示波法原理到 STM32 模拟前端选型
2.1 示波法的测量模型:先理解血压数值是怎么算出来的
先明确一个概念,示波法测得不是“某个时刻的气压值”,而是“袖带压变化过程中脉搏波振荡幅值的趋势”。袖带充气到高于收缩压后开始缓慢放气,此时压力传感器同时拾取两路信号:一路是袖带气压本身的直流分量,另一路是血管搏动传导到袖带气路里的微小压力波动。随着袖带压从高位逐渐下降,这个波动的幅值会经历一个“从小到大再变小”的过程,峰值位置对应平均压 MAP,收缩压出现在包络上升侧幅值约为峰值 0.5 倍的位置,舒张压出现在下降侧约为 0.8 倍的位置。
这个比值关系是示波法判定血压的关键参数,不同文献和不同厂商的取值略有差异。做毕设或课程设计时,通常先用 0.5 和 0.8 作为初始系数,再通过和标准血压计对比做整体修正。理解这个模型后,后面所有硬件选型、采样率设置和滤波算法都有了明确的依据,而不是盲目地“把 ADC 读数显示到屏幕上”。
用听诊法做血压计需要拾音头、声音识别和复杂的时频分析,在单片机上的实现成本明显更高。示波法只需要一路模拟压力输入,加上稳定的放气控制,就能在 STM32 上完整跑通,这也是它在电子竞赛和毕业设计里被反复选择的原因。
2.2 压力传感器选型:MPX5050GP 能省掉一级运放
压力传感器是模拟前端最核心的器件,它决定了脉搏波信号的信噪比。毕设项目里最常见的选型是飞思卡尔/恩智浦的 MPX5050GP,这个传感器内部已经集成了放大和温度补偿电路,输出是一个随绝对压力线性变化的模拟电压,不需要再外接仪表放大器,对 PCB 布板和调试都友好。
| 型号 | 量程 | 输出方式 | 供电电压 | 说明 |
|---|---|---|---|---|
| MPX5050GP | 0~50kPa | 模拟电压,约 0.2V~4.7V | 5V | 片内放大,带温度补偿,使用最普遍 |
| XGZP6847 | 可按需选择量程 | I2C 或模拟输出 | 3.3V / 5V | 国产传感器,采购渠道多,模拟版本更贴合本项目 |
| MS5525DSO | 绝压/差压 | I2C | 3.3V | 数字输出,省 AD 前端,但需要驱动协议栈 |
血压测量范围一般在 0~300mmHg,约 40kPa,对应 MPX5050GP 的输出电压在 0.2V 到 3.8V 之间。如果 STM32 的 ADC 参考电压是 3.3V,这个输出范围会超出量程,常见的处理方式是用两个电阻分压后再送入 ADC,或者选择 3.3V 供电的传感器版本。分压电路上要并联一个 100nF 电容做低通滤波,截止频率选在 1kHz 以上,避免滤掉有效的脉搏波分量。
2.3 气泵、电磁阀与驱动电路
电子血压计的气路系统由微型气泵和常闭型电磁阀组成。气泵负责给袖带充气,电磁阀负责在测量阶段受控放气,并在测量结束后快速泄压。这两个部件都是感性负载,启动电流通常有 300mA 到 500mA,不能用 GPIO 直接驱动。常见做法是各用一个 NPN 三极管或 N-MOS 管做低端驱动,并在电机或电磁阀两端反向并联一个续流二极管,防止关断瞬间的反向电动势干扰 ADC 采样。
| 部件 | STM32 引脚示例 | 驱动方式 | 备注 |
|---|---|---|---|
| 气泵 | PA1 | NPN/NMOS 低端驱动,PWM 或开关控制 | 充气阶段直接开启,到位后关闭 |
| 电磁阀 | PA2 | NPN/NMOS,续流二极管必需 | 放气测量阶段用 PWM 控制开度 |
| 蜂鸣器 | PA3 | NPN 驱动 | 测量完成提示音 |
| 按键 | PB0 | 内部上拉,下降沿触发 | 控制测量开始 |
提示:如果手头有 GD32、APM32 这类国产兼容芯片,工程可以直接套用 STM32 的代码框架,但必须要重新核对 ADC 参考电压和定时器时钟频率,尤其是模拟外设的驱动配置。
2.4 供电与 PCB 布局的基本要求
气泵和电磁阀工作时会通过电源线把噪声耦合进模拟电路,这是很多工程“传感器读数乱跳”的根源。PCB 布局上要把泵的电源地单独走线,在电源入口处汇总,不要让泵的大电流流过压力传感器的模拟地。STM32 的 VREF+ 引脚附近要放 100nF 和 10µF 去耦电容,模拟供电串联一个 10µH 电感和 10µF 电容做 LC 滤波。如果用的是 5V 传感器加 3.3V MCU 的方案,电平转换电阻要靠近 ADC 引脚放置,走线尽量短,减少天线效应。这一层做好,后期算法调试会顺利得多。
3. 基于 STM32 定时器触发 ADC 和 DMA 的 200Hz 采样链路
3.1 为什么采样率要定在 200Hz
脉搏波的主要能量集中在 0.8Hz 到 3Hz,心率 180 次/分时也才 3Hz。理论上 20Hz 采样就够,但实际做包络峰值检测时,每个心跳周期至少要采到 30~50 个点,否则峰值幅度容易被漏掉,所以在示波法血压计里常用 200Hz 作为采样率,也就是 5ms 一个点。这个值对 F103 这类主频 72MHz 的芯片来说没有任何压力,同时还能满足 1.5Hz 脉搏波信号 10 倍以上的过采样要求。
采样方式不能在主循环里用HAL_ADC_GetValue轮询,因为血压计运行期间还同时要做放气控制、LCD 刷新和串口输出,轮询会让采样间隔抖动,导致脉搏波的幅度不稳定。正确做法是:定时器产生 200Hz 的触发信号给 ADC,ADC 转换完成后由 DMA 自动把结果搬到内存,CPU 只消费数据,只在对齐好的缓冲区里取数。
3.2 CubeMX 配置与 ADC DMA 初始化代码
先看定时器配置,在 STM32CubeMX 里把 TIM6 的时钟源选为内部时钟,预分频值设为 7199,自动重载值设为 49,就能从 72MHz 得到 200Hz 触发频率。对应 HAL 代码如下:
static void MX_TIM6_Init(void) { TIM_HandleTypeDef htim6; htim6.Instance = TIM6; htim6.Init.Prescaler = 7200 - 1; /* 72MHz / 7200 = 10kHz */ htim6.Init.Period = 50 - 1; /* 10kHz / 50 = 200Hz */ HAL_TIM_Base_Init(&htim6); /* 把 TIM6 更新事件作为内部触发输出 */ TIM_MasterConfigTypeDef master_config = {0}; master_config.MasterOutputTrigger = TIM_TRGO_UPDATE; HAL_TIMEx_MasterConfigSynchronization(&htim6, &master_config); }逻辑说明:预分频器把 72MHz 降成 10kHz,计数器再从 0 数到 49,每 50 个计数周期输出一次更新事件,最终得到 200Hz 的 TRGO 触发信号。MasterOutputTrigger这行不能省,它是把定时器更新事件引到内部触发总线上的关键。
接下来初始化 ADC 为外部触发模式:
static void MX_ADC1_Init(void) { ADC_HandleTypeDef hadc1; hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = DISABLE; /* 单通道模式 */ hadc1.Init.ContinuousConvMode = DISABLE; /* 必须由定时器触发 */ hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T6_TRGO; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; HAL_ADC_Init(&hadc1); }逻辑说明:ContinuousConvMode设为 DISABLE,是为了让每次转换都由 TIM6 触发,保证采样间隔严格一致。ScanConvMode在只采一路压力信号时关闭,如果还要同时采袖带温度或电池电压,则开启并配置通道序列。
启动采集的代码放在主程序里,两句话就能跑起来:
static uint16_t adc_buf[256]; HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 256); HAL_TIM_Base_Start(&htim6);这里 DMA 工作在循环模式,256 个缓冲区对应 1.28 秒的数据量,足够算法在后台消费。DMA 中断被使能后,每采满 128 个点触发一次半传输中断,每 256 个点触发一次完整传输中断,CPU 在这两个中断中交替取数,避免读到半个缓冲区时数据被覆盖。如果发现采样率不对或有丢点,优先检查 TIM6 的时钟源是否挂在 APB1 总线上,以及 DMA 的循环模式是否配置成功。
3.3 双缓冲处理:不让算法阻塞采集
ADC 通道只有一路时,用循环 DMA 加半传输中断就足够了。两个中断回调里各处理一半数据,逻辑简单可靠:
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { process_sample_block(&adc_buf[0], 128); } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { process_sample_block(&adc_buf[128], 128); } }说明:process_sample_block内部不应做 LCD 绘制、延时这类耗时操作,只负责把原始 ADC 值存到环形队列并做轻量级滤波。显示、状态机、串口输出都在主循环里执行,用标志位通知主循环“有新数据可读”。这个框架就是 STM32 里典型的 DMA+ADC 双缓冲用法,把它跑稳定,后面加滤波算法、包络检测时不用再回头改采集层。
4. 示波法核心算法:从原始信号到脉搏波包络
4.1 信号分离:从 ADC 原始值中抽出脉搏波
压力传感器输出到 ADC 的数值,可以理解成一个缓慢下降的大斜坡叠加上微小的脉搏波动。斜坡的幅值占 80% 以上,脉搏波动可能只有 ADC 满量程的 1% 到 3%。如果直接拿原始数据做峰值检测,会因为斜坡主导而找不到真正的峰值。所以第一步是去掉直流分量,常见做法是一阶高通滤波器:
#define HP_ALPHA 0.97f static int32_t dc_estimate = 0; int32_t highpass_pulse_wave(int32_t raw_adc) { dc_estimate = (int32_t)(HP_ALPHA * dc_estimate + (1.0f - HP_ALPHA) * raw_adc); return raw_adc - dc_estimate; }参数说明:HP_ALPHA越大,截止频率越低,但响应越慢。200Hz 采样率下取 0.97 时,截止频率约为 1Hz,能滤掉放气过程造成的直流漂移,同时保留 1Hz 以上的脉搏能量。如果觉得脉搏波幅度偏小,可以尝试 0.95;如果发现基线还在缓慢漂移,则调到 0.98 后重新观察波形。
4.2 峰值检测:记录每个心跳对应的袖带压和搏动幅度
高通后的信号是一串正负交替的搏动波形,每个心动周期会有一个明显正峰。峰值检测的规则要包含三个条件:当前点比前后各 8 个点都大,相邻两个峰的间隔不小于 400ms,峰幅值大于噪声底限的 3 倍。满足这三个条件后,把这一拍对应的袖带压和峰值幅度存入包络数组:
typedef struct { uint16_t cuff_pressure; /* 袖带压,单位 0.1 mmHg */ uint16_t pulse_amp; /* 脉搏波幅值,单位 LSB */ } envelope_point_t; #define MAX_ENVELOPE_POINTS 200 static envelope_point_t envelope[MAX_ENVELOPE_POINTS]; static uint16_t envelope_count = 0; void detect_peak(int32_t current, int32_t prev_value, uint32_t time_ms) { static uint32_t last_peak_time = 0; static uint32_t peak_index = 0; if (current > prev_value) { peak_index = current; return; } if (time_ms - last_peak_time > 400 && peak_index > noise_floor * 3) { envelope[envelope_count].pulse_amp = peak_index; envelope[envelope_count].cuff_pressure = current_cuff_pressure; envelope_count++; last_peak_time = time_ms; } }这段代码的逻辑是:先等待信号上升到局部最高点,在信号开始回落的那个采样点判定为一个峰值。400ms的最小间隔把心率上限限制在 150 次/分,避免把放气过程中的机械振动当作脉搏。若受测者心率偏快,可以把间隔缩短到 330ms,换算下来是 180 次/分的上限。
4.3 比值法和线性插值:计算 SYS、MAP、DIA
包络数组建立后,血压计算就变成在一维曲线上找三个特殊位置。先找幅值最大点,该点压力就是平均压 MAP。然后从最大值向左找第一个小于等于 0.5 倍峰值的点,右侧找第一个小于等于 0.8 倍峰值的点。由于采样点之间的压力并不是连续变化的,通常还要在两个相邻点之间做一次线性插值,得到更精确的收缩压和舒张压。
把采集到的一组(pressure, amp)数据存成 CSV 后在 PC 端验证算法,是调参最高效的方式。下面这个 Python 脚本可以直接离线运行,用来验证比值法逻辑是否有误:
def calc_blood_pressure(pts, k_sys=0.5, k_dia=0.8): pressures = [p[0] for p in pts] amps = [p[1] for p in pts] imax = max(range(len(amps)), key=lambda i: amps[i]) amax = amps[imax] # 从峰值向左找收缩压阈值穿越点 for i in range(imax, 0, -1): if amps[i] <= k_sys * amax: p0, a0 = pressures[i - 1], amps[i - 1] p1, a1 = pressures[i], amps[i] sys = p0 + (p1 - p0) * (a0 - k_sys * amax) / (a0 - a1) break # 从峰值向右找舒张压阈值穿越点 for i in range(imax, len(amps) - 1): if amps[i + 1] <= k_dia * amax: p0, a0 = pressures[i], amps[i] p1, a1 = pressures[i + 1], amps[i + 1] dia = p0 + (p1 - p0) * (a0 - k_dia * amax) / (a0 - a1) break return sys, pressures[imax], dia说明:循环中pressures使用 0.1mmHg 为单位,这样整个计算过程只用整数和浮点结合,不会出现单位混淆。实际移植到 C 代码时,要注意a0和a1都不为 0,避免浮点除零。
4.4 包络不平滑时的调试方向
如果包络曲线上出现多个峰值,或者阈值穿越点不止一个,首先要检查放气速度是否稳定。放气速度波动大,会导致同一个心率周期在包络上形成不同的展宽,这时候的解决办法是先把电磁阀 PWM 稳速做好,再回头调标定系数。如果包络的上升侧比下降侧陡很多,可能是袖带绑得太紧或太松,标准做法是袖带下缘距肘窝 2~3cm,松紧度以能伸进一根手指为宜。
5. 测量状态机与放气速度闭环:STM32 控制代码实例
5.1 测量流程的状态划分
电子血压计的控制逻辑不能简单写成“按键后充气,然后一直放气”。整个测量过程至少要划分为 5 个状态,每个状态有明确的进入条件和退出条件。
| 状态 | 进入条件 | 状态内动作 | 退出条件 |
|---|---|---|---|
| IDLE | 系统上电或测量完成 | 关闭气泵,关闭电磁阀,等待按键 | 按键按下 |
| INFLATE | 按键触发 | 打开气泵,采集压力 | 袖带压 >= 180mmHg |
| DEFLATE_MEASURE | 充气到位 | PWM 控制电磁阀放气,采样脉搏波 | 袖带压 < 40mmHg |
| RESULT | 放气结束 | 计算并显示血压值,蜂鸣器提示 | 按键确认或超时 |
| EXHALE | RESULT 超时或手动触发 | 电磁阀全开,快速泄压 | 袖带压 <= 2mmHg |
充气目标值设为 180mmHg 是一个通用值,适合成年人。如果要做成支持儿童测量的版本,这个目标值可以在设置菜单里调整。测量中若发现袖带压超过 200mmHg 还在继续上升,必须立刻判断为气路故障,停止充气并快速泄压,这是一个安全保护点。
5.2 放气速度闭环:用 PWM 控制电磁阀开度
放气速度直接决定包络曲线的质量。放气太慢会延长测量时间,用户手臂被压迫时间过长;放气太快又会让每个心跳内压力变化过大,包络幅值测量不准。常见的目标放气速度是 3mmHg/s,允许范围 2~4mmHg/s。电磁阀是比例特性器件,但 PWM 控制的线性区比较窄,所以要用一个简单的闭环来微调占空比:
#define TARGET_SPEED 3.0f /* 目标放气速度,mmHg/s */ #define CONTROL_PERIOD 100 /* 控制周期 100ms */ static uint32_t duty = 40; /* 初始占空比 40% */ static float pressure_prev = 0.0f; void valve_speed_control(float pressure_now) { float speed = (pressure_now - pressure_prev) / 0.1f; /* mmHg/s */ float error = speed + TARGET_SPEED; /* speed 是负值 */ if (error > 0.3f) { duty -= 6; /* 放气慢了,开大电磁阀 */ } else if (error < -0.3f) { duty += 6; /* 放气快了,关小电磁阀 */ } if (duty < 20) duty = 20; /* 低于 20% 电磁阀可能打不开 */ if (duty > 80) duty = 80; /* 高于 80% 放气过快,限制上限 */ __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty); pressure_prev = pressure_now; }逻辑说明:pressure_now从 ADC 原始值转换为 mmHg 后传入,speed是相邻 100ms 两次采样的差分,正常情况下是负值。error > 0.3表示实际放气速度比目标慢,需要增大占空比;error < -0.3则相反。控制滞环 0.3 用来避免 PWM 在小范围内抖动。
5.3 控制与采样的时序关系
放气控制每 100ms 执行一次,而 ADC 采样是每 5ms 一个点。控制代码必须放在定时器回调或主循环的周期任务中,不能用HAL_Delay(100)阻塞在中断里。采样回调中只做数据积累,主循环每次检测到 100ms 时钟标志后,从最近采集的 ADC 数据中取平均作为当前袖带压,再做速度闭环计算。这样两个循环互不干扰,采样率不会被控制逻辑拉偏。
5.4 超时保护:防止测量过程卡死
任何一步卡住都可能导致气泵一直工作或电磁阀一直放气。状态机里要加一个全局超时检查:
if ((HAL_GetTick() - state_entry_tick) > 30000UL) { pump_off(); valve_open_full(); state = IDLE; }30000ms 超时对一次完整测量足够,如果用户没有绑好袖带导致压力一直达不到目标,系统也能自动泄压回到待机状态。这个保护逻辑应该放在主循环的顶部,每个循环周期都检查一次,优先级高于按键响应。
6. 用串口把中间量画出来:验证与标定的实操技巧
6.1 让 STM32 通过串口输出波形数据
算法调通后要做的第一件事,不是拿去和标准血压计对比,而是把原始信号、高通后的脉搏波、包络幅值这三路数据用串口发到电脑上看波形。写一个轻量级的printf重定向,然后在采样回调中按固定节奏输出:
printf("%u,%d,%u,%u\r\n", (unsigned int)raw_adc, (int)highpass_output, (unsigned int)cuff_pressure_mmhg_x10, (unsigned int)current_amp);参数说明:四路数据用逗号分隔,cuff_pressure_mmhg_x10表示袖带压乘 10 后的整数,避免浮点格式化占用太多 CPU。PC 端用 Vofa+ 或串口助手打开,选择帧格式为 CSV,就能实时看到四条曲线。正常波形应当呈现:第一条是缓慢下滑的直线,第二条是等间隔的搏动波,第三条(包络幅值)呈倒钟形,峰值出现在平均压附近。如果第三条曲线没有明显拱形,说明峰值检测参数或滤波系数需要回调。
6.2 标定系数的手动修正方法
和汞柱血压计做对比时,记录三组数据:低压段、中压段、高压段。每组测三次取平均,计算算法值和参考值的偏差。收缩压偏差如果在正负 5mmHg 以内,可以不做修正;超过 5mmHg,直接把差值作为补偿量写入内部 Flash 或外部 EEPROM。STM32 内部 Flash 的最后一个扇区通常用来存这类标定数据,启动时读取并作为全局偏移量参与最终显示。
6.3 阈值系数 K 的离线扫描
如果完成放气闭环后仍然有固定偏差,可以考虑调整阈值系数。保存一组(Pk, Ak)数据后,在电脑上用脚本按 0.45~0.55、0.70~0.85 步进扫描,找出一组误差最小的系数。这个方法比在单片机上反复编译烧录快得多,也是把毕设项目从“能出数”提升到“测的准”的一个高性价比路径。把扫描用到的脚本文件放在工程目录下,方便以后重新标定时直接调用。
本文还有配套的精品资源,点击获取