ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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 的触发信号给 ADCADC 转换完成后由 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 里典型的 DMAADC 双缓冲用法把它跑稳定后面加滤波算法、包络检测时不用再回头改采集层。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_sys0.5, k_dia0.8): pressures [p[0] for p in pts] amps [p[1] for p in pts] imax max(range(len(amps)), keylambda 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按键触发打开气泵采集压力袖带压 180mmHgDEFLATE_MEASURE充气到位PWM 控制电磁阀放气采样脉搏波袖带压 40mmHgRESULT放气结束计算并显示血压值蜂鸣器提示按键确认或超时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 步进扫描找出一组误差最小的系数。这个方法比在单片机上反复编译烧录快得多也是把毕设项目从“能出数”提升到“测的准”的一个高性价比路径。把扫描用到的脚本文件放在工程目录下方便以后重新标定时直接调用。本文还有配套的精品资源点击获取
返回列表