2. 脉搏血氧饱和度监测
任务说明
本案例实现基于光电容积脉搏波描记法(PPG)的血氧饱和度与脉率监测。使用MAX30102传感器采集红光(660nm)与红外光(940nm)透过手指后的交流分量,通过比值R = (AC_red/DC_red) / (AC_IR/DC_IR) 计算SpO₂,并通过红外光交流信号的峰值间隔计算脉率。系统在OLED屏上实时显示SpO₂百分比与脉率(次/分钟)。
硬件连接
核心代码
// 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, ®, 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, ®, 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烧录到开发板。
验证方法
测试步骤:
- 将手指轻轻按压在MAX30102传感器窗口上,保持稳定
- 观察OLED屏幕,等待约5秒完成首次数据采集与计算
- 记录显示的SpO₂数值与脉率数值
判定标准:
- OLED正常显示SpO₂值在95%~100%之间(健康成年人正常范围)
- 脉率显示在60~100次/分钟范围内(静息状态)
- 数值每5秒刷新一次,且波动范围SpO₂不超过±2%,脉率不超过±5bpm
- 当手指移开时,SpO₂值应下降或显示异常值(代码中限制最低70%)
预期输出示例:对于健康成年人,静息状态下预期显示SpO₂: 97.5%,PR: 72 bpm。实际数值因个体差异和传感器贴合程度而不同,以稳定显示且符合上述范围为准。