ARTICLE DETAIL

资讯详情

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

ESP32与MAX30102心率检测实战:从PPG原理到OLED显示完整教程

ESP32与MAX30102心率检测实战:从PPG原理到OLED显示完整教程 1. MAX30102到底在测什么先弄懂那颗传感器1.1 光电容积脉搏波PPG的物理原理做心率检测这个项目我见过太多人跳过原理直接刷代码最后数据乱跳就开始怀疑模块坏了。其实MAX30102的原理并不复杂心脏每收缩一次动脉血管里的血流量就会有一个脉冲式的增加。MAX30102的LED发出一束光照射到皮肤组织上一部分光被血液、皮肤、骨骼吸收剩下的散射回来被光电二极管接收。血液多的时候吸收的光多反射回来的光就弱血液少的时候反射光就强。于是光电二极管接收到的光信号会跟着心跳出现微小波动把这个波动记录下来就是光电容积脉搏波简称PPG。这跟标题里听心跳的比喻是一样的耳朵听到的是声音的节律MAX30102看到的是光强变化的节律本质都是心脏泵血的周期性。你需要理解一个关键点MAX30102并不是直接测心率的芯片它只输出一路受血流调制过的光强度信号。心率数值是你在单片机里通过算法从这个信号里算出来的。把这两件事分开后面遇到问题才不会乱。1.2 模块上那些元器件分别是干嘛的MAX30102模块上有几个关键部分一颗集成了LED和光电二极管的MAX30102芯片、几个去耦电容有的模块还会带一个稳压芯片和电平转换电路方便接5V单片机。MAX30102芯片内部集成了红光LED660nm、红外光LED880nm和一个光电二极管加上ADC、FIFO、I2C接口、数字滤波等一堆东西。外部单片机通过I2C读写它的寄存器就能拿到经过内部ADC量化后的光强数据。对了模块上那个透明的窗口下面就是LED和光电二极管手指要盖住的正是这个区域。有的模块会带一个隔离环做遮光有的没有后者对贴装和佩戴的要求会更高一些。从使用角度你要记住三个核心概念LED电流决定发光亮度、采样率ADC每秒采多少次、FIFO内部数据缓存队列。这三个参数直接决定了你拿到的信号长什么样。1.3 双波长有什么用MAX30102同时集成了红光和红外光两种LED很多人以为测心率需要两路都用上其实不是。测心率只用红外光就足够因为红外光穿透更深信号受肤色、指甲油等因素影响更小。双波长真正的用途是测血氧饱和度SpO2氧合血红蛋白和脱氧血红蛋白对红光和红外光的吸收率不同通过两路信号的比值可以估算血氧浓度。所以你在代码里会看到ledMode这个参数设置为3代表红光和红外光同时开启。即使你现在只想测心率我也建议保持这个模式原因很简单两路数据可以互相校验信号质量。比如红外光信号明显异常而红光正常说明传感器佩戴可能有问题或者皮肤表面状态对两种波长的吸收差异异常。用一路做主计算另一路做辅助判断是实际项目中很实用的做法。2. 零基础环境搭建IDE、板卡和接线2.1 Arduino IDE里装好ESP32开发板先把Arduino IDE装好我用的是2.x版本界面清爽库管理器也比1.x好用。安装完之后要做两件事配置ESP32开发板支持以及安装MAX30102的驱动库。ESP32开发板的支持要手动添加。打开Arduino IDE的文件 - 首选项 - 附加开发板管理器网址填入官方的ESP32开发板JSON地址。这一步很多新手会漏掉填完之后在开发板管理器里搜索esp32安装Espressif官方维护的那一个。国内网络如果下载慢可以切换镜像源但注意不要用来路不明的第三方源安全第一。装完后在工具 - 开发板里找到ESP32 Dev Module或对应你板子的型号。这里有一个很容易踩的坑很多开发板虽然印着不同的名称但芯片都是ESP32-WROOM-32选ESP32 Dev Module基本都能跑。如果你用的是ESP32-S3或C3就要选对应的型号选错会出现编译能过但烧录失败或上电开机闪退的情况。2.2 安装MAX30102驱动库在Arduino IDE的库管理器里搜索MAX30105安装SparkFun MAX3010x Pulse and Proximity Sensor Library。注意这个库的名字带MAX30105但它同时支持MAX30100、MAX30101、MAX30102、MAX30105这些同系列传感器寄存器大部分兼容。还有另一个库叫MAX30102 by OXullo Interworks底层驱动更轻量适合想自己写算法的人。零基础建议先装SparkFun的库因为示例多、社区讨论多、排错容易。装好之后打开文件 - 示例 - SparkFun MAX3010x Pulse and Proximity Sensor Library - Example5_HeartRate先编译烧录一次看串口能不能输出BPM。如果这个Demo能跑通说明环境和接线都没问题后面改自己的逻辑会轻松很多。2.3 接线图和供电检查MAX30102模块引脚一般有VIN、GND、SCL、SDA有的带INT中断输出引脚。与ESP32开发板的接线非常固定MAX30102模块ESP32开发板VIN3.3VGNDGNDSCLGPIO22SDAGPIO21SCL和SDA用的GPIO21和GPIO22这是ESP32默认的I2C引脚。如果你手里的板子I2C引脚被其他外设占用可以用Wire.begin(sda, scl)在代码里改成其他引脚比如改成GPIO18和GPIO19。供电是个容易出问题的环节。很多ESP32开发板的3.3V来自板载稳压器输出电流有限。MAX30102的LED是脉冲方式点亮的瞬时电流不小如果供电线又细又长LED点亮的瞬间会把电压拉低导致I2C通信不稳定甚至模块复位。我的经验是用尽量短的杜邦线最好在模块的VIN和GND之间并联一个10uF电容。如果你的模块板载了稳压芯片标称可以接5V那也要看清楚丝印不要盲目接5V烧掉后级电路。2.4 上电先做I2C扫描接好线之后不要急着跑心率Demo先用一个I2C扫描程序确认模块有没有被系统认出来。MAX30102在I2C总线上的地址固定是0x57。扫描结果如果不是0x57或者干脆扫不到设备多半是接线问题、供电不稳定或者模块本身有问题。#include Wire.h void setup() { Serial.begin(115200); Wire.begin(21, 22); Serial.println(Scanning...); for (byte address 1; address 127; address) { Wire.beginTransmission(address); if (Wire.endTransmission() 0) { Serial.print(Found I2C device at 0x); Serial.println(address, HEX); } } } void loop() {}扫描到0x57说明I2C链路正常。如果扫不到先查接线再查供电最后换模块。我遇到过几次模块是坏的的判断后来发现其实是杜邦线接触不良所以排查顺序千万别反。3. 信号处理链路为什么裸数据不能直接算心率3.1 原始IR信号里都有什么现在你已经能从串口打印IR值了会看到几百到几千之间波动的数字。这个数字可以拆成两部分直流分量DC和交流分量AC。DC分量是皮肤、骨骼、静脉血等静态部分吸收光之后剩下的反射光强数值大、变化缓慢通常占信号绝大部分。AC分量才是动脉血搏动引起的微小波动往往只有DC的1%到5%。心率信息全部藏在AC分量里。可以这样理解DC分量像房间里的背景灯光AC分量像有人在灯光前晃动手掌造成的阴影变化。你想检测的是那个阴影的节奏但背景光变化、人走动、身体晃动都会干扰这个阴影的判读。如果直接用原始IR值做峰值检测不是不行但阈值很难设。手指按压力度稍微一变整个DC分量漂移了峰值检测就全乱了。3.2 滤波和去基线漂移最简单有效的办法是滑动平均 做差。先算一个窗口大约1秒的滑动平均值这个平均值近似于DC分量然后用原始IR值减去这个平均值剩下的就是AC分量的近似值。更讲究的做法是设计一个带宽0.5Hz到4Hz的带通滤波器因为人体心率范围大约对应30到240次/分钟换算成频率就是0.5Hz到4Hz。滤波后波形干净很多峰值更容易检测。我在实际项目里更常用一种轻量级的自适应基线思路用高通滤波器的形式实时更新基线例如每次读到新IR值后执行baseline baseline * 0.95 ir * 0.05; ac ir - baseline;这样不需要维护数组内存占用少ESP32跑起来没有压力。系数0.95到0.98都可以试基线跟随得越慢AC分量越接近真实波动但对低频漂移的抑制会弱一些。0.95差不多是1秒左右的时间常数对心率检测来说够用。3.3 心率算法的最简实现峰值间隔法心率算法有很多种最常用的是峰值间隔法。思路很直接检测出AC波形里的每一个脉冲峰值记录相邻两个峰值的时间间隔然后BPM 60000 / 间隔毫秒数。峰值检测不能只看当前值比上一次大因为波形上会带毛刺。比较稳的做法是设两个条件当前值超过一个动态阈值比如当前波峰幅度的60%距离上一个峰值至少间隔300毫秒对应最高200次/分钟的心率。两个条件同时满足才认为检测到一个合格的心跳峰。SparkFun库的heartRate.h里checkForBeat()用的就是类似逻辑会根据IR值的动态范围自动调整灵敏度。需要提醒的是峰值间隔法对单个误检很敏感一个毛刺被当成心跳BPM会瞬间飙高所以通常要加一个平均窗口。代码里常见的做法是维护一个长度为4的滑动数组存最近4次心跳间隔算出的BPM再取平均能有效平滑偶发抖动。这个方法简单但有效跑通之后你可以再研究更复杂的算法比如自相关法、傅里叶变换法等但零基础阶段先把峰值间隔法吃透就够用。3.4 采样节奏和FIFO的坑MAX30102内部自带ADC和FIFO外部单片机不需要自己控制采样时刻只需要定时把FIFO里的数据取出来。setup()里配置的sampleRate比如100Hz就是内部ADC的采样频率。坑在于如果主循环里delay(20)循环频率约50Hz而FIFO以100Hz的速度积累数据你每次读取拿到的时间间隔是不均匀的。对心率检测这种需要精确峰值间隔的场景时间抖动会直接影响BPM精度。我建议主循环尽量不delay或者最多降到5ms以内。如果要刷OLED也改成每100ms刷新一次而不是每读一个样本就刷一次。4. 完整代码从初始化到串口和OLED显示4.1 初始化参数怎么设置先用一个具体例子说明setup()里几个参数怎么看。SparkFun库的particleSensor.setup()有6个参数particleSensor.setup(60, 4, 3, 100, 411, 16384);60LED亮度范围0到255亮度越大信号越强但太大会让接收信号饱和4采样平均次数内部对4次ADC结果取平均3LED模式3表示红光红外光都开100采样率100Hz心率检测够用411LED脉冲宽度411微秒脉宽越大信噪比越高16384ADC满量程范围范围越大分辨率越高但也要小心信号太弱时量化噪声明显对零基础的同学这几个参数直接用我给出的值就行实测信号质量不错。如果你把LED亮度调到60之后IR值依然经常触顶数值接近16384或2047说明手指放的区域或者模块增益太强可以适当降到40左右。4.2 数据采集和心率计算主循环里做的事情很简单不断调用getIR()读红外光数据交给checkForBeat()判断是否出现心跳峰。如果出现就根据两次心跳的时间差算出瞬时BPM存入环形数组每存满4个值取一次平均。这里有个细节对瞬时BPM做一次合法性判断只保留30到220之间的数值。这个范围过滤掉了大部分误检比如手指短暂挪动产生的假峰值。有人会把范围调到30到200更保守一点都是可以的主要看你想适应的人群。如果超过2秒没有检测到新的心跳峰说明信号可能丢失了应该在显示上归零或显示--否则屏幕上会停着一个老的BPM数值让人误以为还在正常测量。这种小细节在实际体验里很重要。4.3 用OLED显示心率OLED我选的是0.91英寸128x32或者0.96英寸128x64都能用。驱动库用Adafruit SSD1306在库管理器里安装Adafruit SSD1306和Adafruit GFX Library两个库。OLED和MAX30102都挂在I2C总线上地址通常分别是0x3C和0x57不冲突。在代码里先初始化MAX30102再初始化OLED如果OLED初始化失败程序继续跑不影响心率采集只是显示不了。串口依然能输出数据这对调试很有用。显示刷新频率建议100ms一次太频繁反而让OLED驱动占用太多I2C总线时间可能干扰MAX30102的数据读取。4.4 完整代码下面是一份可以直接复制到Arduino IDE的完整代码集成了心率采集、串口输出和OLED显示。如果你没有OLED把开头几行和loop里显示相关部分注释掉即可。#include Wire.h #include MAX30105.h #include heartRate.h #include Adafruit_SSD1306.h #include Adafruit_GFX.h #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 32 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, Wire, OLED_RESET); MAX30105 particleSensor; const byte RATE_SIZE 4; byte rates[RATE_SIZE]; byte rateSpot 0; long lastBeat 0; float beatsPerMinute 0; int beatAvg 0; unsigned long lastDisplayUpdate 0; void setup() { Serial.begin(115200); Wire.begin(21, 22); if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { Serial.println(MAX30102 not found, check wiring); while (1); } particleSensor.setup(60, 4, 3, 100, 411, 16384); if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(OLED init failed); } display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.println(BPM: --); display.display(); } void loop() { long irValue particleSensor.getIR(); if (checkForBeat(irValue)) { long delta millis() - lastBeat; lastBeat millis(); beatsPerMinute 60.0 / (delta / 1000.0); if (beatsPerMinute 30 beatsPerMinute 220) { rates[rateSpot] (byte)beatsPerMinute; rateSpot % RATE_SIZE; beatAvg 0; for (byte x 0; x RATE_SIZE; x) { beatAvg rates[x]; } beatAvg / RATE_SIZE; } } if (millis() - lastDisplayUpdate 100) { lastDisplayUpdate millis(); Serial.print(IR); Serial.print(irValue); Serial.print(, BPM); Serial.print(beatsPerMinute); Serial.print(, Avg); Serial.println(beatAvg); display.clearDisplay(); display.setCursor(0, 0); display.print(BPM: ); if (beatAvg 0) { display.println(beatAvg); } else { display.println(--); } display.display(); } }烧录板子之前记得在工具里选对开发板型号和串口端口。如果烧录时报Connecting... Failed按住开发板上的BOOT键再点烧录这是ESP32最常见的烧录姿势不算问题。5. 实测排错为什么你测出来的是乱跳的数字5.1 佩戴方式决定信号质量代码跑起来之后很多人第一次看到串口输出的BPM就开始头疼数字在50到180之间乱跳。别急先检查自己的佩戴方式。传感器的窗口要完全贴合手指指腹通常用食指或中指。按压力度不能太重轻轻搭上去就行压太紧会把毛细血管压扁反而没有搏动信号。手指也不能晃动一点微小的抖动都会在光信号里被放大。更妙的是你越试图保持不动手越容易抖这是肌肉紧张造成的。我的经验是靠坐在椅子上把手臂搁在桌面或腿上手腕放松几次呼吸之后信号会慢慢稳定下来。还有一个容易被忽略的点环境光。MAX30102的抗环境光能力比老型号好一些但强光直射依然会干扰。在一个明亮窗户旁边测量效果就明显变差把传感器稍微遮一下会好很多。5.2 用串口绘图器看波形Arduino IDE自带的串口绘图器是个调试神器。把串口监视器关掉打开工具 - 串口绘图器如果代码里打印的是IR12345这种格式显示效果不好。最好是单独打印纯数值比如Serial.println(irValue);这样绘图器会画出一条连续的曲线。把手指放上去你应该能看出一条随着心跳起伏的波形每次心跳对应一个明显的凸起。如果波形是一条直线说明信号太弱或者传感器窗口没有对准如果波形上下乱跳没有规律说明身体晃动或者环境光干扰太严重如果波形顶到天花板就是信号饱和了调低LED亮度。看波形比看BPM数字更能训练你对信号质量的感觉。BPM是一个算法处理后的结果中间任何一环出问题都会让它失真而原始波形是最接近真相的。我调试时永远先看波形再看数字。5.3 常见问题排查表把过去几年被问到最多的问题整理成一张排查表按优先级排序现象可能原因解决办法串口无输出板子没选对、串口端口不对检查开发板型号和端口按住BOOT烧录I2C扫描不到0x57接线错误、供电不足、模块损坏先查接线换短杜邦线并联10uF电容IR数值恒定不变手指没贴合、LED亮度太低重新放置手指调高LED亮度到100试试IR数值触顶信号饱和降低LED亮度到30以下BPM乱跳手指抖动、环境光干扰、信号太弱手放稳遮挡环境光观察波形屏幕显示一直--心率算法没检测到峰看串口绘图器波形确认峰是否明显刚上电BPM很高滤波和平均窗口还没稳定等5秒左右再读数别一放上去就看数字这是我自己的血泪总结。顺便说一句ESP32开发板便宜但MAX30102模块市场上有各种质量层次如果你把该检查的都检查了一遍还是不行换一块模块往往比继续折腾更省时间。5.4 还能怎么进阶这个项目跑通之后你是不会想停在这里的几个值得继续深入的方向把数据通过WiFi上传到手机App或物联网平台做远程心率监测这是ESP32最擅长的活之一。给传感器加一个手指夹或腕带式的固定结构从桌面实验变成可穿戴原型。把血氧算法加上MAX30102的双波长硬件就物尽其用了。数据积累后做HRV心率变异性分析这个就比较专业了但很有研究价值。还可以把平均窗口算法换成卡尔曼滤波或小波去噪让数值更平滑同时处理运动伪迹。最后分享一个我调试这类传感器的小习惯不要一上电就把手指放上去先让模块空跑几秒让FIFO和滤波状态稳定下来再把手放上去等大约5秒让波形进入稳定状态这时候看到的心率值才有参考意义。这个习惯帮我省掉了很多误报时间也希望你的MAX30102第一次出数就是准的。
返回列表