news 2026/9/16 14:41:00

STM32电子血压计实现:示波法、脉搏波提取与ADC采样全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32电子血压计实现:示波法、脉搏波提取与ADC采样全解析

简介:基于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 布板和调试都友好。

型号量程输出方式供电电压说明
MPX5050GP0~50kPa模拟电压,约 0.2V~4.7V5V片内放大,带温度补偿,使用最普遍
XGZP6847可按需选择量程I2C 或模拟输出3.3V / 5V国产传感器,采购渠道多,模拟版本更贴合本项目
MS5525DSO绝压/差压I2C3.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 引脚示例驱动方式备注
气泵PA1NPN/NMOS 低端驱动,PWM 或开关控制充气阶段直接开启,到位后关闭
电磁阀PA2NPN/NMOS,续流二极管必需放气测量阶段用 PWM 控制开度
蜂鸣器PA3NPN 驱动测量完成提示音
按键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 代码时,要注意a0a1都不为 0,避免浮点除零。

4.4 包络不平滑时的调试方向

如果包络曲线上出现多个峰值,或者阈值穿越点不止一个,首先要检查放气速度是否稳定。放气速度波动大,会导致同一个心率周期在包络上形成不同的展宽,这时候的解决办法是先把电磁阀 PWM 稳速做好,再回头调标定系数。如果包络的上升侧比下降侧陡很多,可能是袖带绑得太紧或太松,标准做法是袖带下缘距肘窝 2~3cm,松紧度以能伸进一根手指为宜。

5. 测量状态机与放气速度闭环:STM32 控制代码实例

5.1 测量流程的状态划分

电子血压计的控制逻辑不能简单写成“按键后充气,然后一直放气”。整个测量过程至少要划分为 5 个状态,每个状态有明确的进入条件和退出条件。

状态进入条件状态内动作退出条件
IDLE系统上电或测量完成关闭气泵,关闭电磁阀,等待按键按键按下
INFLATE按键触发打开气泵,采集压力袖带压 >= 180mmHg
DEFLATE_MEASURE充气到位PWM 控制电磁阀放气,采样脉搏波袖带压 < 40mmHg
RESULT放气结束计算并显示血压值,蜂鸣器提示按键确认或超时
EXHALERESULT 超时或手动触发电磁阀全开,快速泄压袖带压 <= 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 步进扫描,找出一组误差最小的系数。这个方法比在单片机上反复编译烧录快得多,也是把毕设项目从“能出数”提升到“测的准”的一个高性价比路径。把扫描用到的脚本文件放在工程目录下,方便以后重新标定时直接调用。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 14:39:58

SpringBoot+MyBatis打造新生报到系统:从数据库设计到并发兜底

简介&#xff1a;SpringBoot大学新生报到系统是一份面向计算机专业毕业设计及Spring Boot实战学习的完整工程源码包&#xff0c;覆盖学生信息管理、报到流程管理和宿舍分配等核心模块。压缩包共561个文件&#xff0c;约16.02MB&#xff0c;其中包含123个Java源码、93个Vue页面、…

作者头像 李华
网站建设 2026/9/16 14:38:57

多源医学数据采集实战:统一JSON结构设计与Python爬虫工程化

简介&#xff1a;一套使用Python实现的医学知识采集与爬虫源码&#xff0c;整合知网、掌上高考、上海体检套餐、39net检查、薄荷网等数据源&#xff0c;将爬取的疾病、症状、医生、机构、处方等多类信息统一保存为JSON文件&#xff0c;适合研究多源爬虫与医学数据处理的Python开…

作者头像 李华
网站建设 2026/9/16 14:38:00

Android财务系统毕业设计:登录、账单与统计全模块解析

简介&#xff1a;面向毕业设计场景的Android财务系统完整项目资源包&#xff0c;适合计算机相关专业学生与需要快速搭建财务类App的开发者。资源共1226个文件&#xff0c;压缩包仅10.86MB&#xff0c;以Java源码、XML布局、PNG图片、JAR依赖库等类型为主&#xff0c;包含完整的…

作者头像 李华
网站建设 2026/9/16 14:34:56

SpringBoot智慧校园系统源码跑通实战指南

简介&#xff1a;本资源是一套基于SpringBoot开发的智慧校园管理系统完整源码&#xff0c;面向Java全栈初学者与高校课程设计者&#xff0c;提供可运行、可二次开发的B/S架构校园信息化解决方案。系统涵盖用户管理、图片与视频素材库等核心模块&#xff0c;技术栈包括Java 1.8、…

作者头像 李华