十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于STM32与MAX30102的心率脉搏测量仪设计与实现

基于STM32与MAX30102的心率脉搏测量仪设计与实现 简介本资源是一套面向高校课程设计与毕业设计的STM32嵌入式系统实战项目——基于STM32F103的心率脉搏测量仪完整开发资料适用于单片机初学者进阶实践及医疗电子类课题开发。项目涵盖从光电传感信号采集、多级模拟调理含1200倍放大、四阶低通滤波、迟滞比较与2.5V偏置、到STM32数字处理ADC采样60点队列缓存时间间隔平均算法及多终端交互OLED实时显示、串口MATLAB波形绘制、蓝牙手机传输的全链路实现支持自动启停与心率异常告警50–150bpm。压缩包共1018个文件含564个C源码、249个头文件、51个汇编文件及Altium Designer原理图.schdoc与PCB.pcbdoc工程另有IAR/Keil工程配置、数学库.a文件及调试用.hex/.axf文件总大小24.97MB。目前已有93人学习下载提供可直接编译运行的完整软硬件方案、信号处理关键算法注释、低功耗状态管理逻辑及实测误差6%的验证数据是深入理解生物电信号采集与嵌入式实时处理的优质参考范例。 做嵌入式这些年我陆陆续续整理过不少开发资料其中“基于STM32的心率脉搏测量仪”这套编号ID9算是我经常翻出来给新人当入门范例的一套资料。它并不只是把一个传感器往开发板上一插、读几个数据而已而是完整地走了一遍“需求分析 → 方案选型 → 原理图设计 → 代码编写 → 联调优化 → 问题排查”的全流程。如果你正在做课程设计、毕业设计或者只是想把单片机、传感器、显示模块串起来练手这套项目能让你少走很多弯路。今天我就把其中的核心设计思路、具体电路、调试心得都拆开来讲不光是给你看结论更想让你明白每一步“为什么”。市面上心率测量模块很多但真正做出来的东西是否稳定、读数是否可信往往取决于你对信号链路的理解深度。所以这篇内容会更偏实战我会把我在调这套系统时踩过的坑、验证过的参数、以及随手能用的代码逻辑都写出来。只要你手里有一块STM32F103C8T6、一个MAX30102模块和一个OLED屏基本就能复现整个项目。1. 方案选型为什么用STM32做脉搏测量仪1.1 从需求倒推硬件平台做一个心率脉搏测量仪首先要明确几个核心需求实时采集脉搏信号、计算出每分钟心跳次数BPM、显示波形和数值最好还能串口输出数据方便调试。这些需求看似简单但落到硬件选型上需要主控具备足够的GPIO、I2C/UART外设、定时器资源以及足够的处理能力来处理滤波和峰值检测算法。我选择STM32F103C8T6作为主控核心原因有三个。第一性价比高一片几块钱但内置64KB Flash和20KB RAM跑一个心率算法绰绰有余。第二外设接口丰富硬件I2C、SPI、USART、定时器全都有可以同时接传感器、OLED屏和调试串口。第三生态成熟HAL库、标准库、寄存器版资料到处都是遇到问题能搜到的解决方案非常多。哪怕你用的是STM32G0或者F401思路也完全通用。有人可能会说用Arduino或者ESP32不是更简单Arduino确实写起来快但库封装得太厉害很难让你理解I2C时序和信号处理的细节ESP32性能强但功耗高、价格也高而且用在这一场景属于杀鸡用牛刀。STM32恰好卡在“有点难度但又不至于劝退”的位置做完这个项目你顺带就把定时器、中断、I2C、ADC、状态机这些基础知识都练了一遍。1.2 传感器方案对比光电式、压电式与电容式所谓“心率脉搏测量仪”核心是把心脏搏动引起的血管容积变化转换成电信号。市面主流的方案有三种光电容积波PPG、压电式脉搏波、心电信号ECG。它们各有适用场景不能只看价格。光电式是目前消费级产品最常用的方案典型的模块有MAX30100、MAX30102、MAX30105。它们内部集成了LED和光电二极管通过测量血液对光的吸收变化来感知脉搏。优势是体积小、接口简单I2C采样率可以很高适合做手指夹式或手环式设备。劣势是对运动干扰敏感手指稍微一动信号就会乱。压电式传感器比如常用在中医脉诊仪上的HKG-07B输出的是一个模拟电压脉冲需要自己搭放大和滤波电路能更直观地体现脉搏搏动波形但对电路设计要求高而且传感器本身需要贴合手腕的桡动脉位置对佩戴要求苛刻。心电方案如AD8232测量的是心脏电信号严格来说不是“脉搏”但也能算出心率。它的优势是抗运动干扰能力强缺点是方案复杂需要电极片而且信号调理电路更繁琐。我在这套资料里选的是MAX30102因为它在集成度和可玩性之间平衡最好。这颗芯片内置红光LED、红外LED、光电检测器、ADC和数字滤波外部电路只需要一点点滤波电容。它既能测心率也能测血氧后续想扩展功能也方便。另一个重要原因是它的FIFO缓存能存32组数据主控不用频繁读取适合做低功耗设计。1.3 显示与交互模块的选择心率测量仪的显示部分我推荐使用0.96英寸OLEDSSD1306驱动128×64而不是LCD1602或者TFT彩屏。OLED优势在自发光、对比度高、功耗低而且I2C接口只占两根线能和MAX30102挂在同一条总线上减少GPIO占用。对于教学和实验来说OLED能同时显示波形和数字视觉反馈比串口明显太多。交互方面至少要有一个按键用来切换显示模式比如实时波形模式、数值模式、历史记录模式或者用来消除报警。我通常还会加一个蜂鸣器或LED作为心率指示每检测到一次心跳就同步亮一下这样不需要看屏幕也能直观感受仪器的灵敏度。这里有个很容易踩的坑很多人一开始就想着用TFT彩屏画个炫酷的界面结果发现屏幕刷屏占用大量CPU时间导致心率采样周期不稳定读出来的数据反而乱跳。所以我的建议是第一版老老实实用OLED和简单菜单先把信号链路跑通再考虑美化交互。2. 硬件设计思路与电路解析2.1 传感器与STM32的接口电路MAX30102模块一般已经带有稳压和上拉电路引脚定义通常是VIN、GND、SCL、SDA、INT。VIN接3.3V即可千万不要接5V虽然模块上可能有稳压芯片但超过额定电压长期使用会影响传感器稳定性。SCL和SDA接STM32的I2C1引脚PA9和PA10对应串口PB6/PB7是I2C1注意别搞混。如果MAX30102和OLED共用I2C总线两条线都需要接上拉电阻。模块上一般已经有4.7kΩ上拉如果你是自己搭建的电路记得在SCL和SDA上各接一个4.7kΩ电阻到3.3V。上拉电阻太小会增大功耗太大会导致信号边沿变缓影响高速通信4.7kΩ在100kHz标准模式下是比较稳妥的选择。INT中断引脚可以接PA0并配置为下降沿触发。MAX30102在FIFO数据达到阈值后会产生中断这样主控就不用一直轮询读取状态寄存器既省电又方便做实时处理。如果你只是想快速验证也可以不接INT定时去读取状态寄存器也一样能工作。2.2 信号调理滤波与放大MAX30102内部已经完成了很大一部分信号调理包括环境光消除、ADC转换和数字滤波。但这不意味着外部电路可以完全裸奔。经验证明电源纹波和地噪声对PPG信号的影响非常大尤其是当ADC的满量程设置到高增益时几十毫伏的电源波动都会在脉搏波上叠加出明显的低频噪声。正确的做法是在3.3V电源处并联一个10μF钽电容和一个100nF陶瓷电容靠近传感器电源引脚放置。如果条件允许在电源输入端串一个小磁珠可以进一步滤除高频干扰。这部分电路很简单但很多人因为偷懒漏掉了后面费了大量时间去滤波算法上找原因。如果你用的是压电式模拟传感器信号调理电路就要复杂一些。以HKG-07B为例它的输出幅值大约在0.1~1mV到几十mV之间必须先经过仪表放大器放大再用低通滤波器滤除高频噪声。通常一级运放放大倍数设置在100倍左右然后接一个截止频率在5Hz左右的有源二阶低通滤波器。RC参数可以这样估算取R100kΩC0.33μF则截止频率f1/(2πRC)≈4.8Hz符合脉搏波主要能量集中在0.5~4Hz这个特征。2.3 电源与低功耗设计心率测量仪如果只是插着USB线供电电源设计很简单一个AMS1117-3.3就能解决。但如果你想做成纽扣电池或锂电池供电的便携设备就必须考虑低功耗。STM32F103在72MHz全速运行时的电流大约20mA加上传感器、OLED整体功耗可能在40~50mA这不是一个能长久用电池的数值。实际产品中通常会使用低功耗模式比如只在心搏间隔内唤醒采样其他时间让MCU进入Stop模式。在这套系统里我给出的方案是使用定时器每隔10ms唤醒一次完成一次采样和简单计算然后重新进入低功耗。MAX30102的采样率可以设成100HzLED脉冲宽度为69μs左右平均电流很低。OLED可以在静态显示时关闭需要看波形时再点亮。实测下来用一块400mAh的锂电池如果OLED亮屏时间只占10%系统平均电流能压到15mA以下大约可工作26小时对于实验项目来说已经够用了。3. 固件开发从裸机到实时系统3.1 初始化与I2C通信细节固件我推荐用HAL库配合CubeMX做初始化理由是代码结构清晰、错误处理方便而且网上现成例程多。CubeMX里只需要做几件事配置时钟为72MHz、使能I2C1的标准模式100kHz、配置USART1用于调试、配置一个定时器作为系统时基也可以用SysTick。初始化MAX30102时有几个寄存器是必须配置的。首先是模式配置寄存器地址0x09要设为心率模式0x03而不是血氧模式0x02。然后是SpO2配置寄存器地址0x0A这里可以设置采样率和LED的脉冲宽度。我习惯把采样率设为200HzLED脉宽设为69μs这样可以兼顾信号分辨率和功耗。LED电流通过0x0C和0x0D寄存器分别设置红光和红外LED的电流从低到高有十几个档位。下面的代码展示了读取/写入寄存器的基础函数和初始化序列。注意HAL库的I2C函数在通信出错时会返回超时错误因此每次调用后检查返回值是必须的。uint8_t max30102_write_reg(uint8_t reg, uint8_t val) { return HAL_I2C_Mem_Write(hi2c1, 0xAE, reg, I2C_MEMADD_SIZE_8BIT, val, 1, 100); } uint8_t max30102_read_reg(uint8_t reg, uint8_t *buf, uint8_t len) { return HAL_I2C_Mem_Read(hi2c1, 0xAE, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 100); } void max30102_init(void) { // 软件复位 max30102_write_reg(0x09, 0x40); HAL_Delay(100); // 设置为心率模式 max30102_write_reg(0x09, 0x03); // 采样率200Hz, 脉宽69us, ADC量程4096nA max30102_write_reg(0x0A, 0x44); // 红光LED电流 6.4mA max30102_write_reg(0x0C, 0x24); // 红外LED电流 0mA心模式用不到可以关掉 max30102_write_reg(0x0D, 0x00); // 打开FIFO滑动平均等 }这里特别提醒一下MAX30102的7位I2C地址是0x57左移一位后是0xAE这是HAL库底层要求的8位写地址。我见过太多人直接写0x57导致NACK纠结了半天接线问题其实只是地址没有左移。3.2 心率数据采集与处理流程在心率模式下MAX30102每完成一次采样会把红光的ADC值压入FIFO。程序要周期性地检查FIFO写指针寄存器0x04和读指针寄存器0x05一旦发现FIFO里有新数据就批量读出来。如果配置了INT中断在中断回调里设置一个标志主循环看到标志再读取效果会更好。原始红ADC值是一个有基线漂移的波形。由于手指内的血液会随呼吸轻微变化信号中会叠加一个约0.1~0.5Hz的基线漂移而真正的脉搏波频率在0.8~3Hz之间。为了后续能准确检测峰值需要对信号做预处理。我常用两道处理先去均值再做滑动平均滤波。去均值就是把当前数据减去最近100个采样点的平均值这样可以干掉大部分基线漂移滑动平均滤波用窗口长度为5即可窗口大了虽然波形更平滑但会产生延迟导致BPM更新滞后。如果你觉得滑动平均还不行可以尝试一阶IIR低通滤波公式很简单filtered filtered alpha * (raw - filtered);其中alpha取0.2~0.4可以明显削弱高频毛刺。注意alpha太大会导致滤波效果差太小会让波形响应迟钝。这个参数是我实际测试下来比较折中的选择。3.3 脉率算法从原始波形到BPM脉搏波的峰值检测是最容易出问题的地方也是整个项目的灵魂。我见过不少同学用固定阈值检测比如ADC值超过某个数就算一次心跳结果身体一抖就重复计数或者信号弱的时候漏检。可靠一点的思路是动态阈值加上峰值锁定。具体做法是维护滑动窗口中的最大值max和最小值min计算一个动态阈值 threshold min 0.5 * (max - min) 当信号从低于阈值穿越到高于阈值并且持续上升达到峰值时记录这个峰值点对应的时刻。相邻两个峰值时刻的间隔就是一次心跳周期。假设当前时刻为t2上次峰值为t1那么瞬时心率 bpm 60.0 / (t2 - t1) 注意t1和t2单位是秒。为了提高抗干扰能力我还会做两个判断。第一如果本次间隔和上次间隔差超过20%说明可能是误检丢弃本次数据第二连续5次有效间隔的平均值作为显示值再用一阶低通平滑一下这样读出来的BPM不会疯狂跳数字。下面给出一个简化版的峰值检测思路实际工程里我会把它封装成状态机#define THRESHOLD_RATIO 0.5f float max_val, min_val; uint8_t above_threshold 0; uint32_t last_peak_time 0; float detect_peak(int16_t filtered_value, uint32_t now_ms) { // 更新窗口最值 if (filtered_value max_val) max_val filtered_value; if (filtered_value min_val) min_val filtered_value; float threshold min_val (max_val - min_val) * THRESHOLD_RATIO; if (filtered_value threshold) { if (!above_threshold) { above_threshold 1; } } else { if (above_threshold) { // 完成一个脉搏上升沿计算间隔 above_threshold 0; float interval_s (now_ms - last_peak_time) / 1000.0f; last_peak_time now_ms; if (interval_s 0.3f interval_s 2.0f) { return 60.0f / interval_s; } } } return -1; }判断间隔在0.3~2秒之间对应心率30~200BPM能够剔除异常值。这个范围可以根据使用人群调整。3.4 显示逻辑与交互OLED驱动这一块SSD1306的驱动代码网上太多了核心就是用I2C写命令和数据。这里不贴完整驱动了重点说一下显示逻辑的架构。我习惯把显示分成四层底层是静态背景比如标题“Heart Rate”中间是波形曲线最上层是数字BPM。波形绘制用“列刷新”方式扫描数组里存最近128个采样点每新来一个点就把数组整体左移一位再把新点放到最右边然后通过画像素函数输出整列数据。这种方法比全屏清屏重画快得多实测在72MHz主频下刷新一帧波形只占很少的CPU时间完全不影响采样。要注意的是OLED的I2C速率建议设成400kHz以下我习惯用400kHz太快了会增加出错概率太慢了显示刷新会卡顿。按键逻辑建议用状态机处理短按切换显示模式长按可以关闭背光或进入演示模式。为了消除按键抖动在中断里延时20ms再确认比在主循环里不断扫描要省心。不过如果系统里已经有1ms时基也可以用软件计时器的方式实现非阻塞消抖。4. 系统联调与实测结果4.1 搭建测试环境我把联调过程分成三步每一步的目标都很明确。第一步是验证传感器通信烧写一个从程序只读器件ID和原始FIFO数据通过串口打印确认I2C链路没问题。第二步是验证原始波形用串口配合PC端串口助手或者Python脚本把ADC原始值画成折线图观察用手指压住传感器时波形是否有明显脉动。第三步才是接OLED、调算法、测BPM。如果你不想开PC纯粹用OLED显示波形也行但串口打印调试信息仍然很有必要。我经常用这样一个小技巧把过滤后的ADC值按固定格式通过串口发出去在电脑上用简单的可视化工具画图一眼就能看出滤波效果和峰值点是否准确定位。这里推荐在串口助手里选“波形显示”模式很多助手自带上位机收到的数据会自动成图。如果没有这个功能用Python的pyserial加matplotlib写十几行代码也能做效率很高。4.2 调参过程与数据记录调参的坑主要在LED电流和阈值比例上。MAX30102的红外LED电流在心率模式下默认是某个值但红光LED电流需要根据实际手指厚度调整。我的经验是先设一个中间档比如6.4mA用串口打印原始ADC值观察波形幅值。如果波形峰值接近满量程4096就降低一档如果峰值只有几百信号太小就调高电流。理想状态是峰值在1200~2500左右这样动态阈值算法有足够的浮动空间。波形出现削顶饱和时说明LED电流太大或增益太高波形幅值过小且噪声明显说明电流太低。你可以通过串口画出的曲线直接判断。比较好的原始波形特征是在基线上有清晰、周期性的单峰峰和谷的差值稳定在500以上且没有明显的高频毛刺。滑动平均窗口长度也值得细调。窗口太短如3噪声抑制不够窗口太长如15脉搏波形会被拉平峰值检测的边沿变得不明显。我最终选了5在噪声和实时性之间取了平衡。如果你用IIR滤波alpha在0.2~0.3之间会比较好。4.3 精度优化与抗干扰处理实测中静止状态下这套系统测出的BPM和医疗级血氧仪对比误差通常在±3BPM以内。但有个很重要的大前提手指必须保持静止轻微抖动都会导致波形变形。为了改善这一点可以引入“信号质量”的概念用最近几次峰值间隔的标准差来判断信号是否稳定。如果标准差太大说明信号质量差显示界面可以提示用户“请保持手指静止”或显示“--”。还有一种常见干扰是周期性环境光比如50Hz日光灯。MAX30102内部的环境光消除电路对稳定光有效但对交流调制光效果有限。解决方案是让传感器尽量贴紧手指用遮光泡棉或黑色胶带挡住外部光线同时保证手指不压太紧否则毛细血管被压扁反而没有脉搏波了。另外如果你发现波形存在高频毛刺检查一下I2C总线上是否有其他设备干扰或者OLED刷新是否会占用太长时间导致采样延迟。一个可靠的做法是把传感器中断优先级设为最高确保每次FIFO数据准备好时能及时读取。5. 常见问题与排查技巧5.1 传感器读不到数据或读出来全是0xFF遇到这个问题先用万用表量一下模块的3.3V和GND是否正常。然后重点检查I2C地址MAX30102是0x57但向寄存器写数据时用0xAE。如果仍然通信失败可能就是接线松动或上拉电阻问题。可以用一个简单的扫描程序在总线上扫描所有I2C地址看看0x57有没有响应。还有一个隐蔽问题如果OLED和MAX30102的地址冲突那肯定不行但SSD1306地址是0x3C不会冲突。不过两条线上如果挂了多个I2C设备总线上拉电阻阻值可能被合得太小导致信号边沿过缓这种情况下可以尝试从4.7kΩ改成10kΩ。5.2 心率数值跳变严重数值跳变基本是峰值检测不准确导致的根源可能是信号质量差、滤波过度或动态阈值更新不及时。排查顺序是先用串口看原始波形确认信号是否正常再确认滤波后的波形是否平滑最后看阈值线是否过高或过低。如果你的程序里窗口最值更新太慢比如窗口长达几百个点那么阈值的变化跟不上基线的漂移很容易漏检。我建议把最值窗口设为1~2秒范围内的采样点数也就是200~400个点。另外运动干扰是跳变的最大来源。手指的纵向压力变化会改变血液容积产生类似脉搏的伪波。我的处理方法是当相邻两个峰值间隔波动超过一定范围时连续丢弃异常值并且不更新BPM宁可显示旧值也不显示不稳定值。这里需要一点小聪明BPM更新要平滑可以用指数移动平均alpha0.3左右。5.3 OLED白屏、花屏或显示不全OLED问题一半是I2C通讯问题一半是电源问题。先检查OLED模块是否接反了SSD1306常用I2C接口在2.2V~5.5V都能工作但供电不稳会导致显示字符乱码。如果白屏不显示大概率是在初始化序列发送之后没有做清屏操作或者I2C总线被其他设备占用。解决方法是初始化OLED后先发一句清屏命令然后再开始画图。花屏则多半是I2C速率太高特别是杜邦线较长时信号完整性下降把I2C速率从400kHz降到200kHz或100kHz即可。还有一个小坑很多人把OLED屏幕贴在面包板上导致屏幕排针和传感器、按键挤在一起干扰源无法排除。建议用独立小转接板固定排针让接线规整至少不会出现虚接导致花屏。5.4 程序跑飞、复位或卡死这类问题通常和中断、内存相关。确保在中断回调里没有做耗时操作比如调用HAL_Delay或打印串口否则系统会卡死在中断里。I2C通信时如果外设没准备好HAL_I2C_Mem_Read可能一直等到超时如果超时时间设得太大主循环就会被卡住。建议把超时时间缩短到50ms通信失败就重试。如果你在代码中用了大数组比如存1024个浮点数注意STM32F103的栈默认只有2KB左右很容易溢出把波形缓存数组改成全局数组或者用malloc分配避免栈炸掉。还有一点工程中如果同时使用了SysTick定时器和延迟函数要注意HAL_Delay依赖SysTick如果修改了系统时钟配置要确保SysTick的优先级合适。调试时如果发现延时不定长大概率是时钟配置有问题。除此之外DWT和系统滴答定时器冲突也是网上常问的问题我的建议是先用示波器测一个GPIO翻转的周期确认延时是否准确再往下调算法。最后一公里把项目变成你能拿得出手的作品最后再分享一个我实际整理这套资料时的心得真正让这个项目从“跑起来”变成“作品”的不是那几十行核心代码而是你对整个信号链路的掌控力。我见过太多人拿官方例程下载进去看到OLED上跳动的数字就觉得成功了但一旦手指动一下、换一个人测、换了块电池数字就开始乱跳这时候如果没有亲手调过电路和算法基本无从下手。如果你按我上面说的思路从传感器寄存器配置开始到波形滤波再到峰值检测和显示刷新一层一层搭起来后面遇到任何异常都能用“先看原始信号再看滤波输出最后看算法结果”这个逻辑快速定位。这也是我为什么反复强调要用串口把中间过程打印出来的原因。这套系统后续还能做不少扩展比如加上蓝牙把心率数据传到手机App加入血氧饱和度SpO2计算或者用FreeRTOS把采样、滤波、显示分别放在不同任务里都是很好的进阶方向。但前提是先把基础的心率链路做得足够稳否则上层功能再炫底层数据不准也是白搭。希望这份拆解能让你在复现项目时少踩几个坑一次把硬件和软件调通。本文还有配套的精品资源点击获取
返回列表