STM32F103驱动MAX30102血氧心率检测完整教程(含OLED显示、软件I2C源码)
前言
本篇基于 STM32F103C8T6 主控搭配 MAX30102 光电血氧心率传感器、0.96 寸 I2C OLED 显示屏完成整套脉搏血氧采集工程,全程使用软件 I2C 通信,分开两路 I2C 总线分别驱动传感器与屏幕,规避总线冲突问题。文章完整拆解硬件接线、光学检测原理、寄存器配置、FIFO 数据读取、心率血氧算法、实物调试、完整源码模块,适合嵌入式初学者、课程设计、电子竞赛直接参考。
重要说明:本方案仅作教学演示使用,未经过医疗级临床标定,测量数值仅用于学习信号处理逻辑,不可作为医疗诊断依据。
一、整体实现流程
- 心脏搏动造成手指末梢动脉血液容积周期性变化;
- 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)