ESP32S3+MAX30102打造便携血氧心率监测仪

ESP32S3+MAX30102打造便携血氧心率监测仪 简介一款基于MAX30102、ESP32S3与OLED模块的血氧心率测量方案适用于Arduino平台源自高校2024年暑期电子实习课程设计面向嵌入式入门、电子实习及可穿戴健康监测开发场景。资源包共7个文件整体约71.68MB包含Arduino源代码cpp、ino、h、MAX30102参考文档pdf、连线说明与ESP32S3核心板原理图jpg、rar等覆盖从传感器接线、引脚定义到代码编译运行的完整流程方便新手对照实操。已有454人学习下载适合希望快速搭建血氧心率检测设备的开发者参考。资源不仅提供可直接编译运行的程序代码还附带血氧浓度模块工作原理与引脚定义图、额外的资料压缩包可支撑用户理解MAX30102的采集逻辑、排查连线问题并在课程设计基础上进一步扩展网络上报、数据存储等功能。整体按代码、文档、图纸分类结构清晰便于检索与二次开发是电子实习或健康监测项目的高性价比参考资料。 直接开工。这几天把手头的ESP32S3开发板和MAX30102模块翻出来接上一块0.96寸的OLED屏幕搭了一个能测血氧和心率的便携小设备。MAX30102这颗传感器在穿戴设备里很常见很多手环、血氧仪用的就是它配合ESP32S3的算力和Arduino的生态整个项目从接线到出数据一个晚上基本就能跑通。这篇文章把整个实现过程、代码逻辑和踩过的坑都记录下来给想做健康监测、可穿戴小项目的朋友一个参考。1. 项目整体思路与方案选型1.1 为什么选这套硬件组合先说为什么是这三样东西。MAX30102是美信出品的光学脉搏血氧传感器内部集成了红光LED、红外光LED、光电二极管、ADC和数字滤波通过I2C接口输出红光和红外光的吸收数据。它的最大优势是模块化程度高厂家已经帮我们把模拟前端处理好了不需要自己搭运放和滤波电路Arduino里用现成库就能读数据。ESP32S3这块板子选它有几个原因一是它支持Arduino框架开发门槛低HAL库、寄存器操作都不用管二是它自带WiFi和蓝牙后续想加手机App同步、云端上传都很轻松三是它的算力对于跑血氧饱和度计算这种算法来说绰绰有余哪怕后续加LVGL图形界面都不会卡。OLED模块则纯粹是为了本地显示0.96寸的I2C屏幕功耗低、接线简单四根线就能搞定适合这种便携小设备。提示如果用ESP32的经典款如ESP32 DevKitC也能做但ESP32S3的GPIO更多而且带AI加速指令虽然这个项目用不上但后续扩展空间更大。1.2 血氧心率测量的基本原理动手之前得把原理讲清楚不然代码写完你都不知道为什么要这么写。脉搏血氧仪的核心是光电容积脉搏波描记法简称PPG。它的原理是血液中的氧合血红蛋白和脱氧血红蛋白对红光660nm和红外光940nm的吸收率不同。氧合血红蛋白对红外光吸收更强、对红光吸收更弱脱氧血红蛋白则相反。传感器把两种光轮流打到手指上光电二极管接收穿透组织后的光强信号。心脏搏动时动脉血管内的血容量会周期性变化导致光吸收量也随之波动这个波动信号就是脉搏波。通过分析红光和红外光信号的交流分量与直流分量的比值再代入经验公式就能估算出血氧饱和度。心率则直接数脉搏波波峰的时间间隔就能得到。原理不复杂但实际做的时候光路干扰、手指抖动、环境光都会影响信号质量这也是后面代码里要做滤波处理的原因。2. 硬件接线与开发环境准备2.1 模块接线对照表接线是整个项目最基础也最容易出问题的地方。MAX30102模块和OLED屏幕都是I2C接口把它们挂在ESP32S3的同一组I2C总线上就行。ESP32S3的默认I2C引脚可以用GPIO8SDA和GPIO9SCL当然在代码里也可以用Wire.begin()重新指定任意引脚灵活度很高。MAX30102模块有VIN、GND、SDA、SCL四个引脚部分模块还引出INT中断脚。OLED屏幕通常是VCC、GND、SCL、SDA。接线时要注意有些MAX30102模块板载了电平转换电路VIN可以接3.3V或者5V如果是裸芯片模块必须接3.3V接5V会烧芯片。模块引脚ESP32S3引脚说明MAX30102 VIN3.3V电源部分模块可接5VMAX30102 GNDGND共地MAX30102 SDAGPIO8I2C数据线MAX30102 SCLGPIO9I2C时钟线OLED VCC3.3V电源OLED GNDGND共地OLED SDAGPIO8与MAX30102共用OLED SCLGPIO9与MAX30102共用2.2 Arduino环境配置开发环境用Arduino IDE 2.x版本安装好之后需要添加ESP32的开发板支持。打开Arduino IDE的“开发板管理器”搜索esp32安装Espressif官方提供的esp32包。这一步如果下载慢可以在“附加开发板网址”里填国内镜像源地址我用的是https://espressif.github.io/arduino-esp32/package_esp32_index.json实测下载速度稳定很多。库方面需要装三个SparkFun MAX3010x Pulse and Proximity Sensor Library用于读取MAX30102原始数据、U8g2用于驱动OLED屏幕、Wire库是内置的不用额外装。库安装直接在“库管理器”里搜索安装注意SparkFun库支持的是MAX30101和MAX30102安装的时候要选对。注意MAX30102在Arduino中的设备地址默认是0x57如果I2C扫描不到设备优先排查供电和接线。另外这个传感器对供电稳定性比较敏感最好在VIN和GND之间并一个10uF电解电容能明显减少信号毛刺。2.3 开发板参数选择ESP32S3在Arduino里的选型注意一下开发板管理器中选“ESP32S3 Dev Module”这个选项适用于大多数ESP32S3核心板。如果板子带4MB Flash选默认配置就行如果Flash是8MB或16MB在“Flash Size”里对应选上否则烧录会失败。USB模式选“USB CDC On Boot: Enabled”这样串口监视器才能正常输出数据同时按住BOOT键再插USB可以进入下载模式。3. 核心代码实现与原理剖析3.1 MAX30102初始化配置MAX30102有一个配套的芯片级算法库能够直接从传感器读出心率和血氧值但它对信号质量要求比较高手指稍微动一下数值就会跳。我的做法是用SparkFun库读原始的红外光和红光AC/DC数据然后在ESP32S3上自己算心率和血氧。这样做的优点是中间环节都掌握在自己手里出问题好排查而且后面想换算法、加滤波随时可以改。初始化代码如下#include Wire.h #include MAX30105.h #include heartRate.h MAX30105 particleSensor; void setup() { Wire.begin(8, 9); // SDAGPIO8, SCLGPIO9 Serial.begin(115200); if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { Serial.println(MAX30102 not found); while (1); } particleSensor.setup(60); // 采样率60Hz particleSensor.setPulseAmplitudeRed(0x0F); // 红光LED电流 particleSensor.setPulseAmplitudeIR(0x1A); // 红外LED电流 particleSensor.setPulseAmplitudeProximity(0x1F); }setPulseAmplitudeRed和setPulseAmplitudeIR这两个参数很关键它们控制LED的驱动电流单位是0.2mA。红光一般设置在0x0F约3mA红外设置在0x1A约5.2mA这个组合在肤色偏深或者手指偏厚的人身上效果还行。如果信号太弱可以适当加大红外LED电流但不要超过0x3F电流太大会导致传感器发热反而影响测量稳定性。3.2 OLED显示逻辑OLED部分用U8g2库这个库对中文字体的支持很好不过中文需要额外的字库文件项目里显示英文和数字就够了。U8g2初始化的时候要选对构造函数0.96寸的128x64 OLEDI2C接口通常用U8G2_SSD1306_128X64_NONAME_F_HW_I2C。注意末尾的“HW_I2C”表示硬件I2C构造函数里传入U8G2_R0表示不旋转屏幕。显示刷新我用了一个简单的策略只在数值变化超过一定阈值时才刷新屏幕。OLED写入速度不快每帧全屏刷新大概要30ms左右如果每秒刷新50次占用的时间非常可观还会和传感器采样抢I2C总线的带宽。我设置了心率变化超过2bpm或者血氧变化超过1%才重新绘制这样屏幕不会频繁闪烁也减少了I2C总线上的冲突。#include U8g2lib.h U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* clock*/ 9, /* data*/ 8, /* reset*/ U8X8_PIN_NONE); void displayData(int hr, float spo2) { char buf[16]; u8g2.clearBuffer(); u8g2.setFont(u8g2_font_10x20_tf); itoa(hr, buf, 10); u8g2.drawStr(10, 30, HR:); u8g2.drawStr(45, 30, buf); dtostrf(spo2, 4, 1, buf); u8g2.drawStr(10, 55, SpO2:); u8g2.drawStr(70, 55, buf); u8g2.sendBuffer(); }U8X8_PIN_NONE的意思是不使用复位引脚因为大多数OLED模块把复位脚悬空了。这里用了软件I2C模式初始化虽然速度比硬件I2C慢一点但它允许你自由指定时钟和数据引脚跟上面Wire.begin()指定8、9引脚的逻辑一致。实际测试中这个显示频率完全够用肉眼看不到闪烁。3.3 心率与血氧计算逻辑心率计算用的是峰值检测法。先对红外光信号做滑动平均滤波窗口大小取5个采样点这样可以去掉一部分高频噪声又不会把真实的脉搏波峰削平。然后设定一个动态阈值阈值当前窗口内信号均值0.6倍的标准差。当信号从低于阈值穿越到高于阈值时记为一个心跳然后根据两次心跳的时间间隔计算出瞬时心率。这个算法比网上流传的固定阈值法要好用因为不同人的手指透光性差异很大固定阈值很容易失灵。动态阈值能自动适应信号幅值的变化但它的缺点是有时会误检尤其是在手指移动或者传感器没贴紧的时候。所以我还在代码里加了心率合理性判断计算出的心率如果在30到220之间才更新显示超出范围就保留上一次的有效值。血氧计算相比心率要麻烦一些。经典的经验公式是SpO2 110 - 25 * R其中R是红光和红外光交流分量比值除以直流分量比值。这个公式是近似值实际使用中偏差挺大尤其是在血氧饱和度低于90%的时候。更精确的做法是用查找表把R值映射到对应的血氧值那组数据需要大量实验标定业余条件做不了那么准。我的代码里用的是简化处理用7秒窗口内的红光和红外光波形分别计算交流分量的峰峰值和直流分量的平均值然后计算R值再套公式。实测静息状态下跟商场里的指夹式血氧仪对比误差在±2%以内睡眠监测场景下基本够用。#define BUFFER_SIZE 420 // 60Hz采样率下7秒数据 float irBuffer[BUFFER_SIZE]; float redBuffer[BUFFER_SIZE]; int bufferIndex 0; int irPeaks[20]; int peakCount 0; uint32_t lastBeatTime 0; void processPPG(float irValue, float redValue) { irBuffer[bufferIndex] irValue; redBuffer[bufferIndex] redValue; bufferIndex; if (bufferIndex BUFFER_SIZE) { bufferIndex 0; calculateSpO2(); } }3.4 实时性与多任务处理ESP32S3是双核芯片但Arduino框架下默认只用了一个核。为了让传感器采样和OLED显示互不干扰我把loop()里的采样逻辑和显示逻辑做了分时调度传感器以60Hz的频率持续采样每采完一个点就检测一次心跳显示部分只在数值更新时才运行。这样设计下60Hz的采样率就保证了脉搏波形的完整复现心率计算的实时性也够。如果后续想做得更精细可以用FreeRTOS任务把采样和UI显示放在不同核上一个核跑传感器读取和滤波算法一个核跑屏幕刷新和用户交互。我自己试过这种方案代码复杂度会上一个台阶但系统的稳定性和扩展性确实更好特别是以后要加蓝牙传输或者SD卡存储的时候。用xTaskCreatePinnedToCore把两个任务分别绑到core0和core1上I2C设备归采样任务独占OLED显示归UI任务独占避开了总线竞争。4. 实际操作过程与调试验证4.1 完整接线与上电检查我把ESP32S3开发板插在面包板一侧MAX30102模块和OLED模块插在另一侧用杜邦线连接。这里有个小建议连线尽量短特别是I2C的SDA和SCL线超过20cm之后信号完整性会明显下降导致传感器读取偶发失败。如果要延长最好用双绞线或者屏蔽线。上电后先用I2C扫描程序确认两个设备都能被识别。扫描结果是两个地址0x3C是OLED0x57是MAX30102。如果只扫到一个设备检查对应模块的供电和接线。我遇到过一次OLED能显示但MAX30102死活扫描不到的情况最后发现是模块的VIN接错了引脚接到ESP32S3的5V引脚上而那个模块不支持5V供电。4.2 传感器信号质量验证在跑完整算法之前先通过串口把红外光的原始波形打印出来用手动方式观察波形形态。用串口绘图器打开波形图正常情况下能看到很规律的脉冲波形波峰之间的间隔均匀幅值稳定。如果波形乱跳、峰谷不清晰先检查手指是否放正了传感器窗口要完全贴住指腹再检查LED电流设置。这个环节非常值得花时间。我自己的经验是如果原始波形质量不过关后面无论算法多好都是白搭。通过串口绘图器观察波形还能顺便判断滤波参数合不合理滑动平均窗口设太大波形会变钝设太小噪声压不住。窗口设5个点对应60Hz采样下的约83ms时间窗是我试下来比较好的平衡点。4.3 实测数据对比验证最后一步是把设备固定在一个地方手指放上去静置一分钟和商用的指夹式血氧仪做对比。我自己测出来的静息心率在62到68bpm之间血氧在97%到99%之间跟对照设备的值对得上。深呼吸测试时心率会明显下降血氧基本不变这符合正常生理规律。运动后测试时心率能够快速响应但血氧值偶尔会出现冒高到100%的情况这是因为运动后手指微微出汗影响了光的透过率。这时把手指擦干再测就恢复正常了。总的来说家用场景下这个精度完全够用医疗级就别想了刻度校准和温度补偿这些东西业余条件做不了。5. 常见问题与排查技巧5.1 传感器读取超时或读不到数据这个问题我遇到很多次原因基本集中在三处。第一是接线问题杜邦线接触不良是最大的坑我后来把所有连接处都用胶带固定了第二是供电不稳MAX30102的LED瞬间电流比较大如果供电线细长电压跌落会造成传感器复位表现为读着读着突然超时解决方法是换粗线并加电容第三是地址冲突如果总线上还有别的I2C设备检查地址是不是和0x57冲突了。如果以上都正常还是读不到把I2C时钟频率降低试试particleSensor.begin(Wire, I2C_SPEED_STANDARD)用标准模式牺牲一点速度换稳定性。5.2 心率数值跳变异常大的原因与对策数值跳变通常不是传感器坏了而是信号质量差。手指没放稳是最常见的原因只要微微一动波形就会变形。另外传感器窗口被油脂污染也会导致信号幅值骤降棉签蘸酒精清洁一下就能恢复。算法层面动态阈值对突变信号特别敏感所以在代码里加了限制条件如果两个相邻波峰的间隔小于300ms或者大于2000ms就认为是无效数据直接丢弃。这样即使信号瞬间抖动屏幕上显示的数值也不会疯跳。5.3 OLED屏幕花屏或显示异常花屏几乎都是I2C总线信号质量引起的。ESP32S3的GPIO8和GPIO9内部有上拉但OLED模块和MAX30102模块的板载上拉电阻可能没有或者阻值太大。解决办法是在SDA和SCL到3.3V之间各接一个4.7k欧姆的上拉电阻。如果接了电阻还是花屏把U8g2的初始化从硬件I2C改成软件I2C试试即用U8G2_SSD1306_128X64_NONAME_F_SW_I2C并传入时钟和数据引脚。5.4 常见问题速查表现象可能原因排查方法I2C扫描不到MAX30102供电接错、接线不良、芯片损坏检查VIN电压重新拔插杜邦线波形毛刺多环境光干扰、手指未压实遮挡传感器周围光线重新放置手指心率稳定但血氧不准R值计算偏差、公式标定不够调整LED电流增大计算窗口OLED显示慢全屏刷新频率过高只在数值变化时刷新烧录失败选错开发板型号、未进入下载模式按住BOOT再插USB检查Flash大小设置6. 扩展玩法与项目心得6.1 从测量到物联网这个项目最吸引人的地方是数据的可扩展性。ESP32S3自带WiFi和蓝牙测出来的数据可以直接通过MQTT协议上报到Home Assistant或者其他物联网平台做成睡眠监测、运动心率记录这类应用。我也试过用蓝牙BLE把数据发到手机上在微信小程序里实时显示波形和数据延迟大约100ms左右体验已经比较流畅了。数据存储方面如果想把历史数据记下来加一张MicroSD卡模块用Arduino的SD库写CSV文件就行。这样每次测量完还能回看心率变化曲线比单看当前值有用得多。6.2 项目中的经验教训做这个项目踩过的最大坑是盲目相信库函数。SparkFun库自带的getHeartRate()和getSpO2()方法虽然能直接出结果但它内部封装的算法对信号质量要求极高手指稍微一动数值就跳得没法看。后来我改成自己写滤波和峰值检测虽然代码多了一百多行但对信号质量的掌控感完全不一样了。另一个教训是红外LED的电流设置真的不能照搬示例代码。起初我用官方例程里的0x0F在自己手上信号就偏弱尤其是冬天手冷、末梢循环不好的时候波形幅值会掉到峰值检测阈值以下。后来把红外电流提高到0x1A波形明显好了。这个参数因人而异做出来之后一定要根据自己的使用场景多试几组值。最后再分享一个小技巧测试的时候不要把手指一直压着传感器每隔几分钟松开一下让传感器模块散散热。一方面长时间压迫手指会影响局部血液循环导致测量值偏低另一方面传感器本身会发热温度升高后红外光的响应特性会漂移测出来的数值也会慢慢偏掉。想让手环上的血氧计保持准确也要给它“喘息”的时间。本文还有配套的精品资源点击获取