2. 脉搏血氧饱和度监测

任务说明

本案例实现基于光电容积脉搏波描记法(PPG)的血氧饱和度与脉率监测。使用MAX30102传感器采集红光(660nm)与红外光(940nm)透过手指后的交流分量,通过比值R = (AC_red/DC_red) / (AC_IR/DC_IR) 计算SpO₂,并通过红外光交流信号的峰值间隔计算脉率。系统在OLED屏上实时显示SpO₂百分比与脉率(次/分钟)。

硬件连接

STM32F103 I2C1: PB6(SCL) PB7(SDA) I2C2: PB10(SCL) PB11(SDA) PB6 PB7 PB10 PB11 MAX30102 红光LED 660nm 红外LED 940nm I2C地址: 0x57 SCL SDA OLED 0.96" I2C地址: 0x3C 128×64 SCL SDA 手指放置 3.3V 电源 VCC→3.3V GND→GND 连接说明 I2C1 → MAX30102 I2C2 → OLED

核心代码


// main.c - 脉搏血氧饱和度监测(STM32F103 + MAX30102 + OLED)
#include "stm32f1xx_hal.h"
#include <stdio.h>
#include <math.h>
#include <string.h>

// ---- MAX30102 寄存器定义 ----
#define MAX30102_ADDR     0x57
#define REG_INTR_STATUS_1 0x00
#define REG_INTR_ENABLE_1 0x02
#define REG_FIFO_WR_PTR   0x04
#define REG_FIFO_RD_PTR   0x06
#define REG_FIFO_DATA     0x07
#define REG_FIFO_CONFIG   0x08
#define REG_SPO2_CONFIG   0x0A
#define REG_LED1_PA       0x0C  // 红光LED电流
#define REG_LED2_PA       0x0D  // 红外LED电流
#define REG_MODE_CONFIG   0x09

// ---- 全局变量 ----
I2C_HandleTypeDef hi2c1;  // MAX30102
I2C_HandleTypeDef hi2c2;  // OLED

#define SAMPLE_RATE  100   // 100Hz采样
#define BUFFER_SIZE  500   // 5秒数据缓冲

uint32_t red_buffer[BUFFER_SIZE];
uint32_t ir_buffer[BUFFER_SIZE];

// ---- I2C读写函数 ----
void MAX30102_WriteReg(uint8_t reg, uint8_t val) {
    uint8_t data[2] = {reg, val};
    HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR << 1, data, 2, 100);
}

uint8_t MAX30102_ReadReg(uint8_t reg) {
    uint8_t val = 0;
    HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR << 1, &reg, 1, 100);
    HAL_I2C_Master_Receive(&hi2c1, MAX30102_ADDR << 1, &val, 1, 100);
    return val;
}

// ---- 初始化MAX30102 ----
void MAX30102_Init(void) {
    MAX30102_WriteReg(REG_MODE_CONFIG, 0x03);      // SpO2模式(红光+红外同时)
    MAX30102_WriteReg(REG_SPO2_CONFIG, 0x27);      // 采样率100Hz, 脉宽411μs
    MAX30102_WriteReg(REG_LED1_PA, 0x24);          // 红光LED电流 ~6.4mA
    MAX30102_WriteReg(REG_LED2_PA, 0x24);          // 红外LED电流 ~6.4mA
    MAX30102_WriteReg(REG_FIFO_CONFIG, 0x0F);      // 采样平均=1, FIFO滚动使能
    MAX30102_WriteReg(REG_INTR_ENABLE_1, 0xC0);    // 使能FIFO几乎满中断
}

// ---- 读取FIFO数据 ----
void MAX30102_ReadFIFO(uint32_t *red, uint32_t *ir) {
    uint8_t reg = REG_FIFO_DATA;
    uint8_t data[6];
    HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR << 1, &reg, 1, 100);
    HAL_I2C_Master_Receive(&hi2c1, MAX30102_ADDR << 1, data, 6, 100);
    *red = ((uint32_t)(data[0] & 0x03) << 16) | ((uint32_t)data[1] << 8) | data[2];
    *ir  = ((uint32_t)(data[3] & 0x03) << 16) | ((uint32_t)data[4] << 8) | data[5];
}

// ---- 信号处理与SpO2计算 ----
float CalculateSpO2(uint32_t *red, uint32_t *ir, int n) {
    // 计算DC分量(平均值)
    float dc_red = 0, dc_ir = 0;
    for (int i = 0; i < n; i++) {
        dc_red += red[i];
        dc_ir  += ir[i];
    }
    dc_red /= n;
    dc_ir  /= n;
    
    // 计算AC分量(标准差近似)
    float ac_red = 0, ac_ir = 0;
    for (int i = 0; i < n; i++) {
        ac_red += (red[i] - dc_red) * (red[i] - dc_red);
        ac_ir  += (ir[i] - dc_ir) * (ir[i] - dc_ir);
    }
    ac_red = sqrtf(ac_red / n);
    ac_ir  = sqrtf(ac_ir / n);
    
    // 比值R
    float r = (ac_red / dc_red) / (ac_ir / dc_ir);
    
    // 经验公式(标准血氧标定曲线近似)
    float spo2 = 110.0f - 25.0f * r;
    if (spo2 > 100.0f) spo2 = 100.0f;
    if (spo2 < 70.0f)  spo2 = 70.0f;
    return spo2;
}

// ---- 脉率检测(基于红外信号峰值间隔) ----
int CalculatePulseRate(uint32_t *ir, int n, int sample_rate) {
    // 寻找峰值
    int peaks[50];
    int peak_count = 0;
    int min_interval = sample_rate / 4;  // 最小间隔0.25s(240bpm上限)
    
    for (int i = 2; i < n - 2; i++) {
        if (ir[i] > ir[i-1] && ir[i] > ir[i-2] &&
            ir[i] > ir[i+1] && ir[i] > ir[i+2]) {
            // 检查是否超过阈值(DC的1%)
            if (peak_count == 0 || (i - peaks[peak_count-1]) > min_interval) {
                peaks[peak_count++] = i;
                if (peak_count >= 50) break;
            }
        }
    }
    
    if (peak_count < 2) return 0;
    
    // 计算平均峰值间隔
    float avg_interval = 0;
    for (int i = 1; i < peak_count; i++) {
        avg_interval += (peaks[i] - peaks[i-1]);
    }
    avg_interval /= (peak_count - 1);
    
    float rate = 60.0f * sample_rate / avg_interval;
    return (int)rate;
}

// ---- OLED显示(简化SSD1306驱动) ----
void OLED_ShowResult(float spo2, int pulse_rate) {
    char buf[32];
    // 清屏
    // ...(SSD1306清屏命令序列)
    
    // 显示SpO2
    // ...(设置光标到第1行)
    snprintf(buf, sizeof(buf), "SpO2: %.1f%%", spo2);
    // ...(逐字符显示)
    
    // 显示脉率
    // ...(设置光标到第3行)
    snprintf(buf, sizeof(buf), "PR: %d bpm", pulse_rate);
    // ...(逐字符显示)
}

// ---- 主程序 ----
int main(void) {
    HAL_Init();
    // ...(系统时钟配置为72MHz)
    
    // I2C初始化(略,使用CubeMX生成)
    // hi2c1: PB6=SCL, PB7=SDA, 400kHz
    // hi2c2: PB10=SCL, PB11=SDA, 400kHz
    
    MAX30102_Init();
    
    // 等待手指放置稳定
    HAL_Delay(2000);
    
    while (1) {
        // 采集5秒数据
        for (int i = 0; i < BUFFER_SIZE; i++) {
            // 等待FIFO有数据
            while ((MAX30102_ReadReg(REG_INTR_STATUS_1) & 0x40) == 0);
            MAX30102_ReadFIFO(&red_buffer[i], &ir_buffer[i]);
            HAL_Delay(10);  // 100Hz采样
        }
        
        // 计算SpO2和脉率
        float spo2 = CalculateSpO2(red_buffer, ir_buffer, BUFFER_SIZE);
        int pulse = CalculatePulseRate(ir_buffer, BUFFER_SIZE, SAMPLE_RATE);
        
        // 显示结果
        OLED_ShowResult(spo2, pulse);
        
        // 每5秒更新一次
        HAL_Delay(100);
    }
}

编译与烧录

使用STM32CubeIDE创建工程,选择STM32F103C8T6芯片。在CubeMX中配置I2C1(PB6/PB7)和I2C2(PB10/PB11)均为400kHz快速模式。将上述代码填入main.c,编译生成HEX文件后通过ST-Link烧录到开发板。

验证方法

测试步骤:

  1. 将手指轻轻按压在MAX30102传感器窗口上,保持稳定
  2. 观察OLED屏幕,等待约5秒完成首次数据采集与计算
  3. 记录显示的SpO₂数值与脉率数值

判定标准:

  • OLED正常显示SpO₂值在95%~100%之间(健康成年人正常范围)
  • 脉率显示在60~100次/分钟范围内(静息状态)
  • 数值每5秒刷新一次,且波动范围SpO₂不超过±2%,脉率不超过±5bpm
  • 当手指移开时,SpO₂值应下降或显示异常值(代码中限制最低70%)

预期输出示例:对于健康成年人,静息状态下预期显示SpO₂: 97.5%,PR: 72 bpm。实际数值因个体差异和传感器贴合程度而不同,以稳定显示且符合上述范围为准。