基于STM32的电子血压计设计:示波法原理与完整实现 简介这是一份基于STM32微控制器的电子血压计毕业设计与课程作业完整项目适合嵌入式方向学生、毕设开发者和STM32入门进阶者参考。压缩包共161个文件、2.21MB包含35个C源码、34个头文件及Keil工程文件uvproj、hex烧录文件、编译链接文件等涵盖驱动、应用与构建配置可在此基础上直接编译学习或二次开发。项目覆盖STM32 GPIO、ADC模拟量采集、定时器、串口与FSMC等外设应用同时涉及压力传感器信号处理、血压算法及LCD显示等关键环节能够帮助理解从硬件电路到软件实现的完整流程。目前已有243人浏览学习资料内附系统源码与工程结构对想动手完成类似嵌入式医疗设备设计的同学具有较高参考价值。1. 电子血压计设计从示波法到STM32落地的完整链路做嵌入式毕设的同学第一次拿到基于STM32的电子血压计这个题目时多半以为难点在压力传感器和ADC采样上真正动手后才发现让气泵平稳充气、让放气速度可控、从叠加了斜坡压力的微弱脉搏波里把振幅特征抠出来每一步都比想象中更能消磨耐心。血压计不是测个压力然后显示数字那么简单它要的是在充放气过程中同步捕捉袖带静压和脉动振荡波再通过示波法拟合出收缩压、舒张压和平均压。这个题目恰好覆盖了STM32开发中最常用的几块技能定时器触发ADC采样、DMA搬运数据、PWM控制电机、状态机组织业务流、以及串口调试。无论你是为课程设计交差还是想把它扩展成智能健康终端这套从传感器选型到算法标定的链路都值得完整走一遍。2. 硬件方案怎么选STM32型号、压力传感器与气路部件2.1 示波法测量原理为什么不用听诊法先立住理论基础。听诊法需要人用听诊器听柯氏音无法自动化示波法的核心思想是袖带压力逐渐放气时血管壁振动会在袖带气压上叠加一个微弱的振荡波这个振荡波的幅度随袖带压力变化呈现小→大→小的包络形态。包络峰值对应的袖带压力就是平均压MAP峰值两侧某两个比例位置分别对应收缩压SBP和舒张压DBP。这个方案不需要麦克风只需要一个压力传感器同时感知两路信息一路是缓慢下降的袖带静压另一路是叠加在静压上的脉搏振荡波。软件上通过高通或带通滤波把振荡波从静压信号里分离出来再做包络识别。理解了这个原理硬件选型和软件框架就都有了方向。2.2 传感器选型与STM32内部资源分配压力传感器常见选型有两种一种是MPX5050这类带放大输出的模拟传感器满量程50kPa约375mmHg输出电压与压力成正比另一种是XGZP6847这类小封装表压传感器需要自己加运放放大。课程设计优先选模拟电压输出的型号省去仪表放大器的设计调试成本。STM32型号选择上做电子血压计不需要上F4系列STM32F103C8T6完全够用。它提供2个12位ADC、4个16位定时器、多个UART主频72MHz跑滤波算法绰绰有余。如果你是照着江科大教程入门对标准库或HAL库的工程结构已经熟悉直接复用模板工程会省去新建工程时芯片包配置、启动文件选择的麻烦。下面是这类项目最常用的内部资源分配方式片上资源功能分配说明ADC1 通道0/1压力传感器信号 参考电压校准定时器触发采样DMA搬运TIM2PWM输出驱动气泵电机输出比较通道CH1频率20kHzTIM3ADC触发源更新事件触发ADC转换控制采样率USART1串口调试输出输出实时压力和波形数据I2C1 或 SPI1接OLED显示屏显示测量数值与血压分级GPIO控制电磁阀开闭放气速度控制和急停泄压传感器输出0.5V到4.5V对应0mmHg到300mmHg但STM32的ADC参考电压是3.3V所以不能用传感器输出直连引脚。常见做法是串联电阻分压比如用10k和6.8k电阻把4.5V分压到2.7V左右保证满量程不超出ADC上限。后续软件里通过标定修正分压比带来的偏差比加运放电路简单得多。2.3 气泵、电磁阀与安全保护电路气路系统由气泵、电磁阀、泄气阀、袖带和压力传感器组成。气泵负责快速充气到目标压力测量结束后电磁阀打开快速放气有些设计还会加一个微流量泄气阀实现缓慢匀速放气。气泵驱动用低端MOS管开关N沟道AO3400即可STM32的PWM信号经10Ω电阻接到栅极PWM频率20kHz可以避免可闻噪声。注意气泵是感性负载电机两端要并一个续流二极管SS34防反冲电压烧MOS管。电磁阀驱动用ULN2003集成达林顿管或者直接用三极管继电器。安全保护必须做两层硬件上在气路上加一个机械泄压阀超过280mmHg自动泄气软件上持续监测压力值超过预设上限立即打开电磁阀并关闭气泵。这部分不能省毕竟这是直接接触人体的设备实验时也可能会把袖带扎得过紧。3. 脉搏波信号采集定时器触发ADC与数字滤波3.1 用定时器触发ADCDMA采样双通道信号脉搏波的频率成分集中在0.5Hz到30Hz理论上采样率100Hz就足够但为了波形形态还原和后续包络拟合的平滑性实际取500Hz到1kHz更好。采样率过高CPU忙于中断过低包络峰值容易丢失500Hz是一个均衡点。工程实现上推荐用定时器触发ADC再配合DMA循环搬数据全程不需要CPU干预。以STM32的HAL库为例关键初始化代码片段如下// 定时器3配置为500Hz更新事件 htim3.Init.Period 1440 - 1; // 72MHz / (1440) 50kHz htim3.Init.Prescaler 99; // 实际触发频率 72MHz / 100 / 1440 500Hz HAL_TIM_Base_Start(htim3); // ADC1 配置为定时器触发模式DMA循环模式 hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T3_TRGO; HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 2); // DMA半传输完成中断时处理前一组数据 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { // 处理adc_buf[0]之前的数据 process_samples(adc_buf[0]); } }这段代码两个关键点一是ADC的触发源选择ADC_EXTERNALTRIGCONV_T3_TRGO把ADC采样时刻和定时器更新事件对齐采样间隔精确可控二是DMA设置为循环模式每次采样的2个通道数据按序落在缓冲区中半传输中断里处理前一半数据不影响后一半数据继续填充。压力换算不是线性的12位ADC值先转成电压再根据传感器灵敏度转成mmHgfloat voltage_to_pressure(uint16_t adc_val) { float voltage (float)adc_val * 3.3f / 4095.0f; // 分压比约0.6传感器0.5V对应0mmHg4.5V对应300mmHg float raw_press (voltage / 0.6f - 0.5f) * 300.0f / 4.0f; return raw_press; }注意上面代码里的0.6和0.5都是理想值真实传感器存在零点偏置和灵敏度离散正式标定时要用两个已知压力点拟合出斜率fix_slope和截距fix_offset替换掉这里的固定常数。3.2 数字滤波器设计与截止频率选择传感器输出里包含袖带静压0到1Hz的缓慢斜坡和脉搏振荡波0.5Hz到30Hz还有气泵振动和工频干扰。血压算法关心的是振荡波的幅度所以需要一个带通滤波器低端截止频率要压低到0.5Hz以下以保留完整的脉搏波形态高端截止30Hz即可滤掉高频噪声。我一般直接设计二阶巴特沃斯带通滤波器用matlab或Python的scipy.signal算出系数后再用C语言实现。以500Hz采样率、带通0.5Hz到30Hz为例滤波器的差分方程系数是一组固定小数可以直接实现为浮点运算// 直接I型二阶IIR带通滤波器 // y[n] b0*x[n] b1*x[n-1] b2*x[n-2] - a1*y[n-1] - a2*y[n-2] float filter_bpf(float input, float *state) { float output b0 * input b1 * state[0] b2 * state[1] - a1 * state[2] - a2 * state[3]; state[1] state[0]; state[0] input; state[3] state[2]; state[2] output; return output; }采样率、放气速率和滤波器截止频率三者需要配合。放气速度如果超过5mmHg/s每个mmHg压力区间内的脉搏波采样点过少包络拟合会漏掉峰值放气太慢又会导致测量时间过长用户手臂被袖带勒得难受。建议放气速率控制在2到4mmHg/s配合500Hz采样率每个mmHg压力区间内约有60到120个振荡波样本足以还原波形形态。3.3 去除基线漂移与脉搏波幅度提取带通滤波后的信号已经去掉了静压斜坡得到的是近似零基线的振荡波。接下来要做的是逐拍检测脉搏波的峰值幅度。血压计算并不关心每一个波的精确形态关心的是每个心跳周期内振荡波的最大振幅。常见做法是滑窗峰值检测维护一个30ms到60ms的滑窗窗内找最大值记录这个最大值作为当前拍的脉搏波幅度同时用阈值判断当前窗口内是否有有效脉搏波阈值取最近5个峰值的均值的30%。这样可以避免把小噪声当成有效脉搏波#define WINDOW_SIZE 25 // 500Hz采样率下50ms窗口 #define PEAK_THRESHOLD 0.3f float detect_pulse_amplitude(float *samples, uint16_t len) { float max_val 0; for (uint16_t i 0; i len; i) { if (samples[i] max_val) max_val samples[i]; } // 如果峰值明显低于平均峰值视为噪声 if (max_val average_peak * PEAK_THRESHOLD) return 0; return max_val; }脉搏波幅度提取的准确性直接决定后续血压计算的精度。这里有一个常见的坑不要对原始带通滤波信号直接做峰值检测因为滤波器的瞬态响应在起始阶段会产生大幅波动导致前几个误判。我通常会让程序先运行一组假数据初始化滤波器状态或者直接忽略气泵停止后前2秒内的采样数据。4. 示波法血压计算包络提取、MAP判定与SBP/DBP推算4.1 从脉搏波幅度序列构建包络线随着袖带压力从充气目标值缓慢下降每检测到一个有效脉搏波就把它的幅度和当时的袖带静压记录为一组数据点。全部测量结束后这些数据点构成一个类似开口向下的钟形散点序列横轴是袖带压力纵轴是脉搏波幅度。直接使用散点找峰值容易受个别异常点干扰建议用三次样条插值拟合包络曲线然后用DSP库或自己实现的一维极值搜索在拟合后的曲线找最大值点。对课程设计来说用简单的滑动平均平滑散点序列就足够typedef struct { float pressure; float amplitude; } env_point_t; env_point_t envelope[64]; uint8_t env_len 0; // 对包络做3点滑动平均减小随机波动影响 void smooth_envelope(void) { float temp[64]; for (uint8_t i 1; i env_len - 1; i) { temp[i] (envelope[i-1].amplitude envelope[i].amplitude envelope[i1].amplitude) / 3.0f; } for (uint8_t i 1; i env_len - 1; i) { envelope[i].amplitude temp[i]; } }注意滑动平均会让原始峰值位置偏移最多一个数据点在放气速率3mmHg/s的情况下对应约3mmHg的误差。更精确的做法是保留原始数据用于峰值定位只把平滑后的数据用于确定峰值范围。4.2 平均压MAP的三种判定方法平均压判定有三种常用方法按实现从简单到复杂排列第一是包络峰值法直接取包络最大值对应的袖带压力作为MAP这是最直观的做法误差在正负5mmHg以内。第二是幅度比法利用收缩压和舒张压处脉搏波幅度与最大幅度的比值关系反推MAP。第三是积分法在包络曲线最大幅度前后各取一定比例区域计算加权中心。课程设计用第一种工程上追求精度用第三种。包络峰值查找要注意血压计的袖带压范围。如果受测者血压偏高包络峰值可能在充气未完成时就已经出现这种情况下峰值会被截断。所以充气目标压力不能固定常见做法是先快速充气到160mmHg放气过程中实时检测到包络上升趋势后再继续加压到预估收缩压上方30mmHg。4.3 SBP与DBP的比例系数标定与可调参数确定MAP位置后收缩压SBP定义为包络上升沿上幅度等于某个比例k_sys处的袖带压力舒张压DBP定义为包络下降沿上幅度等于某个比例k_dia处的袖带压力。这个比例系数没有普适标准不同品牌设备差异很大。以最大脉搏波幅度amp_max为基准一般教学代码里会直接用0.7和0.5实际效果往往欠佳。我在调试过的方案中合理的系数范围如下系数常见范围初始建议值调整方向k_sys0.45 ~ 0.750.55计算结果偏高时增大该值k_dia0.35 ~ 0.700.60计算结果偏低时减小该值MAP偏移-5 ~ 5 mmHg0与听诊法对比后修正这两个系数不是对称的。收缩压判定受气泵停止后瞬间压力波动影响大放气初期包络上升沿较陡峭比例系数微调0.05就会带来5到8mmHg的偏差舒张压判定在包络下降沿放气后期脉搏波本身变弱噪声影响更明显。建议调试时用同一组数据反复回放先固定舒张压系数调收缩压再反向调不要两个系数同时改。下面是一个根据包络数组计算三组血压值的核心实现typedef struct { float sbp; float dbp; float map; } bp_result_t; bp_result_t calc_bp_from_envelope(env_point_t *env, uint8_t len) { bp_result_t bp {0, 0, 0}; uint8_t max_idx 0; float max_amp 0; // 找包络峰值位置 for (uint8_t i 0; i len; i) { if (env[i].amplitude max_amp) { max_amp env[i].amplitude; max_idx i; } } bp.map env[max_idx].pressure; // 在峰值左侧找收缩压对应的压力值 for (uint8_t i max_idx; i 0; i--) { if (env[i-1].amplitude max_amp * K_SYS env[i].amplitude max_amp * K_SYS) { bp.sbp env[i].pressure; break; } } // 在峰值右侧找舒张压对应的压力值 for (uint8_t i max_idx; i len - 1; i) { if (env[i1].amplitude max_amp * K_DIA env[i].amplitude max_amp * K_DIA) { bp.dbp env[i].pressure; break; } } return bp; }代码里用了线性插值会更精确直接用离散点的压力值会有最大一个采样间隔的量化误差。如果放气速率为3mmHg/s两次有效记录间隔约为1.5mmHg这个精度对教学演示足够。4.4 心率计算的附带收益测量过程中已经提取了每个脉搏波的峰值位置心率的计算不需要额外传感器。记录相邻两次脉搏波峰之间的时间间隔T心率就是60/T。在放气阶段测量时间约为25到40秒足够计算出稳定的平均心率。把心率计算放在同一组数据处理里显示时能多一个参数毕设答辩时也是一个加分项。5. 整机调试与标定示波法参数的最终微调5.1 用状态机组织测量流程更不容易出bug电子血压计的业务流程天然适合状态机充气、保持、放气、测量、快速泄压。每个状态有明确的进入条件和超时退出条件比用标志位和if嵌套实现要清晰得多。typedef enum { IDLE, INFLATING, HOLDING, MEASURING, DEFLATING, FINISHED, FAULT } sys_state_t; sys_state_t state IDLE; void state_machine_tick(void) { switch (state) { case INFLATING: start_pump(); if (pressure target_pressure) { stop_pump(); state MEASURING; } if (pressure 280) state FAULT; // 超压保护 break; case MEASURING: record_envelope_point(); if (pressure 40) state DEFLATING; // 放气到40mmHg以下结束 break; case DEFLATING: open_valve(); if (pressure 5) state FINISHED; break; // 其他状态处理省略 } }状态机的时间基准建议放在一个1ms的定时器中断中每个状态内的延时都用计数器累加避免使用阻塞式HAL_Delay。阻塞延时在测量过程中会直接导致采样中断丢失数据这是初学者最常见的隐性bug来源。5.2 串口输出关键量用数据调算法系数开发调试阶段要能看到实时数据才能判断算法问题在哪。串口输出的关键数据是当前袖带压力、滤波后的脉搏波幅度、包络数组点以及最终计算出的三组血压值。用带波形显示的串口工具或其他可以绘制串口数据的软件把包络曲线画出来调试效率会高很多。调试时建议准备一个水银血压计或电子血压计作为参照。同一个受测者交替测量数次记录设备读数和参照设备读数。差值如果呈现统一的偏高或偏低趋势说明是比例系数问题如果差值分散、时高时低问题大概率出在气路或滤波参数上。5.3 压力标定的两段式修正很多同学只做一次静态标定就完事忽略了压力传感器的非线性。传感器在0到300mmHg的满量程范围内低端和高端灵敏度并不完全一致。做两点线性校正后中间段的残余误差通常仍在正负3mmHg以内。提高精度的方法很简单做三段式校正。在0、100、300mmHg三点记录ADC原始值分别计算低段和中高段的斜率与截距代码里根据当前压力值所在的区间切换不同的标定系数。这样传感器非线性误差可以压到正负1.5mmHg以内对于课程设计来说已经属于精度很完整的方案。标定完成后最后一步是拟合系数统一管理。把K_SYS、K_DIA、压力标定系数集中放一个头文件中宏定义或结构体常量避免散落在代码各处。整理系数表也会让毕设报告的数据分析章节更好写。本文还有配套的精品资源点击获取