云计算百科
云计算领域专业知识百科平台

STM32F103驱动MAX30102血氧心率检测完整教程(含OLED显示、软件I2C源码)

前言

本篇基于 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

一、整体实现流程

  • 心脏搏动造成手指末梢动脉血液容积周期性变化;
  • MAX30102 内部红光(660nm)、红外光(880nm)LED 交替照射手指;
  • 光电二极管接收组织反射光,转化为微弱电流;
  • 片内模拟前端抑制环境光干扰,18 位 ADC 将光信号转为数字量;
  • 红光、红外成对样本存入芯片内部 32 组 FIFO 缓存;
  • STM32 通过软件 I2C 轮询读取 FIFO 原始数据;
  • 软件去除波形直流基线、识别脉搏峰值,通过双光比值计算血氧、峰间隔计算心率;
  • 实时在 OLED 屏幕打印红外原始采样值、心率、血氧以及设备工作状态。

  • 二、硬件电路接线说明

    2.1 引脚对应表

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

    OLED 显示屏接线
    OLED 引脚STM32F103 引脚功能说明
    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)

    优势:

  • 刷新 OLED 屏幕时不会干扰 MAX30102 传感器总线,避免采样丢数;
  • 单设备通信故障不会牵连另一路总线,调试故障更容易定位;
  • 教学场景可以单独断开一路设备,分别测试传感器与屏幕功能。
  • 2.3 供电与上拉电阻注意事项

  • MAX30102 裸片内部数字电路工作 1.8V,市面上成品五针模块已经集成稳压电路,直接 3.3V 供电即可;
  • 若购买无板载上拉电阻的裸模块,SCL、SDA 引脚需要外接 4.7kΩ 电阻上拉至 3.3V;
  • 程序开启 STM32 对应 GPIO 内部上拉,进一步提升实验室面包板接线稳定性;
  • 所有外设必须与主控共地,否则会出现 I2C 读写出错、数值跳变严重问题;
  • 禁止给模块接入 5V 电压,会直接烧毁传感器芯片。
  • 三、MAX30102 光学检测核心原理

    本传感器采用反射式 PPG 光电容积脉搏波检测法,依靠两种波长光线区分心率与血氧。

    3.1 心率检测原理

    心脏收缩时手指动脉血流量变大,舒张时血流量减小,血液多少会改变光线吸收、反射强度,光电二极管输出波形会带有和心跳同步的周期性波动。 波形分为两部分:

  • DC 直流分量:皮肤、肌肉、静脉血形成的基础基线,变化速度缓慢;
  • AC 交流分量:心跳带来的血流波动,是计算心率的有效信号。
  • 软件先滤除缓慢变化的 DC 基线,再识别波形峰值。 本工程采样率固定 100Hz,心率计算公式: HR = 6000 / N N 为相邻脉搏峰之间的平均采样点数,举例:峰间隔 80 个采样点,心率 = 6000/80=75 次 / 分钟。

    3.2 血氧检测原理

    含氧血红蛋白、脱氧血红蛋白对红光、红外光吸收系数差异巨大,通过两路光波形比值估算血氧饱和度。

  • 分别提取红光、红外波形的 AC 交流分量、DC 直流基线;
  • 计算归一化脉动比值 R = (红光 AC / 红光 DC) ÷ (红外 AC / 红外 DC);
  • 采用教学适用经验公式估算血氧:SpO2 ≈ -45.06×R² + 30.354×R + 94.845;
  • STM32 采用定点整数运算实现公式,规避 Cortex-M3 内核浮点库占用资源过大问题。
  • 四、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 就绪中断标志: 仅对比指针会出现 “缓存已满时读写指针重合,误判为空缓存” 问题;结合溢出计数、就绪标志双重判断,消除数据读取歧义。

    五、数据采样与算法完整处理逻辑

  • 轮询读取状态寄存器、FIFO 读写指针,判断是否存在未读取样本;
  • 存在新数据则连续读取一组红光、红外原始采样值;
  • 判断红外数值是否达到手指检测阈值,数值过低判定无手指接触;
  • 有效采样存入 400 点滑动波形窗口(对应 4 秒波形数据);
  • 采集满 400 点后执行算法:去除基线、峰值检测、计算心率血氧;
  • 计算完成后保留最近 300 点波形,后续每新增 100 点(1 秒)重新刷新一次测量结果。
  • 工程关键固定参数

    参数数值功能
    采样率 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

    字段释义:

  • IR:最新一组红外光 18 位原始采样数值;
  • HR:计算得到实时心率;
  • SpO2:估算血氧饱和度;
  • MEASUREMENT OK:波形质量达标,数值有效。
  • 6.2 调试信息界面(无有效采样时)

    W:00 R:00 S:000

    • W:FIFO 写指针
    • R:FIFO 读指针
    • S:中断状态寄存器十进制值,S=64 代表新样本就绪

    6.3 各类异常提示

  • WAIT FIFO DATA:传感器暂无采样数据;
  • FINGER / IR LOW:采样存在数据,但手指未放好、反射光不足;
  • SAMPLING xxx%:正在采集 400 点基础波形,显示采集进度;
  • KEEP FINGER STILL:波形噪声过大、运动干扰,结果无效。
  • 七、实物演示效果章节

    7.1 实物硬件清单

  • STM32F103C8T6 最小系统板
  • MAX30102 血氧心率传感器模块
  • 0.96 寸 I2C OLED 显示屏
  • 杜邦线若干、面包板
  • 3.3V 直流供电电源
  • 7.2 实物接线操作步骤

  • 将 MAX30102 VCC、GND 接到最小系统 3.3V 与 GND,SCL 接 PB6、SDA 接 PB7;
  • OLED 屏幕 VCC、GND 共地供电,SCL 接 PB8、SDA 接 PB9;
  • 整体接线完成后检查无短路、虚接,接入 USB 供电;
  • 上电屏幕先打印 FIFO 指针调试信息,等待传感器初始化完成。
  • 7.3 正常测量演示

  • 将指腹完整贴紧 MAX30102 光学窗口,完全遮挡发光管;
  • 手指轻压传感器,不要用力挤压阻断血液循环,手部保持静止放置桌面;
  • 屏幕显示 SAMPLING 进度,等待进度 100%(约 4 秒);
  • 进度完成后自动输出心率、血氧,稳定静置可观察数值小幅波动;
  • 移开手指,屏幕立刻切换 FINGER / IR LOW 提示,IR 数值大幅下降。

  • 八、源代码模块(可直接复制插入区域)

    本节预留完整源码插入位置,工程分为 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

    8.2 各文件核心函数功能说明

  • MAX30102_Init ():初始化 GPIO、读取芯片 ID 校验、配置全部传感器寄存器;
  • MAX30102_ReadFIFO ():I2C 读取一组 6 字节红光红外原始采样;
  • MAX30102_Update ():轮询 FIFO 缓存,一次性读取所有未处理样本;
  • MAX30102_AddSample ():手指阈值判断、维护 400 点滑动波形窗口;
  • MAX30102_Calculate ():波形滤波、峰值识别、心率血氧数值计算;
  • MAX30102_ShowData ():调用 OLED 驱动,打印全部状态与测量结果。
  • 8.3 源码使用说明

  • 源码基于标准库 STM32F10x 开发,不依赖 HAL 库,适配初学课程设计;
  • 所有宏定义集中在头文件,可直接修改 LED 电流、手指阈值、采样窗口大小;
  • 代码内部注释完整,每一步寄存器操作、算法处理均标注说明;
  • 下载程序后无需额外修改参数即可直接上电测试,出现数值饱和、灵敏度不足再微调 LED 电流寄存器值。
  • 九、测量误差与环境影响优化方案

    9.1 影响测量精度的常见因素

  • 肢体晃动:运动伪差幅值远大于脉搏信号,是数值跳变最主要原因;
  • 漏光:手指未完全盖住光学窗口,环境光混入光电二极管;
  • 按压力度:过轻信号不稳,过重压迫血管削弱脉搏波动;
  • 末梢循环:低温手指血流弱,交流脉搏分量极小;
  • 外部光路:深色指甲油、厚皮肤、传感器与皮肤间距都会改变光线反射量;
  • 硬件通信:I2C 虚接、缺少上拉电阻、电源纹波造成采样数据错乱。
  • 9.2 优化调试方法

  • 出现 IR 数值饱和(接近 262143):降低 LED1_PA、LED2_PA 寄存器电流值;
  • 经常提示 IR LOW、无有效采样:适当提高 LED 驱动电流,或降低手指阈值宏;
  • 数值频繁跳变:增大最小峰间距参数,延长波形采样窗口长度;
  • I2C 读取不稳定:增加 4.7k 外部上拉电阻,重新检查共地接线。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » STM32F103驱动MAX30102血氧心率检测完整教程(含OLED显示、软件I2C源码)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!