ESP32S3+MAX30102打造便携血氧心率监测仪
2026/9/9 1:26:35 网站建设 项目流程

简介:一款基于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引脚可以用GPIO8(SDA)和GPIO9(SCL),当然在代码里也可以用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电源,部分模块可接5V
MAX30102 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); // SDA=GPIO8, SCL=GPIO9 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); }

setPulseAmplitudeRedsetPulseAmplitudeIR这两个参数很关键,它们控制LED的驱动电流,单位是0.2mA。红光一般设置在0x0F(约3mA),红外设置在0x1A(约5.2mA),这个组合在肤色偏深或者手指偏厚的人身上效果还行。如果信号太弱,可以适当加大红外LED电流,但不要超过0x3F,电流太大会导致传感器发热,反而影响测量稳定性。

3.2 OLED显示逻辑

OLED部分用U8g2库,这个库对中文字体的支持很好,不过中文需要额外的字库文件,项目里显示英文和数字就够了。U8g2初始化的时候要选对构造函数,0.96寸的128x64 OLED,I2C接口,通常用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是OLED,0x57是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,波形明显好了。这个参数因人而异,做出来之后一定要根据自己的使用场景多试几组值。

最后再分享一个小技巧:测试的时候不要把手指一直压着传感器,每隔几分钟松开一下,让传感器模块散散热。一方面长时间压迫手指会影响局部血液循环,导致测量值偏低;另一方面传感器本身会发热,温度升高后红外光的响应特性会漂移,测出来的数值也会慢慢偏掉。想让手环上的血氧计保持准确,也要给它“喘息”的时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询