我一直觉得,“健康监测”这四个字真正落到硬件上,最有成就感的一刻,就是看到自己的实时心率被一块小屏幕画出来。用ESP32驱动MAX30102心率血氧传感器,再把数据送到OLED屏幕上显示——这个项目听起来很工程化,实际上拆开看,只有三个核心组件:一块ESP32主控、一个MAX30102模块、一块0.96寸OLED屏。两天时间,从零开始就能走通。
这篇文章就是给真正零基础的朋友准备的。我不会只贴一堆代码让你复制,而是会把每个选择背后的原因、每根线为什么这么接、每个数字为什么这么设都讲清楚。不管你是之前玩过Arduino想升级到ESP32,还是完全没接触过嵌入式、第一次听说GPIO和I2C,看完这篇都能自己搭出一个戴着手指就能实时显示心率和血氧的小设备。
1. 为什么是这三件套——组件选型里的门道
很多新手拿到别人分享的项目,第一反应是“照着买就行了”,但选型才是真正决定项目成败的第一步。为什么会选ESP32、MAX30102、OLED这三样?我分别说一下我的考虑。
1.1 ESP32不是“必须”,但它是零基础上手的最优解
先看主控。类似项目也有人用Arduino Uno、STM32、树莓派Pico实现,但ESP32有几个明显优势:
- 性能冗余足够:ESP32是双核240MHz,带520KB SRAM,跑心率算法、刷OLED屏、以后想加Wi-Fi上传数据,全部游刃有余。Arduino Uno的ATmega328P只有2KB RAM,MAX30102的血氧算法动辄要开100个uint32_t的缓冲区,光这个缓冲区就吃掉400字节,Uno跑起来会很紧张。
- I2C资源丰富:ESP32的I2C外设非常灵活,可以用软件映射到任意GPIO,默认引脚也能改。MAX30102和OLED都是I2C设备,一根总线上挂两个设备,ESP32天然支持。
- Arduino生态成熟:虽然有ESP-IDF专业开发框架,但零基础完全没必要一上来就啃IDF。Arduino环境下ESP32的开发体验和普通Arduino几乎一样,这也是它适合新手最重要的原因。
- 自带无线能力:ESP32内置Wi-Fi和蓝牙,今天做的是本地显示,明天想做个手机App远程查看,或者把数据传到云端,同一块板子不用换。
1.2 MAX30102模块把复杂光电系统浓缩成一颗芯片
心率血氧传感器市面上不少,最基础的方案是用一对红外对管自己做,我早年也这么干过,结果就是:信号噪声大、波形不稳定、算法写了一周还是跳动剧烈。MAX30102的优势在于,它把红光LED、红外LED、光电检测器、ADC、滤波电路全部封装在一个小模块里,我们只需要通过I2C读寄存器,就能拿到红光和红外两路ADC原始数据。
这个模块的另外一个名字是“MAX30105的近距离表亲”,很多库直接兼容MAX30105。它支持红光(660nm)和红外光(880nm)两路LED,对心率测量来说,红外光通道是主力;对血氧来说,需要同时用红光和红外两路数据的比值来推算。
1.3 OLED屏幕,128x64像素刚刚好
显示部分我选的是最常见的0.96寸OLED,驱动芯片是SSD1306。这个屏幕虽然只有128x64像素,但显示心率数字、血氧数字、小波形绰绰有余。相比LCD1602,OLED不需要背光,对比度高,黑底白字或者黑底蓝字都很有科技感,而且SSD1306驱动在Arduino生态下有Adafruit官方库,开发效率很高。
选屏时注意一点:0.96寸的I2C版才有4个引脚(VCC、GND、SCL、SDA),如果是SPI版引脚更多。本项目强烈建议买I2C版,后面接线和代码都会简单很多。
2. 从两根数据线说起——I2C协议和实物接线
硬件接线看起来只是简单插几根杜邦线,但如果不懂I2C的基本原理,出了问题完全无从下手。这里我花点时间把I2C讲明白,因为它是连接ESP32、MAX30102、OLED三者的桥梁。
2.1 I2C为什么能用两根线挂多台设备
I2C(Inter-Integrated Circuit)是一种串行通信协议,由飞利浦公司发明,特点就是用两根线通信:一根时钟线SCL、一根数据线SDA。主机通过SCL产生时钟节拍,通过SDA发送地址和数据。总线上所有从设备都并联在这两根线上,每个设备有一个唯一的7位地址。
你可以把I2C想象成办公室里的一条内部电话线:总机(ESP32)拨号时先报一个分机号(设备地址),只有对应分机的电话会接起来,其他分机继续安静等着。这就能解释为什么MAX30102和OLED可以接在同一组SDA/SCL引脚上——MAX30102的I2C地址是0x57,OLED屏的地址通常是0x3C,双方互不干扰。
2.2 接线明细
我用的默认I2C引脚是GPIO21(SDA)和GPIO22(SCL),这是ESP32 DevKit系列最常见的一组I2C引脚,也是Arduino代码里不显式指定时的默认设置。
| ESP32引脚 | MAX30102模块 | OLED 0.96寸 (I2C) |
|---|---|---|
| 3.3V | VIN | VCC |
| GND | GND | GND |
| GPIO21 (SDA) | SDA | SDA |
| GPIO22 (SCL) | SCL | SCL |
这里有个容易踩的坑:MAX30102模块上丝印写的可能是VIN而不是VCC,这个VIN可以接3.3V。有些模块板上带了稳压芯片,标的输入范围可能是1.8V到5V,但为了和OLED统一,我建议直接全接3.3V。
另外,如果手头开发板的引脚排列不是GPIO21/22,也不用担心。ESP32的I2C支持软件映射,代码里用Wire.begin(sdaPin, sclPin)就能指定任意GPIO。比如你只有GPIO4和GPIO5空闲,就用Wire.begin(4, 5)。
2.3 关于供电的一个关键建议
这个项目我不要建议你把ESP32的5V引脚直接接MAX30102的VIN。原因很现实:一些MAX30102模块板载的稳压芯片发热不小,如果直接用USB 5V供电,模块温度会明显升高,而红外传感器对温度非常敏感,温漂会导致血氧值持续漂移。
我实测下来,最稳的供电方案是:ESP32用USB供电,MAX30102和OLED都从ESP32的3.3V引脚取电。如果后面要长时间运行,可以在3.3V和GND之间并一个100uF的电解电容,再靠近MAX30102电源脚放一个0.1uF陶瓷电容,这能把电源纹波压下来,手指按压时读数会更稳定。
3. MAX30102从原始数据到BPM和血氧——中间发生了什么
很多人拿到MAX30102第一件事就是跑例程,然后屏幕上出现一个“魔法数字”。但心率血氧到底是怎么算出来的,能说清楚的人不多。这部分不搞懂,后面数据不对时你根本不知道是传感器坏了、算法不对,还是手放歪了。
3.1 光电容积描记法的物理基础
MAX30102的核心测量原理叫PPG(Photoplethysmography,光电容积描记法)。简单来说,皮肤组织和血液对光的吸收能力不一样,血液随着心跳会有周期性的容积变化。把手指贴在传感器上,LED发出的光穿过手指组织,一部分被血液吸收,一部分被反射回来,光电检测器捕捉到被反射/透射的光强,这个光强会随着心跳周期发生微小波动。
这个波动的本质是:心脏收缩时,动脉血容量增加,吸收的光变多,检测器接收到的光变弱;心脏舒张时,血容量减少,透过的光变强。于是,检测器输出的电信号就形成了一条类似脉搏的波形。
MAX30102牛逼的地方在于,它同时集成了红光LED和红外LED。对血氧计算来说,需要对比两种波长的光信号差异:氧合血红蛋白对红外光的吸收较强,还原血红蛋白对红光吸收较强,通过计算红光和红外光的交流分量比值,就能估算血氧饱和度SpO2。
3.2 需要配置的几个关键参数
MAX30102内部有完整的中断、FIFO和ADC系统。初始化时最重要的几个参数如下:
- LED亮度(Pulse Amplitude):每颗LED的驱动电流可调,范围从0到255。手指皮肤厚薄、肤色深浅都会影响最佳亮度。太亮会直接饱和(ADC输出顶到满量程),太暗则信号幅度太小。我第一次测试时用了255,结果红光通道饱和,波形完全平了。调整为50左右基本够用。
- 采样率(Sample Rate):单位是每秒采多少个点。心率范围一般在40~200次/分钟,也就是每分钟2400次心跳,换算成Hz大约40Hz以下就能捕捉到,但为了波形平滑,通常取100Hz。
- 脉冲宽度(Pulse Width):决定LED点亮的时长,也决定ADC的分辨率。411us对应15位ADC分辨率,69us对应18位分辨率,但位数越高,单次测量时间越长。实际项目中411us是性价比最高的选择。
- ADC量程(ADC Range):可选2048、4096、8192、16384。量程越大,能承受的直流偏置越高,但信号的微小变化会被稀释。手指放在传感器上时会有很强的直流分量,一般选4096就能覆盖。
3.3 从原始波形到心率数字
假设你已经通过I2C连续读到红外通道的ADC值,并且画出波形,会看到一个类似“脉搏波”的周期信号。此时心率BPM的计算思路是:检测波形的主峰,记录相邻两个主峰的时间间隔,再用60秒除以间隔秒数。
比如波形每0.8秒出现一个峰,那么BPM = 60 / 0.8 = 75次/分钟。
听上去简单,但真正的难点是抗噪:手指轻抖、环境光变化、传感器没有贴合,都会产生毛刺,这些毛刺会被误判成心跳。所以实际工程中会用Sliding Window均值滤波、动态阈值、或者更复杂的自相关算法。Arduino的heartRate.h库里的checkForBeat()函数本质就是做动态阈值的峰值检测。
3.4 血氧的估算和它的局限性
血氧计算要稍微绕个弯。我们定义R值:
R = (AC_red / DC_red) / (AC_IR / DC_IR)
其中AC是交流分量(脉搏波动的幅度),DC是直流分量(组织对光的稳定吸收)。根据R值,经验公式可以近似写成:
SpO2 ≈ 110 - 25 * R
这个公式是经验拟合,不同论文给出的系数略有差异,但基本逻辑就是这样——R越大,说明还原血红蛋白占比高,血氧越低。所以当你在代码里看到maxim_heart_rate_and_oxygen_saturation()这个函数时,它内部就是在做这些计算。
我提前打个预防针:消费级光电传感器的血氧精度,和医疗级指夹式血氧仪还是有差距的。做好滤波之后,静息状态下和商业指夹式血氧仪的误差一般在2%~3%以内,做健康趋势参考没问题,但不能作为医疗诊断依据。这一点要在产品说明里写清楚,免得读者拿它去量给家里的老人做“开药决策”。
4. OLED屏幕不只是显示——“0.96点不亮”背后的事
OLED部分看起来是最简单的,但恰恰是零基础最容易卡住的地方。我见过大量“OLED点不亮”的提问,一半以上根本原因不是屏幕坏了,而是I2C地址没对上、初始化时序不对或者库版本冲突。
4.1 先确认I2C地址,而不是直接改代码
SSD1306常见地址是0x3C,但也有部分模块是0x3D,还有一些兼容屏地址可能不同。MAX30102是0x57。如果OLED显示不出来,第一步不是换库,而是跑一个I2C扫描程序,把所有能检测到的地址打印出来。
#include <Wire.h> void setup() { Serial.begin(115200); Wire.begin(21, 22); Serial.println("I2C Scanner"); for (byte addr = 1; addr < 127; addr++) { Wire.beginTransmission(addr); if (Wire.endTransmission() == 0) { Serial.print("Found I2C device at 0x"); Serial.println(addr, HEX); } } } void loop() {}把这段程序烧进去,打开串口监视器,波特率调到115200。如果一切正常,你能看到0x3C或0x3D、0x57两个地址。如果什么都没打印,先查接线;如果只看到一个地址,那大概率是另一个设备的地址写错或者模块供电异常。
4.2 显示界面设计的几个原则
屏幕只有128x64像素,信息空间非常宝贵。我个人把界面分成三块:
- 左上区域:显示设备状态,比如“HR 75 bpm”和“SpO2 98%”
- 中间区域:画实时波形,用最近几十个红外采样点画一条滚动曲线
- 下方区域:放一条进度条或提示文字,比如“请轻压手指”
这个布局在128x64的屏幕上是可行的。字体用Adafruit GFX库自带的5x7点阵字体,心率数字用setTextSize(2)放大,波形用drawPixel()逐点绘制。
4.3 OLED刷新率问题
很多人第一次写显示程序,会用display.clearDisplay()把整个屏幕清掉再重画,结果是屏幕疯狂闪烁。原因很简单:清屏+重绘128x64=8192个像素,每次刷新都需要通过I2C传大量数据,刷新率一高就闪烁。
更好的办法是只更新变化区域,或者用“整帧缓冲”的思路:先display.clearDisplay()清空缓冲,再在缓冲里把所有内容画好,最后一次性display.display()提交。Adafruit库本来就是帧缓冲模式,只要你不在一帧中间反复提交,闪烁问题基本能避免。
4.4 中文显示的副产物:字库问题
用OLED显示中文,是初学者绕不开的坎。Adafruit GFX白带字体只支持ASCII字符,要想显示“心率”两个汉字,要么自己取模生成字库,要么用U8g2库。我的建议是:第一个版本先用英文缩写省掉麻烦,HR和SpO2全世界都看得懂。等主流程跑通后,再研究字库取模也不迟。
如果你用U8g2库,选择U8G2_SSD1306_128X64_NONAME_F_HW_I2C这个构造器,并在begin()之前调用setI2CAddress(0x3C * 2),因为U8g2库内部用的是8位地址格式。这个地址乘2的坑,我见过不止一个朋友栽在里面。
5. 环境搭建和完整代码走读——按步骤复现
现在进入实操环节。我会按我实测通过的顺序来写,避免让你走弯路。
5.1 Arduino IDE配置ESP32
打开Arduino IDE,进入“文件 -> 首选项”,在“附加开发板管理器网址”里填入:
https://espressif.github.io/arduino-esp32/package_esp32_index.json然后进入“工具 -> 开发板 -> 开发板管理器”,搜索esp32,找到esp32 by Espressif Systems点击安装。这个安装包比较大,几百MB,需要等一会儿。
安装完成后,在“工具 -> 开发板”下翻到ESP32系列,选择ESP32 Dev Module。如果手头板子用的是CH340或CP2102 USB转串口芯片,记得装好对应驱动,Windows系统一般会自动识别。打开设备管理器确认COM口编号,然后在Arduino IDE里选对COM口。
5.2 安装依赖库
在“工具 -> 管理库”里搜索并安装:
- Adafruit SSD1306
- Adafruit GFX Library
- SparkFun MAX3010x Pulse and Proximity Sensor Library
注意,Adafruit SSD1306安装过程中会提示安装依赖,照做即可。SparkFun的库是MAX30105的,但兼容MAX30102,因为两者寄存器结构几乎完全一样。如果你看到代码里写的是MAX30105 particleSensor,不用慌,无论如何,MAX30102都能用这个库。
5.3 第一版:先验证OLED显示
我习惯每次只加一个功能,先保证屏幕能亮,再让传感器工作,最后把它们拼起来。这样出问题时定位范围很小。下面这段代码只做一件事:初始化OLED并在屏幕上显示一段文字。
#include <Wire.h> #include <Adafruit_SSD1306.h> #include <Adafruit_GFX.h> #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); void setup() { Wire.begin(21, 22); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(10, 20); display.println("OLED OK"); display.display(); } void loop() {}如果屏幕没有亮,先检查0x3C是不是你的真实地址。把这个地址改成I2C扫描出来的值,基本都能亮。
5.4 第二版:初始化MAX30102并读取原始数据
OLED亮起来之后,我们把MAX30102接进来。下面是初始化传感器的代码:
#include <Wire.h> #include "MAX30105.h" #include "heartRate.h" MAX30105 particleSensor; const byte RATE_SIZE = 4; byte rates[RATE_SIZE]; byte rateSpot = 0; long lastBeat = 0; float beatsPerMinute; float beatAvg; void setup() { Serial.begin(115200); Wire.begin(21, 22); if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { Serial.println("MAX30102 not found!"); while (1); } byte ledBrightness = 50; byte sampleAverage = 4; byte ledMode = 2; int sampleRate = 100; int pulseWidth = 411; int adcRange = 4096; particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); particleSensor.setPulseAmplitudeRed(0x0F); particleSensor.setPulseAmplitudeIR(0x1F); } void loop() { long irValue = particleSensor.getIR(); if (checkForBeat(irValue) == true) { long delta = millis() - lastBeat; lastBeat = millis(); beatsPerMinute = 60 / (delta / 1000.0); rates[rateSpot++] = (byte)beatsPerMinute; rateSpot %= RATE_SIZE; beatAvg = 0; for (byte i = 0; i < RATE_SIZE; i++) { beatAvg += rates[i]; } beatAvg /= RATE_SIZE; Serial.print("IR="); Serial.print(irValue); Serial.print(", BPM="); Serial.print(beatsPerMinute); Serial.print(", Avg BPM="); Serial.println(beatAvg); } }这个程序里最关键的是particleSensor.getIR()读取红外通道的值,以及checkForBeat()做峰值检测。如果你把串口监视器打开,会看到IR值在跳动,当你有心跳时BPM会跟着跳动。我第一次跑通这个程序时也蛮激动的,虽然屏幕还没数据,但那种“传感器真的能感知心跳”的体验已经很直观了。
有几个参数说明一下:ledBrightness = 50是LED亮度,我为什么不用默认的255?因为手指贴上去很容易饱和。sampleAverage = 4是硬件均值滤波,一次ADC结果实际是4次采样的平均值,可以起到一定降噪作用。ledMode = 2是双LED模式,红光和红外都亮,给血氧计算做准备。
5.5 血氧计算:调用官方算法库的姿势
上面已经能测心率了,但血氧还需要额外处理。SparkFun的库中自带了spo2_algorithm.h,它的推荐用法是:维护一个缓冲区,连续采集100个红外和红光样本,然后调用算法函数。
我简化过的完整流程如下:
#include <Wire.h> #include "MAX30105.h" #include "spo2_algorithm.h" MAX30105 particleSensor; #define BUFFER_LENGTH 100 uint32_t irBuffer[BUFFER_LENGTH]; uint32_t redBuffer[BUFFER_LENGTH]; int32_t spo2, heartRate; int8_t validSPO2, validHeartRate; void setup() { Serial.begin(115200); Wire.begin(21, 22); if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { Serial.println("MAX30102 not found!"); while (1); } byte ledBrightness = 50; byte sampleAverage = 4; byte ledMode = 2; int sampleRate = 100; int pulseWidth = 411; int adcRange = 4096; particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); for (byte i = 0; i < BUFFER_LENGTH; i++) { while (particleSensor.available() == false) { particleSensor.check(); } redBuffer[i] = particleSensor.getRed(); irBuffer[i] = particleSensor.getIR(); } maxim_heart_rate_and_oxygen_saturation(irBuffer, BUFFER_LENGTH, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate); }调用maxim_heart_rate_and_oxygen_saturation之后,spo2和heartRate就存放着计算结果,validSPO2和validHeartRate是数据有效标志。如果为0,说明当前信号质量太差,不能采信。
但我得说句实在话:这种一次采集100个样本再算一次的做法,在Loop里直接跑会导致刷新率很低。更好的做法是每次循环从FIFO里读新的样本,丢掉最老的一个样本,然后对整个缓冲区重新计算。这就是滑动窗口。因为窗口每次移动一个样本,而算法需要对整个数组重新求一次自相关,计算量不小,但ESP32跑得动。
5.6 第三版:把心率和血氧显示到OLED
最后把显示和采集合并。我在显示端画了波形,使用最近120个IR值绘出一条滚动曲线。核心逻辑是:把IR值在数组里向左移一格,把新值放到最后,然后遍历数组在屏幕上画点。
const int waveLength = 120; int wave[waveLength]; int waveIndex = 0; void drawWave(int value) { for (int i = 0; i < waveLength - 1; i++) { wave[i] = wave[i + 1]; } wave[waveLength - 1] = value; display.clearDisplay(); for (int i = 0; i < waveLength - 1; i++) { int x = i; int y = map(wave[i], 5000, 50000, 50, 20); int x2 = i + 1; int y2 = map(wave[i + 1], 5000, 50000, 50, 20); display.drawLine(x, y, x2, y2, SSD1306_WHITE); } display.setTextSize(1); display.setCursor(0, 0); display.print("HR: "); display.print(beatsPerMinute); display.setCursor(0, 8); display.print("SpO2: "); display.print(spo2); display.println("%"); display.display(); }注意这里的map()映射范围,我取的IR值范围是5000到50000,这个范围根据你实际串口打印的值来调。如果你发现波形一直顶到屏幕边缘或者贴到屏幕底部,就把这两个阈值调一调。
6. 五个最容易翻车的点,我几乎都踩过
写到这里,这个项目已经从零跑通到能显示完整数据了。但我很清楚,很多人跟着做还是会出问题。我把最常见的问题按“症状-原因-对策”整理成一张排查表,你如果卡住了可以直接对照。
| 现象 | 可能原因 | 排除办法 |
|---|---|---|
| OLED不亮,I2C扫描不到任何设备 | 接线错误、SCL/SDA接反、共地问题 | 先不要接MAX30102,只保留OLED,用I2C扫描程序确认地址。确保OLED的VCC接到了3.3V,GND和ESP32的GND确实共地 |
| MAX30102初始化失败,串口打印not found | I2C地址冲突、模块供电不足、SDA/SCL虚接 | 拔掉OLED后单独跑MAX30102初始化的例程。如果用杜邦线,检查插头是否松动。必要时给模块单独一个3.3V电源 |
| OLED能亮,但不停闪烁 | 代码在循环里频繁clearDisplay和display | 只做一次清屏,把要显示的内容都画到缓冲区里,最后统一display一次。避免一帧内多次提交 |
| 心率数值极不稳定,比如显示200或40 | 手指按压太紧、接触不均、动态阈值太灵敏 | 手指轻放不要太用力,等待3-5秒让波形稳定。调低LED亮度避免饱和,或把sampleAverage调大到8 |
| 血氧一直显示95%没有变化 | 算法有效标志为0、红/红外信号强度差异过大、手指放置位置偏 | 把红光和红外的原始ADC值打印出来,看红光通道是否有明显的脉搏波动。如果红外通道波动正常而红光通道几乎不变,往往是放置位置偏移了 |
6.1 一个很多人都忽略的“硬件细节”
MAX30102有些模块上标注的I2C地址是可调的,板上有两个地址选择焊盘或者一个拉高/拉低电阻。默认是0x57,但如果板子厂家动了手脚,可能变成0x58之类的。这也是为什么我反复强调第一次调试务必用I2C扫描程序,而不是照着网上的代码盲填。
6.2 关于传感器“漂移”的玄学
还有一个非常现实的问题:MAX30102持续通电一段时间后,模块温度会升高,红外LED的光强、检测器的暗电流都会受影响,最后体现为BPM数值小幅漂移。这是物理特性决定的,不是代码bug。
如果你要把这个项目做成一个稳定运行的桌面设备,我有几条改机心得:
- 把MAX30102模块的背面贴上散热片或者铜箔胶带,对稳定读数有实际帮助;
- 尽量让模块周围空气流通,不要塞在全封闭外壳里;
- 算法上给BPM加一个“变化限幅”,相邻两次BPM差值超过20就丢弃当前值。这个粗暴的约束在静息状态下效果很好,能滤掉大量尖峰干扰。
6.3 我能理解但你最好不要做的一件事
有网友为了追求高精度,把MAX30102模块的LED亮度开到最大,然后人为加压让手指变红,期望得到一个稳定的血氧值。这样操作的后果是红光通道几乎必然饱和,算法算出来的血氧完全不可信。正确做法是让手指轻放在传感器表面,以遮住光线但没有明显压痕为最好。
7. 实测效果和使用中的几个变化
我在桌面上跑了整整一个下午,整体表现是:静坐状态下心率值稳定在64~72之间,和Apple Watch对比误差在±3以内;血氧稳定在96%~98%之间,符合我这个海拔高度的正常范围。做深蹲测试时,心率能明显上升,休息后逐渐回落,变化趋势很合理。这个精度去不了医院,但作为个人健康趋势参考很有意义。
如果你想让这个项目继续往前走,有几个方向可以考虑:
- 加蓝牙:ESP32自带蓝牙,可以用BLE把心率和血氧数据发给手机App。你可以研究一下ESP32的BLE服务端配置,把心率作为Notify特征值推送出去;
- 加Wi-Fi:把测量数据定时上传到ThingsBoard、巴法云或者其他物联网平台,做长期健康曲线;
- 加屏幕交互:用两个按键切换显示界面,比如一个界面看实时波形,一个界面看历史平均值,一个界面看时间;
- 加震动提醒:如果连续几次测量心率超过设定阈值,就驱动一个震动马达。
我个人最推荐的下一步是“改成一个手腕佩戴式样机”:用一块小锂电池、一个TP4056充电板,再把现有代码功耗优化一下,让ESP32在测量间隙进入浅睡眠。蓝牙和Wi-Fi不用的时候完全关闭,实测能把待机电流控制在几十毫安以内。这样你就能做成一个真正可以戴着走动的健康监测设备,而不是一直插在USB线上。
最后分享一个小细节:MAX30102手指放置的位置非常挑剔,模块上通常会有一个小圆点标记,实际上就是LED窗口的中心。我习惯在模块正面贴一圈黑色的遮光海绵,只留中间一个小孔露出LED,这样能明显减少环境光干扰。这个土办法比什么高级滤波都管用,成本几乎为零,推荐你试试。