3. 血氧饱和度监测

任务目标

利用MAX30102传感器(内置红光与红外光LED)采集人体手指透射光信号,通过比值法计算血氧饱和度(SpO₂),并在OLED屏幕上实时显示数值与脉搏波形状态。

硬件与依赖

  • 主控:ESP32 / Arduino(I²C接口)
  • 传感器:MAX30102脉搏血氧模块
  • 显示:0.96寸SSD1306 OLED(I²C)
  • 库依赖:Wire.h、Adafruit_SSD1306、Adafruit_GFX、SparkFun_MAX3010x

核心原理

血氧饱和度通过红光(660nm)与红外光(880nm)的交流分量(AC,脉动部分)与直流分量(DC,组织吸收)比值计算:

R = (AC_red / DC_red) / (AC_ir / DC_ir)

经验公式:SpO₂ = 110 - 25 × R(线性近似,适用于R在0.4~1.0范围)

关键完整代码

#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#include <SparkFun_MAX3010x.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

SFE_MAX30102 sensor;
const int SAMPLE_RATE = 100;  // Hz
const int BUFFER_SIZE = 100;  // 1秒数据窗口

float redAC[BUFFER_SIZE], irAC[BUFFER_SIZE];
float redDC[BUFFER_SIZE], irDC[BUFFER_SIZE];
int bufferIndex = 0;

void setup() {
    Serial.begin(115200);
    Wire.begin();
    
    if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
        Serial.println("OLED init failed");
        while(1);
    }
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    
    if (!sensor.begin()) {
        display.setCursor(0, 0);
        display.println("MAX30102 not found");
        display.display();
        while(1);
    }
    
    sensor.setup(SAMPLE_RATE, 4);  // 100Hz采样,4mA LED电流
    sensor.setPulseAmplitudeRed(0x0A);
    sensor.setPulseAmplitudeIR(0x1F);
}

float calculateSpO2() {
    // 计算AC/DC比值
    float redMean = 0, irMean = 0;
    for (int i = 0; i < BUFFER_SIZE; i++) {
        redMean += redDC[i];
        irMean += irDC[i];
    }
    redMean /= BUFFER_SIZE;
    irMean /= BUFFER_SIZE;
    
    float redACSum = 0, irACSum = 0;
    for (int i = 0; i < BUFFER_SIZE; i++) {
        redACSum += abs(redDC[i] - redMean);
        irACSum += abs(irDC[i] - irMean);
    }
    float redACVal = redACSum / BUFFER_SIZE;
    float irACVal = irACSum / BUFFER_SIZE;
    
    if (redACVal < 50 || irACVal < 50) return -1;  // 信号弱
    
    float R = (redACVal / redMean) / (irACVal / irMean);
    float spo2 = 110 - 25 * R;
    return constrain(spo2, 70, 100);
}

void loop() {
    if (sensor.check()) {
        redDC[ bufferIndex] = sensor.getRed();
        irDC[bufferIndex] = sensor.getIR();
        bufferIndex = (bufferIndex + 1) % BUFFER_SIZE;
        
        if (bufferIndex == 0) {  // 每满1秒计算一次
            float spo2 = calculateSpO2();
            
            display.clearDisplay();
            display.setCursor(0, 0);
            display.print("SpO2: ");
            if (spo2 > 0) {
                display.print(spo2, 1);
                display.println("%");
            } else {
                display.println("--");
            }
            
            // 绘制简易脉搏波形(最近50个红外AC值)
            for (int i = 0; i < 50; i++) {
                int idx = (bufferIndex + i) % BUFFER_SIZE;
                int y = 40 - (irDC[idx] / 1000);
                display.drawPixel(i * 2, y, SSD1306_WHITE);
            }
            display.display();
        }
    }
}

运行与验证

  1. 接线:MAX30102与OLED均接I²C(SDA→GPIO21,SCL→GPIO22),VIN→3.3V,GND→GND
  2. 编译上传:Arduino IDE选择ESP32开发板,安装上述库后编译上传
  3. 操作:将手指轻按传感器表面,保持稳定约3秒
  4. 通过条件:OLED显示SpO₂数值在95%~100%之间(健康成年人正常范围),且波形区域有随心跳起伏的脉动曲线;手指移开后数值显示"--"

常见问题

  • 数值持续偏低或显示"--":手指按压过紧或过松,调整至波形幅度稳定;检查LED电流设置是否过低
  • 波形无起伏:确认传感器贴紧皮肤且未受环境光直射;可尝试增大采样率至200Hz
  • I²C地址冲突:MAX30102默认地址0x57,OLED为0x3C,若冲突需修改OLED地址