拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于MAX30102的可穿戴血氧监测:驱动、算法与跨平台移植实践

简介面向单片机与嵌入式开发者的MAX30102血氧浓度模块实战资源包适用于物联网项目快速验证与传感器驱动移植。压缩包包含14个文件涵盖Arduino源码、C驱动与头文件、原理图、PDF说明文档及一键重置KEIL编译的批处理脚本体积仅1.45MB结构紧凑。代码在Arduino平台实测通过接线引脚均在源码中标注可对照原理图快速复现相同系列芯片理论上可直接沿用其他型号需自行调整接口。包内目录层级清晰jpg与pdf配合可快速核对硬件接法db缓存文件不影响使用。已有306人学习下载适合需要绕过底层调试、直接获取可用驱动与硬件连接参考的开发者尤其适用于课程设计、毕业设计及竞赛前期的功能验证阶段。1. 先从一块反射式血氧传感器说起MAX30102 这颗传感器在可穿戴设备里几乎是标配手环、血氧仪、睡眠监测贴片算法好一点的方案里都能看到它的身影。它把红光 LED、红外 LED 和光电二极管封装在一个 5.6mm × 3.3mm 的模块里通过 I2C 直接输出 PPG 原始采样数据不需要外接运放和 ADC这对单片机开发来说省了很大一块模拟前端设计。但真正上手之后你会发现通信和寄存器配置只是第一步更麻烦的是信号质量——同一个传感器有人能稳定测出波形有人读回来的全是噪点。这套 RD117 例程的价值就在于它给出了可以直接烧录的 Arduino 代码、原理图和相关算法实现把传感器驱动、数据滤波和血氧计算串成了一条完整链路适合做嵌入式课程设计、传感器入门和可穿戴原型验证的人直接参考。2. MAX30102 寄存器级 I2C 通信与采样配置2.1 反射式 PPG 信号与寄存器映射MAX30102 的工作原理是反射式光电容积脉搏波描记法PPG红光660nm和红外光940nm交替打到皮肤表面光电二极管接收经过组织吸收、散射后返回的光强。动脉血搏动时血管容积变化导致光吸收量周期性波动这就是 AC 分量而皮肤、骨骼、静脉血等静态组织产生相对稳定的 DC 分量。含氧血红蛋白对红光的吸收率显著低于还原血红蛋白对红外光的吸收率则刚好相反通过两个波长的 AC/DC 比值就能估算血氧饱和度。寄存器层面核心配置集中在三个地址寄存器地址关键位段作用MODE_CONFIG0x09MODE[2:0] 0x03切换 Heart Rate 模式、SpO2 模式或多 LED 模式SPO2_CONFIG0x0AADC_RGE、SPO2_SMPRATE、LED_PW设置 ADC 量程、采样率和 LED 脉冲宽度FIFO_CONFIG0x08SMP_AVE、FIFO_ROLLOVER_EN、A_FULL控制 FIFO 平均滤波与中断阈值这套 Arduino 代码里通常把 MODE_CONFIG 写为 0x03 进入 SpO2 模式此时红/红外两个通道交替采样每个采样点由 6 字节组成分别对应红光 3 字节和红外 3 字节从 FIFO_DATA0x07连续读取即可。FIFO 深度为 32 个样本意味着如果采样率超过单片机读取速度数据会覆盖写满并产生 INT 中断。工程中常见的做法是用一个 GPIO 接模块的 INT 引脚FIFO 接近满时触发中断再批量读避免轮询占用主循环。2.2 软件 I2C 与硬件 I2C 的取舍Arduino Uno 的硬件 I2C 默认在 A4SDA和 A5SCL上但很多实际项目里这两个引脚会被 OLED、温湿度传感器或其他模块占据而且 Uno 的硬件 I2C 是纯轮询实现遇到 SCL 被拉低时容易卡死。这套例程里带了一个 SoftI2CMaster.h说明作者考虑到引脚复用和移植性用普通 IO 模拟了 I2C 时序。软件 I2C 的核心是精准控制 START、STOP 和数据位翻转#define SCL_PIN A5 #define SDA_PIN A4 void i2c_start() { digitalWrite(SDA_PIN, HIGH); digitalWrite(SCL_PIN, HIGH); delayMicroseconds(5); digitalWrite(SDA_PIN, LOW); // SCL 高电平期间 SDA 拉低 START delayMicroseconds(5); digitalWrite(SCL_PIN, LOW); } void i2c_stop() { digitalWrite(SDA_PIN, LOW); digitalWrite(SCL_PIN, HIGH); delayMicroseconds(5); digitalWrite(SDA_PIN, HIGH); // SCL 高电平期间 SDA 拉高 STOP delayMicroseconds(5); } uint8_t i2c_write_byte(uint8_t data) { for (int i 7; i 0; i--) { digitalWrite(SDA_PIN, (data i) 0x01); digitalWrite(SCL_PIN, HIGH); delayMicroseconds(3); digitalWrite(SCL_PIN, LOW); delayMicroseconds(3); } pinMode(SDA_PIN, INPUT); // 释放 SDA 读取 ACK digitalWrite(SCL_PIN, HIGH); delayMicroseconds(3); uint8_t ack digitalRead(SDA_PIN); digitalWrite(SCL_PIN, LOW); pinMode(SDA_PIN, OUTPUT); return ack 0 ? 0 : 1; // 返回 0 表示从机应答正常 }这段代码直接用digitalWrite翻转引脚逻辑很直白SCL 高电平时 SDA 的电平变化代表 START/STOP 条件数据位则在 SCL 低电平时准备好、高电平时被从机采样。写一个字节后要释放 SDA 读 ACK 应答位否则后续传输会失败。MAX30102 的 I2C 从机地址是 0x577 位地址写操作时地址字节为 0xAE读操作为 0xAF。如果你在 I2C 扫描程序里找不到这个芯片先检查 SDA 线上是不是没有接上拉电阻或者把模块的 VIN 接到了 Arduino 的 5V 而不是 3.3V——MAX30102 的 I/O 电平上限是 1.8V 到 3.6V直接接 5V 上拉会进入不稳定状态。3. RD117 例程拆解驱动层、算法层与主循环3.1 驱动层寄存器读写与 FIFO 批量读取压缩包里 max30102.cpp 负责底层寄存器操作核心接口是单字节读、单字节写和 FIFO 批量读。单字节写的过程是先发送从机地址加写标志再发送寄存器地址最后发送数据字节单字节读则多一个重复 START 条件把方向位切换为读。bool max30102_write_reg(uint8_t reg, uint8_t value) { i2c_start(); if (i2c_write_byte(0xAE)) return false; // 从机地址 写标志 if (i2c_write_byte(reg)) return false; // 寄存器地址 if (i2c_write_byte(value)) return false; // 待写入数据 i2c_stop(); return true; } bool max30102_read_regs(uint8_t reg, uint8_t *buf, uint8_t len) { i2c_start(); if (i2c_write_byte(0xAE)) return false; if (i2c_write_byte(reg)) return false; i2c_start(); // 重复 START切换读方向 if (i2c_write_byte(0xAF)) return false; for (uint8_t i 0; i len; i) { buf[i] i2c_read_byte(i len - 1); // 最后一个字节回 NACK } i2c_stop(); return true; }i2c_read_byte的最后一个参数很关键读倒数第二个字节时回 ACK告诉从机继续发送读到最后一个字节时回 NACK通知从机传输结束然后主机发送 STOP。MAX30102 的 FIFO 指针会自动递增连续读 6 字节拿到的就是红光和红外各一个采样点。如果读出来的数据全是 0x00优先检查 LED 是否点亮如果全是 0xFF大概率是寄存器没有写入成功或者芯片进入了 shutdown 模式需要把 MODE_CONFIG 的 bit7 清零。3.2 算法层AC/DC 特征提取与 SpO2 计算algorithm.cpp 这块是血氧算法的核心。FIFO 里读出来的是未经处理的 ADC 原始值数量级取决于 ADC 量程和 LED 电流——4096nA 量程下红光通道典型幅值在 10000 到 60000 之间红外通道略高。原始信号里既有接近直流的组织吸收分量也有叠加在上面的微小脉搏波幅度通常只占 DC 的 1% 到 3%。一步到位算血氧必然失败必须先把 AC 分量从 DC 里分离出来。const int kSampleCount 150; float R_ratio 0.0f; { // 对红光和红外两路分别计算 AC 峰峰值与 DC 均值 float red_max -1e9, red_min 1e9; float ir_max -1e9, ir_min 1e9; double red_dc 0, ir_dc 0; for (int i 0; i kSampleCount; i) { red_dc red_wave[i]; ir_dc ir_wave[i]; red_max max(red_max, red_wave[i]); red_min min(red_min, red_wave[i]); ir_max max(ir_max, ir_wave[i]); ir_min min(ir_min, ir_wave[i]); } red_dc / kSampleCount; ir_dc / kSampleCount; float red_ac red_max - red_min; // 峰峰值近似 AC 幅值 float ir_ac ir_max - ir_min; R_ratio (red_ac / red_dc) / (ir_ac / ir_dc); } float spo2; if (R_ratio 0.4f) { spo2 100.0f; } else if (R_ratio 3.0f) { spo2 80.0f; } else { spo2 110.0f - 25.0f * R_ratio; // 线性近似仅作参考 }这里用固定窗口内的峰峰值模拟 AC 幅值窗口长度按采样率换算100SPS 采样率下 150 个点对应 1.5 秒能覆盖两到三个完整脉搏周期。R 比值是血氧计算的关键中间量理论上与 SpO2 呈反比关系含氧量高时红光吸收量变化小R 值偏小SpO2 接近 100%。线性公式110 - 25*R只是经验近似在 70% 以下误差明显MAXIM 官方算法用的是查表加滑动平均这套例程里的 algorithm.h 就是按查表思路实现的。实际使用时一般会叠加一个低通滤波把工频干扰和高频噪声去掉再做峰值检测确认脉搏频率有效否则手指轻微抖动就会让 R 值产生剧烈波动。3.3 主循环与串口输出RD117_ARDUINO.ino 的主循环不复杂每次读 6 字节 FIFO 数据拆出红/红外 ADC 值更新本地环形缓冲区缓冲区攒够窗口长度后调用算法函数把 SpO2 和心率通过串口打印出来。void loop() { if (max30102_read_regs(0x07, fifo_buf, 6)) { uint32_t red_raw (fifo_buf[0] 16) | (fifo_buf[1] 8) | fifo_buf[2]; uint32_t ir_raw (fifo_buf[3] 16) | (fifo_buf[4] 8) | fifo_buf[5]; red_buffer[write_idx] red_raw; ir_buffer[write_idx] ir_raw; write_idx (write_idx 1) % kSampleCount; if (write_idx 0) { // 缓冲区写满一轮做一次血氧和心率计算 float ratio compute_ratio(red_buffer, ir_buffer, kSampleCount); int hr compute_heart_rate(ir_buffer, kSampleCount); Serial.print(SpO2); Serial.print(spo2_from_ratio(ratio)); Serial.print( HR); Serial.println(hr); } } }三个字节拼成 18 位无符号数对齐方式是 MSB 在前所以直接按左移合并。心率是从红外通道算的因为红外通道受静脉血影响小、信噪比更高这是可穿戴设备里的常规做法。需要注意的是这套代码默认以 100SPS 运行如果改了采样率kSampleCount需要同步调整否则窗口时长和缓冲长度不匹配算法输出的 SpO2 会忽高忽低。4. 接线、参数标定与血氧测量的常见坑4.1 接线与电平匹配原理图文件 MAX30100-SCH.jpg 和 MAX30102.pdf 里能直接查到模块管脚定义。常见的 MAX30102 模块引出 6 个引脚VIN、GND、SCL、SDA、INT、PG电源地。与 Arduino Uno 的接法如下模块引脚Arduino Uno硬件 I2CArduino UnoSoft I2C说明VIN3.3V3.3V模块电源禁止接 5VGNDGNDGND共地SCLA5任意数字引脚默认 A5需 4.7kΩ 上拉到 VINSDAA4任意数字引脚默认 A4需 4.7kΩ 上拉到 VININTD2可选D2可选FIFO 溢出中断输出低电平有效多数现成模块已经把上拉电阻做进去了但如果你用的是 MAX30102 裸片转接板务必自己在 SCL/SDA 上各加一个 4.7kΩ 上拉电阻到 3.3V。MAX30102 的 I2C 引脚是 1.8V 逻辑电平直接接 Uno 的 5V 引脚有损坏风险。怎么看模块有没有做电平转换看模块背面有没有 4 个以上的电阻排和一个电平转换芯片如果没有稳妥做法是在 SCL/SDA 上串联 1kΩ 限流电阻或者在中间加一颗 TXS0102 电平转换芯片。4.2 LED 电流与采样参数的配合LED 电流直接决定 PPG 信号的信噪比。电流太小光电二极管输出微弱AC 分量被噪声淹没电流太大接收端饱和ADC 读数顶到 0x3FFFF波形直接削顶。这套代码里 LED1_PA 和 LED2_PA 通常从 0x24对应 7.2mA起步再根据实际波形微调。#define LED_RED_PA 0x0C // 红光 LED 电流寄存器 #define LED_IR_PA 0x0D // 红外 LED 电流寄存器 void set_led_current(uint8_t red_ma, uint8_t ir_ma) { // 电流档位0~50.6mA步进 0.2mA max30102_write_reg(LED_RED_PA, (uint8_t)(red_ma / 0.2f)); max30102_write_reg(LED_IR_PA, (uint8_t)(ir_ma / 0.2f)); }实测经验肤色偏深、佩戴偏紧或传感器表面有脏污时需要把 LED 电流适当调高保证红光通道 ADC 读数在 30000 到 50000 之间红外通道在 40000 到 60000 之间。这个区间既不会饱和又能让 AC 分量达到几千的幅值便于后续算法提取。还要注意 SPO2_CONFIG 里的采样率和脉宽联动采样率选 100SPS 以下时脉宽用 411μs 可以获得更好的积分信噪比如果采样率跳到 400SPS 以上脉宽要相应缩短否则两个 LED 的采样时间会互相挤占。4.3 波形异常时的排查路径信号质量最好的验证方法不是读 SpO2 数值而是直接看原始波形。手指轻轻按住传感器让传感器正对指腹串口输出的红外 ADC 值应该呈现明显的波浪形起伏频率约每秒 1 到 1.5 个波峰。如果波形是平直线但有数值说明 LED 没照到血管换个手指或调整按压位置如果波形毛刺多、没有规律先检查是不是手指在抖再看采样率是否被环境光干扰。MAX30102 对红外光敏感度很高阳光直射或白炽灯近距离照射都会引入 100Hz 工频波动测试时最好用手遮住传感器上方。另一个高频问题是一开始 SpO2 显示 100%但把手指拿开后仍然显示 100%——这说明算法把噪声当成脉搏波了需要增加 AC 幅值阈值判断AC 低于某个最小幅值时直接判定无有效信号输出 0 而不是推算值。5. 用串口绘图验证波形、把例程移植到其他单片机5.1 串口绘图仪与 Wokwi 仿真的组合验证Arduino IDE 自带的串口绘图仪Serial Plotter是验证信号质量最快的工具不需要额外安装插件。把主循环输出改成两行文本第一行是红光原始值第二行是红外原始值Serial.print(red_raw); Serial.print(,); Serial.println(ir_raw);打开 Tools → Serial Plotter波特率选 115200能看到两条几乎重叠但幅值不同的曲线。红光通道的振幅通常只有红外通道的 60% 左右两个波形应该同步起伏、无明显相位差。如果两路波形反向或不同步检查是不是寄存器模式误设成了多 LED 模式而不是 SpO2 模式。没有硬件在手的时候Wokwi 这类在线仿真平台可以直接跑 Arduino 代码配合虚拟串口和逻辑分析仪完成语法和协议层的调试先把 I2C 扫描、寄存器读写这些基础通路验证干净再上真机只排查传感器物理信号效率会高很多。5.2 移植到 STM32 的关键改动点从 Arduino 移植到 STM32比如常见的 STM32F103C8T6并不复杂核心是把 SoftI2C 换成硬件 I2C或者保留软件 I2C 思路改一下引脚宏。压缩包里还有一个重置KEIL编译.bat这是清理 Keil 工程中间文件的批处理说明这套代码的算法部分从一开始就是按跨平台设计的。移植时三个文件的定位很清晰max30102.cpp 里的寄存器读写函数换成 HAL 库 I2C 接口algorithm.cpp 保持纯 C 实现不动主循环里把Serial.print换成printf重定向到串口即可。#include stm32f1xx_hal.h extern I2C_HandleTypeDef hi2c1; bool max30102_read_regs(uint8_t reg, uint8_t *buf, uint8_t len) { // HAL 库的寄存器读先写寄存器地址再带重复起始位读数据 HAL_I2C_Master_Transmit(hi2c1, 0xAE, reg, 1, 100); HAL_I2C_Master_Receive(hi2c1, 0xAF, buf, len, 100); return true; }STM32 的硬件 I2C 上拉要求更严格如果 PCB 上没有集成上拉电阻必须外接 4.7kΩ 到 3.3V否则 HAL 库的HAL_I2C_Master_Transmit会一直卡在总线忙状态。另外 STM32 的引脚速率要设为开漏输出并开启内部上拉GPIO 推挽模式直接连 I2C 总线会导致时序异常。算法部分移植时注意algorithm.cpp里的缓冲数组类型STM32F103 是 32 位内核int长度与 Arduino 一致不需要改数据类型但如果换到 8 位单片机ADC 原始值 18 位必须用uint32_t显式声明否则拼接时高位会被截断。这套例程的逻辑链路寄存器配置 → FIFO 读取 → AC/DC 计算 → SpO2 映射在各个平台上是完全一致的调通一次之后换芯片只需要替换最底层那十几个 I2C 函数。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门