前言
本篇基于 STM32F103C8T6 主控搭配 MAX30102 光电血氧心率传感器、0.96 寸 I2C OLED 显示屏完成整套脉搏血氧采集工程,全程使用软件 I2C 通信,分开两路 I2C 总线分别驱动传感器与屏幕,规避总线冲突问题。文章完整拆解硬件接线、光学检测原理、寄存器配置、FIFO 数据读取、心率血氧算法、实物调试、完整源码模块,适合嵌入式初学者、课程设计、电子竞赛直接参考。
重要说明:本方案仅作教学演示使用,未经过医疗级临床标定,测量数值仅用于学习信号处理逻辑,不可作为医疗诊断依据。
视频讲解:STM32F103驱动MAX30102血氧心率检测完整教程(含OLED显示、软件I2C源码)
https://www.bilibili.com/video/BV11Vbk6vE3Y/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb
一、整体实现流程

二、硬件电路接线说明
2.1 引脚对应表
MAX30102 传感器接线
| SCL | PB6 | 传感器软件 I2C 时钟 |
| SDA | PB7 | 传感器软件 I2C 数据 |
| INT | 悬空 | 工程采用轮询方式读取数据,未使用中断引脚 |
| VCC | 3.3V | 模块供电,严禁 5V 直供 |
| GND | GND | 主控与传感器共地,保证通信稳定 |

OLED 显示屏接线
| SCL | PB8 | 屏幕独立软件 I2C 时钟 |
| SDA | PB9 | 屏幕独立软件 I2C 数据 |
| VCC | 3.3V | 显示屏供电 |
| GND | GND | 共地处理 |
2.2 两路独立软件 I2C 设计优势
MAX30102 与 OLED 都采用 I2C 通信,但工程分开两套引脚做独立软件 I2C:
- 传感器:PB6 (SCL)、PB7 (SDA)
- 显示屏:PB8 (SCL)、PB9 (SDA)
优势:
2.3 供电与上拉电阻注意事项
三、MAX30102 光学检测核心原理
本传感器采用反射式 PPG 光电容积脉搏波检测法,依靠两种波长光线区分心率与血氧。
3.1 心率检测原理
心脏收缩时手指动脉血流量变大,舒张时血流量减小,血液多少会改变光线吸收、反射强度,光电二极管输出波形会带有和心跳同步的周期性波动。 波形分为两部分:
软件先滤除缓慢变化的 DC 基线,再识别波形峰值。 本工程采样率固定 100Hz,心率计算公式: HR = 6000 / N N 为相邻脉搏峰之间的平均采样点数,举例:峰间隔 80 个采样点,心率 = 6000/80=75 次 / 分钟。
3.2 血氧检测原理
含氧血红蛋白、脱氧血红蛋白对红光、红外光吸收系数差异巨大,通过两路光波形比值估算血氧饱和度。
四、I2C 通信、寄存器与 FIFO 缓存机制
4.1 I2C 设备地址
MAX30102 7 位设备地址 0x57;写操作地址 0xAE,读操作地址 0xAF。
4.2 工程核心寄存器配置说明
| 0x00 | INTR_STATUS_1 | 读取 PPG_RDY 标志,判断是否产生新采样数据 |
| 0x02 | INTR_ENABLE_1 | 写入 0x40,开启新样本就绪状态标志 |
| 0x04 | FIFO_WR_PTR | FIFO 写指针,记录芯片存入数据位置 |
| 0x05 | OVF_COUNTER | FIFO 溢出计数,防止数据丢失 |
| 0x06 | FIFO_RD_PTR | FIFO 读指针,记录 MCU 读取数据位置 |
| 0x07 | FIFO_DATA | 连续读取一组红光 + 红外 18 位采样原始值 |
| 0x08 | FIFO_CONFIG | 配置值 0x0F,设置 FIFO 缓存参数 |
| 0x09 | MODE_CONFIG | 配置值 0x03,开启血氧双光检测模式 |
| 0x0A | SPO2_CONFIG | 配置值 0x27,4096nA 电流、100Hz 采样、18 位采样精度 |
| 0x0C | LED1_PA | 红光 LED 驱动电流设置 0x24 |
| 0x0D | LED2_PA | 红外 LED 驱动电流设置 0x24 |
| 0xFF | PART_ID | 读取芯片 ID 0x15,用于硬件在线检测 |
4.3 FIFO 数据存储格式
血氧模式下,一组完整采样数据占用 6 字节: 字节 0~2:红光 18 位原始数据 字节 3~5:红外光 18 位原始数据 数据提取方式: RED = ((B0 << 16) | (B1 << 8) | B2) & 0x03FFFF IR = ((B3 << 16) | (B4 << 8) | B5) & 0x03FFFF 18 位采样数值范围:0 ~ 262143;芯片内置 32 组 FIFO 缓存,MCU 刷新屏幕短暂阻塞时,不会丢失采样数据。
4.4 FIFO 数据读取逻辑
程序同时读取 FIFO 读写指针差值与 PPG 就绪中断标志: 仅对比指针会出现 “缓存已满时读写指针重合,误判为空缓存” 问题;结合溢出计数、就绪标志双重判断,消除数据读取歧义。
五、数据采样与算法完整处理逻辑
工程关键固定参数
| 采样率 | 100Hz | 每秒输出 100 组红光 + 红外采样 |
| 算法波形窗口 | 400 点 | 单次完整计算依靠 4 秒波形数据 |
| 刷新步长 | 100 点 | 基础采样完成后每秒更新一次结果 |
| 手指红外阈值 | 5000 | IR 采样值低于该值判定未放置手指 |
| 手指丢失计数 | 20 点 | 连续 0.2 秒低信号清空采样缓存 |
| 最小脉搏峰间距 | 30 点 | 过滤噪声伪峰,最大识别心率 200 次 / 分 |
| 心率有效区间 | 30~200BPM | 超出范围判定结果无效 |
| 血氧有效区间 | 70~100% | 超出范围不展示测量数值 |
六、OLED 屏幕显示信息说明
屏幕分多种状态打印内容,方便调试与直观观察:
6.1 正常测量成功界面
IR:123456
HR:075 BPM
SpO2:098 %
MEASUREMENT OK
字段释义:
6.2 调试信息界面(无有效采样时)
W:00 R:00 S:000
- W:FIFO 写指针
- R:FIFO 读指针
- S:中断状态寄存器十进制值,S=64 代表新样本就绪
6.3 各类异常提示
七、实物演示效果章节
7.1 实物硬件清单
7.2 实物接线操作步骤
7.3 正常测量演示

八、源代码模块(可直接复制插入区域)
本节预留完整源码插入位置,工程分为 3 个核心文件:MAX30102 底层驱动、OLED 软件 I2C 驱动、主函数逻辑
8.1 工程文件结构
MAX30102.c // 传感器底层读写、FIFO读取、采样缓存维护
oled.c // 独立软件I2C屏幕驱动
main.c // 主循环、状态刷新、OLED打印显示
MAX30102算法函数 // 去基线、峰值检测、心率血氧计算模块
main.c代码
/* MAX30102 heart-rate/SpO2 teaching demonstration for STM32F103C8T6. */
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "MAX30102.h"
#define LOOP_DELAY_MS 10U
#define DISPLAY_LOOP_COUNT 20U
static void MAX30102_ShowData(const MAX30102_DataTypeDef *data);
static void MAX30102_ShowError(void);
int main(void)
{
MAX30102_DataTypeDef sensor = {0};
u8 display_count = 0U;
u8 sensor_ok;
delay_init();
NVIC_Configuration();
OLED_Init();
OLED_ColorTurn(0);
OLED_DisplayTurn(0);
OLED_Clear();
OLED_ShowString(0, 0, (u8 *)"MAX30102 DEMO", 16);
OLED_ShowString(0, 24, (u8 *)"INITIALIZING…", 12);
OLED_Refresh();
sensor_ok = MAX30102_Init();
if(sensor_ok == 0U)
{
MAX30102_ShowError();
}
else
{
MAX30102_ShowData(&sensor);
}
while(1)
{
if(sensor_ok != 0U)
{
if(MAX30102_Update(&sensor) == 0U)
{
sensor_ok = 0U;
MAX30102_ShowError();
}
else
{
display_count++;
if(display_count >= DISPLAY_LOOP_COUNT)
{
display_count = 0U;
MAX30102_ShowData(&sensor);
}
}
}
delay_ms(LOOP_DELAY_MS);
}
}
static void MAX30102_ShowData(const MAX30102_DataTypeDef *data)
{
OLED_Clear();
if(data->fifo_active != 0U)
{
OLED_ShowString(0, 0, (u8 *)"IR:", 12);
OLED_ShowNum(18, 0, data->ir, 6, 12);
}
else
{
OLED_ShowString(0, 0, (u8 *)"W:", 12);
OLED_ShowNum(12, 0, data->fifo_write_pointer, 2, 12);
OLED_ShowString(30, 0, (u8 *)"R:", 12);
OLED_ShowNum(42, 0, data->fifo_read_pointer, 2, 12);
OLED_ShowString(60, 0, (u8 *)"S:", 12);
OLED_ShowNum(72, 0, data->interrupt_status, 3, 12);
}
OLED_ShowString(0, 14, (u8 *)"HR:", 16);
if(data->heart_rate_valid != 0U)
{
OLED_ShowNum(32, 14, data->heart_rate, 3, 16);
}
else
{
OLED_ShowString(32, 14, (u8 *)"—", 16);
}
OLED_ShowString(64, 14, (u8 *)"BPM", 16);
OLED_ShowString(0, 32, (u8 *)"SpO2:", 16);
if(data->spo2_valid != 0U)
{
OLED_ShowNum(48, 32, data->spo2, 3, 16);
}
else
{
OLED_ShowString(48, 32, (u8 *)"—", 16);
}
OLED_ShowString(80, 32, (u8 *)"%", 16);
if(data->fifo_active == 0U)
{
OLED_ShowString(0, 52, (u8 *)"WAIT FIFO DATA", 12);
}
else if(data->finger_detected == 0U)
{
OLED_ShowString(0, 52, (u8 *)"FINGER / IR LOW", 12);
}
else if(data->progress < 100U)
{
OLED_ShowString(0, 52, (u8 *)"SAMPLING", 12);
OLED_ShowNum(60, 52, data->progress, 3, 12);
OLED_ShowString(78, 52, (u8 *)"%", 12);
}
else if((data->heart_rate_valid == 0U) || (data->spo2_valid == 0U))
{
OLED_ShowString(0, 52, (u8 *)"KEEP FINGER STILL", 12);
}
else
{
OLED_ShowString(0, 52, (u8 *)"MEASUREMENT OK", 12);
}
OLED_Refresh();
}
static void MAX30102_ShowError(void)
{
u8 error;
error = MAX30102_GetLastError();
OLED_Clear();
OLED_ShowString(0, 0, (u8 *)"MAX30102 ERROR", 16);
if(error == MAX30102_ERROR_I2C)
{
OLED_ShowString(0, 20, (u8 *)"I2C NO ACK", 12);
}
else if(error == MAX30102_ERROR_PART_ID)
{
OLED_ShowString(0, 20, (u8 *)"PART ID:", 12);
OLED_ShowNum(54, 20, MAX30102_GetPartID(), 3, 12);
}
else if(error == MAX30102_ERROR_RESET)
{
OLED_ShowString(0, 20, (u8 *)"RESET FAILED", 12);
}
else if(error == MAX30102_ERROR_CONFIG)
{
OLED_ShowString(0, 20, (u8 *)"CONFIG FAILED", 12);
}
else
{
OLED_ShowString(0, 20, (u8 *)"FIFO I2C ERROR", 12);
}
OLED_ShowString(0, 36, (u8 *)"SCL:PB6 SDA:PB7", 12);
if(error == MAX30102_ERROR_PART_ID)
{
OLED_ShowString(0, 52, (u8 *)"EXPECT:021 (0x15)", 12);
}
else
{
OLED_ShowString(0, 52, (u8 *)"CHECK I2C PULL-UP", 12);
}
OLED_Refresh();
}
MAX30102.c代码
#include "MAX30102.h"
#include "delay.h"
/**
* @file MAX30102.c
* @brief MAX30102 软件 I2C、FIFO 采样及心率血氧计算实现。
* @note 文件编码为 UTF-8;测量结果仅用于教学演示,不用于医疗诊断。
*/
/* MAX30102 的 7 位 I2C 地址为 0x57,发送时需要左移一位。 */
#define MAX30102_ADDR_WRITE 0xAEU
#define MAX30102_ADDR_READ 0xAFU
/* MAX30102 寄存器地址表。 */
#define REG_INTR_STATUS_1 0x00U
#define REG_INTR_STATUS_2 0x01U
#define REG_INTR_ENABLE_1 0x02U
#define REG_INTR_ENABLE_2 0x03U
#define REG_FIFO_WR_PTR 0x04U
#define REG_OVF_COUNTER 0x05U
#define REG_FIFO_RD_PTR 0x06U
#define REG_FIFO_DATA 0x07U
#define REG_FIFO_CONFIG 0x08U
#define REG_MODE_CONFIG 0x09U
#define REG_SPO2_CONFIG 0x0AU
#define REG_LED1_PA 0x0CU
#define REG_LED2_PA 0x0DU
#define REG_PILOT_PA 0x10U
#define REG_REV_ID 0xFEU
#define REG_PART_ID 0xFFU
#define MAX30102_FIFO_DEPTH 32U /* 硬件 FIFO 可保存的样本组数 */
#define MAX30102_WINDOW_SAMPLES 400U /* 算法窗口:100 Hz 下约 4 秒 */
#define MAX30102_WINDOW_STEP 100U /* 每收到 100 个新样本重新计算 */
#define MAX30102_FINGER_IR_MIN 5000UL /* 判断手指存在的最低红外原始值 */
#define MAX30102_FINGER_LOST 20U /* 连续低信号达到此值才判定手指离开 */
#define MAX30102_MIN_AC_LEVEL 80UL /* 有效脉搏交流分量的最低幅度 */
#define MAX30102_MIN_PEAK_GAP 30U /* 相邻脉搏峰最小间隔,对应最高约 200 BPM */
#define MAX30102_MAX_PEAKS 16U /* 单个算法窗口允许保存的最大峰值数量 */
#define MAX30102_SCL_HIGH() GPIO_SetBits(GPIOB, MAX30102_I2C_SCL_PIN)
#define MAX30102_SCL_LOW() GPIO_ResetBits(GPIOB, MAX30102_I2C_SCL_PIN)
#define MAX30102_SDA_HIGH() GPIO_SetBits(GPIOB, MAX30102_I2C_SDA_PIN)
#define MAX30102_SDA_LOW() GPIO_ResetBits(GPIOB, MAX30102_I2C_SDA_PIN)
#define MAX30102_SDA_READ() GPIO_ReadInputDataBit(GPIOB, MAX30102_I2C_SDA_PIN)
/* 心率血氧算法使用的滑动采样窗口及运行状态。 */
static u32 max30102_red_buffer[MAX30102_WINDOW_SAMPLES];
static u32 max30102_ir_buffer[MAX30102_WINDOW_SAMPLES];
static s32 max30102_filtered[MAX30102_WINDOW_SAMPLES];
static u16 max30102_sample_count;
static u8 max30102_no_finger_count;
static u8 max30102_measurement_ready;
static u8 max30102_last_error;
static u8 max30102_part_id;
/* 驱动内部函数声明。 */
static void MAX30102_I2C_Delay(void);
static void MAX30102_SDA_Output(void);
static void MAX30102_SDA_Input(void);
static void MAX30102_I2C_Start(void);
static void MAX30102_I2C_Stop(void);
static u8 MAX30102_I2C_WriteByte(u8 value);
static u8 MAX30102_I2C_ReadByte(u8 acknowledge);
static void MAX30102_I2C_BusRecovery(void);
static u8 MAX30102_ReadBytes(u8 reg, u8 *buffer, u8 length);
static void MAX30102_ClearResult(MAX30102_DataTypeDef *data);
static void MAX30102_ResetMeasurement(MAX30102_DataTypeDef *data);
static void MAX30102_AddSample(u32 red, u32 ir, MAX30102_DataTypeDef *data);
static void MAX30102_Calculate(MAX30102_DataTypeDef *data);
static u32 MAX30102_IntegerSqrt(uint64_t value);
/** @brief 软件 I2C 半周期延时。 */
static void MAX30102_I2C_Delay(void)
{
delay_us(4);
}
/*
* 每次发送、应答和读取时都显式切换 SDA 方向。
* 输入上拉模式可在模块上拉电阻较弱时保持 SDA 电平稳定。
*/
static void MAX30102_SDA_Output(void)
{
GPIO_InitTypeDef gpio;
gpio.GPIO_Pin = MAX30102_I2C_SDA_PIN;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
gpio.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_Init(GPIOB, &gpio);
}
static void MAX30102_SDA_Input(void)
{
GPIO_InitTypeDef gpio;
GPIO_SetBits(GPIOB, MAX30102_I2C_SDA_PIN);
gpio.GPIO_Pin = MAX30102_I2C_SDA_PIN;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
gpio.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &gpio);
}
/** @brief 产生 I2C 起始信号:SCL 为高电平时 SDA 由高变低。 */
static void MAX30102_I2C_Start(void)
{
MAX30102_SDA_Output();
MAX30102_SDA_HIGH();
MAX30102_SCL_HIGH();
MAX30102_I2C_Delay();
MAX30102_SDA_LOW();
MAX30102_I2C_Delay();
MAX30102_SCL_LOW();
}
/** @brief 产生 I2C 停止信号:SCL 为高电平时 SDA 由低变高。 */
static void MAX30102_I2C_Stop(void)
{
MAX30102_SDA_Output();
MAX30102_SCL_LOW();
MAX30102_SDA_LOW();
MAX30102_I2C_Delay();
MAX30102_SCL_HIGH();
MAX30102_I2C_Delay();
MAX30102_SDA_HIGH();
MAX30102_I2C_Delay();
}
/**
* @brief 通过软件 I2C 发送一个字节并等待从机应答。
* @return 收到应答返回 1,等待应答超时返回 0。
*/
static u8 MAX30102_I2C_WriteByte(u8 value)
{
u8 i;
u16 timeout;
MAX30102_SDA_Output();
MAX30102_SCL_LOW();
for(i = 0U; i < 8U; i++)
{
MAX30102_SCL_LOW();
if((value & 0x80U) != 0U)
{
MAX30102_SDA_HIGH();
}
else
{
MAX30102_SDA_LOW();
}
MAX30102_I2C_Delay();
MAX30102_SCL_HIGH();
MAX30102_I2C_Delay();
value <<= 1;
}
MAX30102_SCL_LOW();
MAX30102_SDA_Input();
MAX30102_SDA_HIGH();
MAX30102_I2C_Delay();
MAX30102_SCL_HIGH();
MAX30102_I2C_Delay();
timeout = 200U;
while((MAX30102_SDA_READ() != Bit_RESET) && (timeout > 0U))
{
timeout–;
delay_us(1);
}
MAX30102_SCL_LOW();
MAX30102_SDA_Output();
return (timeout > 0U) ? 1U : 0U;
}
/**
* @brief 通过软件 I2C 读取一个字节。
* @param acknowledge 非 0 表示读完后发送 ACK,0 表示发送 NACK。
*/
static u8 MAX30102_I2C_ReadByte(u8 acknowledge)
{
u8 i;
u8 value = 0U;
MAX30102_SDA_Input();
MAX30102_SDA_HIGH();
for(i = 0U; i < 8U; i++)
{
MAX30102_SCL_LOW();
MAX30102_I2C_Delay();
MAX30102_SCL_HIGH();
value <<= 1;
if(MAX30102_SDA_READ() != Bit_RESET)
{
value |= 0x01U;
}
MAX30102_I2C_Delay();
}
MAX30102_SCL_LOW();
MAX30102_SDA_Output();
if(acknowledge != 0U)
{
MAX30102_SDA_LOW();
}
else
{
MAX30102_SDA_HIGH();
}
MAX30102_I2C_Delay();
MAX30102_SCL_HIGH();
MAX30102_I2C_Delay();
MAX30102_SCL_LOW();
MAX30102_SDA_HIGH();
return value;
}
/** @brief 输出 9 个时钟并发送停止信号,用于释放异常占用的 I2C 总线。 */
static void MAX30102_I2C_BusRecovery(void)
{
u8 i;
MAX30102_SDA_Output();
MAX30102_SDA_HIGH();
for(i = 0U; i < 9U; i++)
{
MAX30102_SCL_LOW();
MAX30102_I2C_Delay();
MAX30102_SCL_HIGH();
MAX30102_I2C_Delay();
}
MAX30102_I2C_Stop();
}
/** @brief 向指定寄存器写入一个字节。 */
u8 MAX30102_WriteRegister(u8 reg, u8 value)
{
MAX30102_I2C_Start();
if((MAX30102_I2C_WriteByte(MAX30102_ADDR_WRITE) == 0U) ||
(MAX30102_I2C_WriteByte(reg) == 0U) ||
(MAX30102_I2C_WriteByte(value) == 0U))
{
MAX30102_I2C_Stop();
return 0U;
}
MAX30102_I2C_Stop();
return 1U;
}
/** @brief 从指定寄存器开始连续读取多个字节。 */
static u8 MAX30102_ReadBytes(u8 reg, u8 *buffer, u8 length)
{
u8 i;
if((buffer == 0) || (length == 0U))
{
return 0U;
}
MAX30102_I2C_Start();
if((MAX30102_I2C_WriteByte(MAX30102_ADDR_WRITE) == 0U) ||
(MAX30102_I2C_WriteByte(reg) == 0U))
{
MAX30102_I2C_Stop();
return 0U;
}
MAX30102_I2C_Start();
if(MAX30102_I2C_WriteByte(MAX30102_ADDR_READ) == 0U)
{
MAX30102_I2C_Stop();
return 0U;
}
for(i = 0U; i < length; i++)
{
buffer[i] = MAX30102_I2C_ReadByte((i + 1U < length) ? 1U : 0U);
}
MAX30102_I2C_Stop();
return 1U;
}
/** @brief 从指定寄存器读取一个字节。 */
u8 MAX30102_ReadRegister(u8 reg, u8 *value)
{
return MAX30102_ReadBytes(reg, value, 1U);
}
/** @brief 从 FIFO 读取红光和红外光各一个 18 位原始样本。 */
u8 MAX30102_ReadFIFO(u32 *red, u32 *ir)
{
u8 raw[6];
if((red == 0) || (ir == 0))
{
return 0U;
}
if(MAX30102_ReadBytes(REG_FIFO_DATA, raw, 6U) == 0U)
{
return 0U;
}
*red = (((u32)raw[0] << 16) | ((u32)raw[1] << 8) | raw[2]) & 0x03FFFFUL;
*ir = (((u32)raw[3] << 16) | ((u32)raw[4] << 8) | raw[5]) & 0x03FFFFUL;
return 1U;
}
/**
* @brief 初始化 MAX30102。
* @details
* 依次完成 GPIO 配置、I2C 总线恢复、芯片型号检测、软件复位、
* FIFO 清空以及 SpO2 模式配置。任一步骤失败都会记录对应错误码。
*/
u8 MAX30102_Init(void)
{
GPIO_InitTypeDef gpio;
u8 value;
u8 timeout;
u8 attempt;
max30102_last_error = MAX30102_ERROR_NONE;
max30102_part_id = 0U;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_SetBits(GPIOB, MAX30102_I2C_SCL_PIN | MAX30102_I2C_SDA_PIN);
gpio.GPIO_Pin = MAX30102_I2C_SCL_PIN | MAX30102_I2C_SDA_PIN;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
gpio.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_Init(GPIOB, &gpio);
MAX30102_SCL_HIGH();
MAX30102_SDA_HIGH();
delay_ms(10);
MAX30102_I2C_BusRecovery();
for(attempt = 0U; attempt < 3U; attempt++)
{
if(MAX30102_ReadRegister(REG_PART_ID, &value) != 0U)
{
break;
}
MAX30102_I2C_BusRecovery();
delay_ms(2);
}
if(attempt >= 3U)
{
max30102_last_error = MAX30102_ERROR_I2C;
return 0U;
}
max30102_part_id = value;
if(value != MAX30102_EXPECTED_PART_ID)
{
max30102_last_error = MAX30102_ERROR_PART_ID;
return 0U;
}
if(MAX30102_WriteRegister(REG_MODE_CONFIG, 0x40U) == 0U)
{
max30102_last_error = MAX30102_ERROR_RESET;
return 0U;
}
timeout = 100U;
do
{
delay_ms(1);
if(MAX30102_ReadRegister(REG_MODE_CONFIG, &value) == 0U)
{
max30102_last_error = MAX30102_ERROR_RESET;
return 0U;
}
timeout–;
} while(((value & 0x40U) != 0U) && (timeout > 0U));
if((value & 0x40U) != 0U)
{
max30102_last_error = MAX30102_ERROR_RESET;
return 0U;
}
/* 按已验证的五针模块驱动顺序配置工作寄存器。 */
if((MAX30102_WriteRegister(REG_INTR_ENABLE_1, 0x40U) == 0U) ||
(MAX30102_WriteRegister(REG_INTR_ENABLE_2, 0x00U) == 0U) ||
(MAX30102_WriteRegister(REG_FIFO_WR_PTR, 0x00U) == 0U) ||
(MAX30102_WriteRegister(REG_OVF_COUNTER, 0x00U) == 0U) ||
(MAX30102_WriteRegister(REG_FIFO_RD_PTR, 0x00U) == 0U) ||
(MAX30102_WriteRegister(REG_FIFO_CONFIG, 0x0FU) == 0U) ||
(MAX30102_WriteRegister(REG_MODE_CONFIG, 0x03U) == 0U) ||
(MAX30102_WriteRegister(REG_SPO2_CONFIG, 0x27U) == 0U) ||
(MAX30102_WriteRegister(REG_LED1_PA, 0x24U) == 0U) ||
(MAX30102_WriteRegister(REG_LED2_PA, 0x24U) == 0U) ||
(MAX30102_WriteRegister(REG_PILOT_PA, 0x7FU) == 0U))
{
max30102_last_error = MAX30102_ERROR_CONFIG;
return 0U;
}
delay_ms(10);
if((MAX30102_ReadRegister(REG_MODE_CONFIG, &value) == 0U) ||
((value & 0x07U) != 0x03U))
{
max30102_last_error = MAX30102_ERROR_CONFIG;
return 0U;
}
/* 读取两个状态寄存器,清除上电或配置过程中遗留的中断标志。 */
if((MAX30102_ReadRegister(REG_INTR_STATUS_1, &value) == 0U) ||
(MAX30102_ReadRegister(REG_INTR_STATUS_2, &value) == 0U))
{
max30102_last_error = MAX30102_ERROR_CONFIG;
return 0U;
}
max30102_sample_count = 0U;
max30102_no_finger_count = 0U;
max30102_measurement_ready = 0U;
max30102_last_error = MAX30102_ERROR_NONE;
return 1U;
}
/** @brief 返回最近一次初始化或 FIFO 通信错误码。 */
u8 MAX30102_GetLastError(void)
{
return max30102_last_error;
}
/** @brief 返回初始化阶段读取到的芯片型号标识。 */
u8 MAX30102_GetPartID(void)
{
return max30102_part_id;
}
/** @brief 清除心率、血氧数值及其有效标志。 */
static void MAX30102_ClearResult(MAX30102_DataTypeDef *data)
{
data->heart_rate = 0U;
data->spo2 = 0U;
data->heart_rate_valid = 0U;
data->spo2_valid = 0U;
}
/** @brief 清空当前采样进度,在手指离开或 FIFO 异常后重新测量。 */
static void MAX30102_ResetMeasurement(MAX30102_DataTypeDef *data)
{
max30102_sample_count = 0U;
max30102_measurement_ready = 0U;
data->progress = 0U;
data->finger_detected = 0U;
MAX30102_ClearResult(data);
}
/**
* @brief 计算 64 位无符号整数的平方根。
* @note 使用逐位算法,避免在 Cortex-M3 上引入浮点运算库。
*/
static u32 MAX30102_IntegerSqrt(uint64_t value)
{
uint64_t bit;
uint64_t result;
result = 0U;
bit = ((uint64_t)1U << 62);
while(bit > value)
{
bit >>= 2;
}
while(bit != 0U)
{
if(value >= result + bit)
{
value -= result + bit;
result = (result >> 1) + bit;
}
else
{
result >>= 1;
}
bit >>= 2;
}
return (u32)result;
}
/**
* @brief 根据 4 秒红光和红外光采样窗口计算心率与血氧。
* @details
* 血氧使用红光/红外光交流分量与直流分量的比值估算;心率先去除
* 缓慢变化的直流基线,再检测有效脉搏峰并根据平均峰间距换算 BPM。
*/
static void MAX30102_Calculate(MAX30102_DataTypeDef *data)
{
u16 i;
u16 start;
u16 end;
u16 peak_count;
u16 peak_locations[MAX30102_MAX_PEAKS];
u32 ir_sum;
u32 red_sum;
u32 ir_mean;
u32 red_mean;
u32 ir_ac;
u32 red_ac;
u32 interval_sum;
u32 average_interval;
u32 valid_interval_sum;
u16 valid_intervals;
s32 baseline;
s32 difference;
s32 positive_peak;
s32 negative_peak;
s32 threshold;
s32 polarity;
uint64_t ir_square_sum;
uint64_t red_square_sum;
uint64_t denominator;
uint64_t ratio_x100;
s32 spo2_x100;
ir_sum = 0U;
red_sum = 0U;
MAX30102_ClearResult(data);
for(i = 0U; i < MAX30102_WINDOW_SAMPLES; i++)
{
ir_sum += max30102_ir_buffer[i];
red_sum += max30102_red_buffer[i];
}
ir_mean = ir_sum / MAX30102_WINDOW_SAMPLES;
red_mean = red_sum / MAX30102_WINDOW_SAMPLES;
if((ir_mean < MAX30102_FINGER_IR_MIN) || (red_mean == 0U))
{
MAX30102_ClearResult(data);
return;
}
/* 通过红光和红外光的交流/直流双比值估算教学用血氧值。 */
ir_square_sum = 0U;
red_square_sum = 0U;
for(i = 0U; i < MAX30102_WINDOW_SAMPLES; i++)
{
difference = (s32)max30102_ir_buffer[i] – (s32)ir_mean;
ir_square_sum += (uint64_t)((int64_t)difference * difference);
difference = (s32)max30102_red_buffer[i] – (s32)red_mean;
red_square_sum += (uint64_t)((int64_t)difference * difference);
}
ir_ac = MAX30102_IntegerSqrt(ir_square_sum / MAX30102_WINDOW_SAMPLES);
red_ac = MAX30102_IntegerSqrt(red_square_sum / MAX30102_WINDOW_SAMPLES);
if((ir_ac >= MAX30102_MIN_AC_LEVEL) && (red_ac >= MAX30102_MIN_AC_LEVEL))
{
denominator = (uint64_t)ir_ac * red_mean;
ratio_x100 = ((uint64_t)red_ac * ir_mean * 100U + denominator / 2U) /
denominator;
if((ratio_x100 >= 30U) && (ratio_x100 <= 170U))
{
spo2_x100 = 9485L;
spo2_x100 += (s32)((30354ULL * ratio_x100 + 500ULL) / 1000ULL);
spo2_x100 -= (s32)((4506ULL * ratio_x100 * ratio_x100 + 5000ULL) / 10000ULL);
if(spo2_x100 > 10000L)
{
spo2_x100 = 10000L;
}
if(spo2_x100 >= 7000L)
{
data->spo2 = (u16)((spo2_x100 + 50L) / 100L);
data->spo2_valid = 1U;
}
}
}
/* 去除缓慢变化的直流基线,再在交流波形中寻找脉搏峰。 */
baseline = (s32)max30102_ir_buffer[0];
positive_peak = 0;
negative_peak = 0;
for(i = 0U; i < MAX30102_WINDOW_SAMPLES; i++)
{
baseline += ((s32)max30102_ir_buffer[i] – baseline) / 16;
max30102_filtered[i] = (s32)max30102_ir_buffer[i] – baseline;
if(max30102_filtered[i] > positive_peak)
{
positive_peak = max30102_filtered[i];
}
if(max30102_filtered[i] < negative_peak)
{
negative_peak = max30102_filtered[i];
}
}
polarity = (positive_peak >= -negative_peak) ? 1 : -1;
threshold = ((polarity > 0) ? positive_peak : -negative_peak) * 35L / 100L;
if(threshold < (s32)MAX30102_MIN_AC_LEVEL)
{
data->heart_rate_valid = 0U;
return;
}
peak_count = 0U;
for(i = 2U; i < (MAX30102_WINDOW_SAMPLES – 2U); i++)
{
s32 current;
s32 previous;
s32 next;
current = max30102_filtered[i] * polarity;
previous = max30102_filtered[i – 1U] * polarity;
next = max30102_filtered[i + 1U] * polarity;
if((current >= threshold) && (current >= previous) && (current > next))
{
if(peak_count == 0U)
{
peak_locations[peak_count++] = i;
}
else if((i – peak_locations[peak_count – 1U]) < MAX30102_MIN_PEAK_GAP)
{
if(current > max30102_filtered[peak_locations[peak_count – 1U]] * polarity)
{
peak_locations[peak_count – 1U] = i;
}
}
else if(peak_count < MAX30102_MAX_PEAKS)
{
peak_locations[peak_count++] = i;
}
}
}
if(peak_count < 3U)
{
data->heart_rate_valid = 0U;
return;
}
interval_sum = 0U;
for(i = 1U; i < peak_count; i++)
{
interval_sum += peak_locations[i] – peak_locations[i – 1U];
}
average_interval = interval_sum / (peak_count – 1U);
/* 剔除与平均峰间距偏差过大的运动伪峰。 */
valid_interval_sum = 0U;
valid_intervals = 0U;
start = (u16)(average_interval * 60U / 100U);
end = (u16)(average_interval * 140U / 100U);
for(i = 1U; i < peak_count; i++)
{
u16 interval;
interval = peak_locations[i] – peak_locations[i – 1U];
if((interval >= start) && (interval <= end))
{
valid_interval_sum += interval;
valid_intervals++;
}
}
if(valid_intervals >= 2U)
{
average_interval = valid_interval_sum / valid_intervals;
if(average_interval != 0U)
{
data->heart_rate = (u16)((MAX30102_SAMPLE_RATE_HZ * 60U +
average_interval / 2U) / average_interval);
if((data->heart_rate >= 30U) && (data->heart_rate <= 200U))
{
data->heart_rate_valid = 1U;
}
}
}
}
/**
* @brief 处理一组新样本并加入算法窗口。
* @details 红外值过低时累计手指丢失次数;窗口填满后每新增 100 个样本
* 重新计算一次,并保留最近 3 秒数据形成滑动窗口。
*/
static void MAX30102_AddSample(u32 red, u32 ir, MAX30102_DataTypeDef *data)
{
u16 i;
data->red = red;
data->ir = ir;
data->sample_ready = 1U;
data->fifo_active = 1U;
if(ir < MAX30102_FINGER_IR_MIN)
{
if(max30102_no_finger_count < MAX30102_FINGER_LOST)
{
max30102_no_finger_count++;
}
if(max30102_no_finger_count >= MAX30102_FINGER_LOST)
{
MAX30102_ResetMeasurement(data);
}
return;
}
max30102_no_finger_count = 0U;
data->finger_detected = 1U;
if(max30102_sample_count < MAX30102_WINDOW_SAMPLES)
{
max30102_red_buffer[max30102_sample_count] = red;
max30102_ir_buffer[max30102_sample_count] = ir;
max30102_sample_count++;
}
if(max30102_measurement_ready != 0U)
{
data->progress = 100U;
}
else
{
data->progress = (u8)(((u32)max30102_sample_count * 100U) /
MAX30102_WINDOW_SAMPLES);
}
if(max30102_sample_count >= MAX30102_WINDOW_SAMPLES)
{
MAX30102_Calculate(data);
max30102_measurement_ready = 1U;
for(i = 0U; i < (MAX30102_WINDOW_SAMPLES – MAX30102_WINDOW_STEP); i++)
{
max30102_red_buffer[i] = max30102_red_buffer[i + MAX30102_WINDOW_STEP];
max30102_ir_buffer[i] = max30102_ir_buffer[i + MAX30102_WINDOW_STEP];
}
max30102_sample_count = MAX30102_WINDOW_SAMPLES – MAX30102_WINDOW_STEP;
data->progress = 100U;
}
}
/**
* @brief 轮询中断状态和 FIFO 指针,读取当前全部可用样本。
* @details 同时利用 PPG_RDY 状态位解决 FIFO 满时读写指针相等的歧义。
*/
u8 MAX30102_Update(MAX30102_DataTypeDef *data)
{
u8 interrupt_status;
u8 write_pointer;
u8 read_pointer;
u8 overflow;
u8 available;
u8 i;
u32 red;
u32 ir;
if(data == 0)
{
return 0U;
}
data->sample_ready = 0U;
if((MAX30102_ReadRegister(REG_INTR_STATUS_1, &interrupt_status) == 0U) ||
(MAX30102_ReadRegister(REG_FIFO_WR_PTR, &write_pointer) == 0U) ||
(MAX30102_ReadRegister(REG_OVF_COUNTER, &overflow) == 0U) ||
(MAX30102_ReadRegister(REG_FIFO_RD_PTR, &read_pointer) == 0U))
{
max30102_last_error = MAX30102_ERROR_FIFO;
return 0U;
}
write_pointer &= 0x1FU;
read_pointer &= 0x1FU;
data->interrupt_status = interrupt_status;
data->fifo_write_pointer = write_pointer;
data->fifo_read_pointer = read_pointer;
available = (write_pointer – read_pointer) & (MAX30102_FIFO_DEPTH – 1U);
if((overflow & 0x1FU) != 0U)
{
/* 发生溢出后指针相等表示 FIFO 已满,而不是 FIFO 为空。 */
if(available == 0U)
{
available = MAX30102_FIFO_DEPTH;
}
}
else if((available == 0U) && ((interrupt_status & 0x40U) != 0U))
{
/* PPG_RDY 置位时至少有一个新样本,可消除指针相等的歧义。 */
available = 1U;
}
for(i = 0U; i < available; i++)
{
if(MAX30102_ReadFIFO(&red, &ir) == 0U)
{
max30102_last_error = MAX30102_ERROR_FIFO;
return 0U;
}
MAX30102_AddSample(red, ir, data);
}
if((overflow & 0x1FU) != 0U)
{
if(MAX30102_WriteRegister(REG_OVF_COUNTER, 0U) == 0U)
{
max30102_last_error = MAX30102_ERROR_FIFO;
return 0U;
}
}
max30102_last_error = MAX30102_ERROR_NONE;
return 1U;
}
MAX30102.h代码
#ifndef __MAX30102_H
#define __MAX30102_H
#include "sys.h"
/**
* @file MAX30102.h
* @brief MAX30102 心率血氧传感器驱动接口。
* @note 文件编码为 UTF-8,适用于 STM32F103C8T6 教学演示工程。
*/
/*
* MAX30102 接线:
* SCL -> PB6
* SDA -> PB7
*
* PB6/PB7 组成 MAX30102 独立软件 I2C 总线;OLED 使用 PB8/PB9,
* 两条总线相互独立,刷新 OLED 时不会改变传感器的总线状态。
*/
#define MAX30102_I2C_SCL_PIN GPIO_Pin_6
#define MAX30102_I2C_SDA_PIN GPIO_Pin_7
#define MAX30102_EXPECTED_PART_ID 0x15U /* MAX30102 芯片型号标识 */
#define MAX30102_SAMPLE_RATE_HZ 100U /* 采样率:100 个样本/秒 */
/* 驱动错误码,可通过 MAX30102_GetLastError() 读取。 */
#define MAX30102_ERROR_NONE 0U /* 无错误 */
#define MAX30102_ERROR_I2C 1U /* I2C 地址阶段未收到应答 */
#define MAX30102_ERROR_PART_ID 2U /* 芯片型号标识不是 0x15 */
#define MAX30102_ERROR_RESET 3U /* 芯片复位失败或超时 */
#define MAX30102_ERROR_CONFIG 4U /* 工作寄存器配置失败 */
#define MAX30102_ERROR_FIFO 5U /* FIFO 轮询或读取失败 */
/** MAX30102 测量数据及运行状态。 */
typedef struct
{
u16 heart_rate; /* 心率,单位:次/分钟(BPM) */
u16 spo2; /* 血氧饱和度,单位:百分比(%) */
u8 heart_rate_valid; /* 心率有效标志:1 有效,0 无效 */
u8 spo2_valid; /* 血氧有效标志:1 有效,0 无效 */
u8 finger_detected; /* 手指检测标志:1 已检测到,0 未检测到 */
u8 sample_ready; /* 本次更新至少读取到一个新样本时置 1 */
u8 fifo_active; /* 成功读取过 FIFO 样本后保持为 1 */
u8 progress; /* 首次测量采集进度,范围 0~100 */
u8 fifo_write_pointer; /* FIFO 写指针,用于通信状态诊断 */
u8 fifo_read_pointer; /* FIFO 读指针,用于通信状态诊断 */
u8 interrupt_status; /* 中断状态寄存器 1 的最近读取值 */
u32 red; /* 最近一次红光原始采样值,18 位有效 */
u32 ir; /* 最近一次红外光原始采样值,18 位有效 */
} MAX30102_DataTypeDef;
/**
* @brief 初始化 GPIO、软件 I2C 和 MAX30102 工作寄存器。
* @return 初始化成功返回 1,失败返回 0。
*/
u8 MAX30102_Init(void);
/**
* @brief 轮询 FIFO、读取新样本并更新心率和血氧计算结果。
* @param data 用于保存测量结果和运行状态的结构体地址。
* @return 通信正常返回 1,I2C 或 FIFO 操作失败返回 0。
*/
u8 MAX30102_Update(MAX30102_DataTypeDef *data);
/** @brief 读取最近一次驱动错误码。 */
u8 MAX30102_GetLastError(void);
/** @brief 读取初始化阶段检测到的芯片型号标识。 */
u8 MAX30102_GetPartID(void);
/* 以下底层接口可用于课堂上的寄存器读写和原始数据实验。 */
/**
* @brief 向 MAX30102 指定寄存器写入一个字节。
* @return 写入成功返回 1,通信失败返回 0。
*/
u8 MAX30102_WriteRegister(u8 reg, u8 value);
/**
* @brief 从 MAX30102 指定寄存器读取一个字节。
* @return 读取成功返回 1,通信失败返回 0。
*/
u8 MAX30102_ReadRegister(u8 reg, u8 *value);
/**
* @brief 从 FIFO 读取一组红光和红外光原始样本。
* @return 读取成功返回 1,通信失败返回 0。
*/
u8 MAX30102_ReadFIFO(u32 *red, u32 *ir);
#endif
网硕互联帮助中心





评论前必须登录!
注册