
简介本资源是一套完整的STM32嵌入式健康监测项目源码面向嵌入式初学者与课程设计实践者解决心率血氧实时采集、本地OLED可视化显示及串口数据上位机同步传输的核心开发需求。项目基于STM32F1系列单片机HAL库开发集成MAX30102传感器I²C驱动、SSD1306 OLED显示控制、UART串口通信模块并包含完整时序管理与数据解析逻辑适用于智能手环、便携式体征监测等教学与原型开发场景。压缩包共249个文件含39个C源文件如OLED.c、stm32f10x_i2c.c、39个头文件.h、50个编译中间文件.o、48个依赖描述.crf及Keil工程配置.uvprojx/.uvoptx、调试脚本.bat等结构清晰便于理解编译流程与模块划分。资源包大小为6.91MB已有2497人学习下载提供可直接编译运行的工程框架、关键外设初始化范例、传感器原始数据到生理参数的转换逻辑及串口协议封装显著降低I²C通信调试与多任务协同实现门槛。 从收到 MAX30102 模块到把心率、血氧数值稳定地显示在 OLED 上并发送到串口调试助手我大概折腾了两个通宵。第一晚全耗在“为什么读出来的数据全是 0”和“为什么数值在 60 到 180 之间疯狂跳”这两个问题上。第二晚把原始波形从串口拉出来看之后才明白问题不在传感器而在我的寄存器配置和数据处理方式上。这篇东西就是把这两晚的完整过程记录下来包括硬件怎么接、寄存器怎么配、FIFO 数据怎么读、心率血氧算法怎么在单片机上落地以及最终如何把数据通过串口发到上位机。项目本身不复杂但里面有几个坑确实会让新手卡很久适合正在做同类项目的同学参考。1. 硬件选型和接线为什么这套组合是入门心率血氧的最稳选项1.1 四颗料的选择逻辑先聊一下这套系统里每一颗芯片/模块的选择理由因为很多同学不是不会写代码而是被硬件选型坑了。主控是 STM32F103C8T6也就是大家常说的“蓝丸”核心板。它便宜、资料多、引脚够用而且 3.3V 供电和 MAX30102 模块完全兼容。I2C、USART、OLED 驱动这些外设全部由硬件支持不需要额外模拟开发门槛低。如果你手头有 STM32F407、G031 或者其他系列也能照搬只要注意引脚复用和时钟配置的不同就行。MAX30102 这颗传感器是 Maxim 推出的集成式脉搏血氧传感器内部集成了红光 LED660nm、红外 LED880nm、光电检测二极管、ADC、环境光抑制、LED 驱动和数字接口。它最大的优势就是把模拟前端全部集成在模块内部外部只需要接 I2C 两根线就能拿到数字化的光电容积脉搏波PPG数据不需要自己搭跨阻放大器、滤波器和 ADC 采样电路。和上一代 MAX30100 相比MAX30102 的功耗更低、信噪比更好而且红光和红外光的通道一致性更好这也是我推荐用 02 而不是 00 的原因。OLED 用的是 0.96 寸 I2C 接口版本主控芯片是 SSD1306分辨率 128x64。选 I2C 版本而不是 SPI 版本原因是省引脚——整块屏幕只需要 SCL、SDA、VCC、GND 四根线和 MAX30102 共用一条 I2C 总线即可连线极简。串口部分用板载 CH340 的 USB 转 TTL 模块或者直接用 STM32 核心板上的 USB 转串口芯片都可以。我用的是独立的 CH340 模块原因是调试时不想和烧录口抢资源而且独立模块可以随时换到其他板子上用。1.2 接线表与电平匹配细节整套系统的接线非常简单我直接列个表STM32F103C8T6MAX30102 模块0.96 寸 OLEDCH340 串口模块3.3VVINVCCVCC可选GNDGNDGNDGNDPB6I2C1_SCLSCLSCL-PB7I2C1_SDASDASDA-PA9USART1_TX--RXDPA10USART1_RX--TXD有几个细节要特别注意第一所有模块必须统一由 3.3V 供电。MAX30102 的 VIN 虽然标称可以到 5V但模块上的电平转换电路通常只是把 5V 降下来给内部用I2C 引脚的电平还是 3.3V 逻辑。如果你用 5V 给传感器供电同时又用 3.3V 的 STM32 去拉 I2C虽然大多数时候能工作但在极端情况下可能会因为电平不匹配导致通信异常。OLED 模块也是同样道理虽然 SSD1306 支持 3.3V~5V但 I2C 引脚的电平逻辑建议保持和 MCU 一致。第二I2C 上拉电阻的问题。MAX30102 模块和 OLED 模块上通常都已经焊好了 4.7kΩ 或 10kΩ 的上拉电阻到 VCC。如果你用的是淘宝上最常见的成品模块那基本不用额外接上拉。但如果你用的是 MAX30102 裸片或者自己画的板子那必须在 SCL 和 SDA 上各接一个 4.7kΩ 上拉电阻到 3.3V否则 I2C 通信会时好时坏逻辑分析仪上看起来波形就像一团毛线。第三共地是必须的。CH340 模块和 STM32 之间一定要共地否则串口数据会出现乱码或者完全收不到。这个坑我踩过一次当时把两个模块分别用两个 USB 口供电结果 TX 线上量出来有 1V 左右的压差串口助手收到的全是乱码。1.3 为什么不用 MAX30100 或者 PulseSensor很多教程还在用 MAX30100这颗芯片有点老I2C 地址和 MAX30102 不同0x57 vs 0xAE而且内部 FIFO 只有 16 个样本红光和红外光的串扰也比较明显。相比之下 MAX30102 的 FIFO 深度同样是 16 个样本但 ADC 分辨率更高18 位动态范围更好对于手指放置位置的宽容度也更高。PulseSensor 是另一类方案它输出模拟信号需要 MCU 自带的 ADC 去采样。好处是算法可以自己在时域上随便玩坏处是电路设计和信号调理全靠自己没有数字滤波环境光干扰非常严重。对于“拿到就能跑、跑起来还能看”的入门项目MAX30102 的集成度优势太大了。2. MAX30102 驱动核心寄存器配置与 FIFO 数据读取的完整链路2.1 初始化时序厂家推荐的寄存器配置顺序MAX30102 的寄存器配置是整个项目里最容易出错的地方但也是最套路化的地方。它不像一些 MCU 外设那样需要复杂的时钟树配置只要按照数据手册的推荐顺序写寄存器即可。我整理了一份标准的初始化函数流程代码是 HAL 库风格的void MAX30102_Init(void) { // 1. 复位传感器 MAX30102_WriteReg(REG_MODE_CONFIG, 0x40); // 0x40 写入 RESET 位 HAL_Delay(100); // 2. 配置 FIFO MAX30102_WriteReg(REG_FIFO_CONFIG, 0x4F); // 采样平均 4 次FIFO 满时不再写入 MAX30102_WriteReg(REG_FIFO_WR_PTR, 0x00); MAX30102_WriteReg(REG_OVF_COUNTER, 0x00); MAX30102_WriteReg(REG_FIFO_RD_PTR, 0x00); // 3. 配置 SpO2 模式 MAX30102_WriteReg(REG_SPO2_CONFIG, 0x47); // ADC 范围 4096nA采样率 400HzLED 脉宽 411us // 4. 配置 LED 电流 MAX30102_WriteReg(REG_LED1_PA, 0x24); // 红光 LED 电流约 6.4mA MAX30102_WriteReg(REG_LED2_PA, 0x24); // 红外 LED 电流约 6.4mA // 5. 使能中断可选轮询时可不用 MAX30102_WriteReg(REG_INTR_ENABLE_1, 0xC0); // 使能 FIFO 几乎满中断 MAX30102_WriteReg(REG_INTR_ENABLE_2, 0x00); // 6. 设置为心率和血氧模式连续采样 MAX30102_WriteReg(REG_MODE_CONFIG, 0x03); // SpO2 模式红红外同时开 }这里要解释几个关键寄存器为什么这么配REG_MODE_CONFIG0x09复位后芯片默认处于关机模式SHDN1所以第一步要么先清掉 SHDN 位要么直接写 0x40 复位再重新配置。数据手册要求复位后等待至少 100ms 再操作寄存器否则可能不稳定。最后配置成 0x03即 SpO2 模式这个模式下红光和红外 LED 会交替点亮ADC 交替采样。REG_FIFO_CONFIG0x08高三位设置多次采样平均值SAMPLE_AVG0x4F 表示 4 次平均即 000 是 1 次001 是 2 次010 是 4 次以此类推。4 次平均可以在几乎不损失实时性的情况下有效降低噪声。低两位的 FIFO_ROLLOVER_EN 设为 1表示 FIFO 满时新数据覆盖旧数据这样轮询读取时不容易漏掉最新数据。REG_SPO2_CONFIG0x0Abit[5:0] 分别是 ADC_RGE[1:0] 和 LED_PW[1:0] 以及 SPO2_SR[2:0]。0x47 拆开来看ADC_RGE01±4096nALED_PW11411μsSPO2_SR100采样率 400Hz。实际上我把采样率降到 100Hz 就够用了频率太高只会增加数据处理压力心率信号本身是 0.5~4Hz 的频带。LED 电流寄存器0x0C 和 0x0D这是最大的坑之一。电流太小信号幅度低信噪比差电流太大容易饱和ADC 输出满量程导致波形削顶。0x24 是 36mA 满量程的 25% 左右如果手指放上去之后原始波形幅度太小峰值小于 ADC 满量程的 20%可以适当增大到 0x40~0x60。但注意不要一上来就调到最大否则红光通道很容易饱和。2.2 FIFO 数据读取三字节拼一个 18 位样本MAX30102 内部有一个 16 样本深度的 FIFO每个样本包含红光和红外两个通道的数据每个通道 18 位。读 FIFO 时从寄存器 REG_FIFO_DATA0x11连续读取 6 个字节前三个字节是红光通道后三个字节是红外通道。关键点不是所有 24 位都有效每个通道的有效数据只有高 18 位。也就是说读取三个字节后需要把第一个字节左移 16 位第二个字节左移 8 位第三个字节放在最低位然后整体右移 6 位才能得到真正的 18 位无符号整数。#define MAX30102_READ_BUF_SIZE 6 void MAX30102_ReadFIFO(uint32_t *red, uint32_t *ir) { uint8_t buf[MAX30102_READ_BUF_SIZE]; MAX30102_ReadRegs(REG_FIFO_DATA, buf, 6); *red ((uint32_t)(buf[0] 0x03) 16) | ((uint32_t)buf[1] 8) | buf[2]; *ir ((uint32_t)(buf[3] 0x03) 16) | ((uint32_t)buf[4] 8) | buf[5]; }这里的 0x03 是因为 18 位有效数据的高 2 位占用第一个字节的 bit[1:0]bit[7:2] 属于上一通道的溢出位或保留位。如果不做掩码处理读到的高 6 位可能是无效的填充数据导致数值偏大。另一种更安全的方式是直接把 24 位提取出来再右移 6 位uint32_t raw ((uint32_t)buf[0] 16) | ((uint32_t)buf[1] 8) | buf[2]; uint32_t sample raw 6;两种方式结果一样但第二种不容易出错。2.3 读 FIFO 的时机轮询还是中断MAX30102 支持两种方式判断 FIFO 里有没有新数据轮询读取 FIFO 数据计数寄存器 REG_FIFO_DATA_COUNT0x06或者使用中断引脚。我用的是轮询方式在主循环里先读 REG_INTR_STATUS_10x00检查 A_FULL 位再读取数据。原因很简单MAX30102 的数据产生速率是固定的比如 100Hz单片机的空余时间足够多轮询完全不会阻塞其他任务。而且省掉中断引脚和中断服务程序逻辑更清晰。不过有一个细节FIFO 的数据计数寄存器返回的是 FIFO 中未读样本的数量不是当前写入位置。如果你很久没读 FIFO计数会累计到 16 或 32取决于配置这时候要一鼓作气把 FIFO 里的数据全部读空避免下次读到旧数据。所以我的读取逻辑是uint8_t num_avail MAX30102_ReadReg(REG_FIFO_DATA_COUNT); if (num_avail 0) { for (uint8_t i 0; i num_avail; i) { MAX30102_ReadFIFO(red, ir); // 处理数据 } }注意 REG_FIFO_DATA_COUNT 寄存器在数据手册里是 0x06但某些模块的数据手册打印错误写成了 0x05。以 Maxim 官方数据手册为准0x06 才对。2.4 模拟 I2C 和硬件 I2C 的选择STM32F103 的硬件 I2C 外设风评很差主要因为 bug 多、时序要求严格、出错后容易卡死。很多人建议直接用 GPIO 模拟 I2C我这边的做法是调试阶段用模拟 I2C稳定运行后可以切回硬件 I2C。模拟 I2C 的好处是可控性极强什么时候拉高拉低完全由代码决定即使 SCL 频率不太准确也能稳定工作而且出错后不会像硬件 I2C 那样进入 Busy 状态需要复位外设。缺点是占用 CPU 时间不过对于 100kHz 的 I2C 时钟来说开销完全可以忽略。如果你坚持用硬件 I2C有几个必须注意的点使能 I2C 外设后要等待 I2C_ISR 的 BUSY 位清零再开始通信。发送 START 条件后要确认 SB 位置位发送地址后要确认 ADDR 置位。在读取最后一个字节之前必须关闭 ACK否则会多发一个 ACK 导致传感器误判通信结束。出错后不要只是清错误标志要调 __HAL_I2C_ENABLE() 禁用再使能外设彻底复位状态机。我在项目代码里默认用的是软件模拟 I2C文件只有 100 多行稳健性比硬件 I2C 好太多。3. 从原始光电容积波到心率血氧算法的工程化落地3.1 看懂 PPG 波形原始数据长什么样把 MAX30102 的原始数据通过串口发到电脑上用串口调试助手的波形显示功能或者导入到 Excel 里画出来你会看到两条交替变化的曲线这就是光电容积脉搏波PPG。红光通道Red和红外通道IR的形状非常相似但幅度不同。PPG 波形有两个关键特征直流分量DC数值相对稳定的基底部分主要反映了皮肤、骨骼、肌肉组织对光的恒定吸收以及静脉血的吸收。DC 值通常占据原始信号的 80%~90% 以上。交流分量AC叠加在 DC 之上、随心跳周期性波动的分量主要来自动脉血随心脏搏动而产生的血管容积变化。AC 分量的幅度通常只有 DC 的 1%~5%。心率的检测就是基于 AC 分量的周期性峰值。血氧饱和度则是基于红光和红外两路 AC/DC 比值的对比。3.2 心率检测差分阈值法最省事在单片机上跑心率检测不需要上什么深度学习或者小波变换最实用的方法还是差分阈值法。核心思路是找到 AC 信号的峰值计算相邻两个峰值之间的时间间隔即 RR 间隔然后换算成每分钟心跳次数。我实现的步骤是这样的先做一阶差分检测波形的上升沿和下降沿int32_t diff sample - prev_sample;然后设定一个自适应阈值。阈值不能是固定的因为不同人的手指透光性、肤色、血管粗细都不同AC 幅度差异很大。我用的是“滑动最大值的一定比例”作为阈值static int32_t max_val 0; static int32_t min_val 0; if (sample max_val) max_val sample; if (sample min_val) min_val sample; int32_t threshold min_val (max_val - min_val) * 3 / 5;当一个样本从低于阈值变成高于阈值并且一段时间内没有触发过新的峰值就认为检测到了一个心跳峰。关键点必须加一个“最短间隔”限制。如果检测到两个峰值之间的间隔小于 300ms对应 200bpm说明是噪声误触发直接丢弃。这个 300ms 的窗口就是不应期和心电检测里的算法思路一样。计算心率的方式有两种一是用最近 N 个 RR 间隔的平均值换算二是用滑动窗口内峰值个数的倒数乘 60。我实测下来平均值法更平滑实时性略差计数法响应快但会出现短时抖动。我的做法是记录最近 5 个 RR 间隔取中位数然后用 60000 / median_interval_ms 得到瞬时心率。3.3 血氧饱和度R 值查表还是公式拟合血氧饱和度 SpO2 的计算比心率稍微复杂一点但工程上仍然有非常成熟的简化方案。核心是计算调制比 RR (AC_red / DC_red) / (AC_ir / DC_ir)然后通过一个经验公式或者查找表把 R 值映射到 SpO2 百分比。MAX 官方提供了一个参考公式SpO2 110 - 25 × R这个公式在 R 值处于 0.4~3.0 的范围内足够用。但如果你想要更好的精度可以在 PC 上用标准血氧仪采集一批数据拟合出一条二次或三次曲线。我在代码里用的是查表法const uint8_t spo2_lookup[] { 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 86, 84, 82, 80, 78, 76, 74, 72, 70, 68, 66, 64, 62, 60, 58, 56, 54 }; uint8_t SpO2_Calc(uint16_t ac_red, uint16_t dc_red, uint16_t ac_ir, uint16_t dc_ir) { float r ((float)ac_red / dc_red) / ((float)ac_ir / dc_ir); uint8_t idx (uint8_t)((r - 0.4f) / 0.1f); if (idx sizeof(spo2_lookup)) idx sizeof(spo2_lookup) - 1; return spo2_lookup[idx]; }查表法的优势是逻辑简单没有浮点运算的误差积累也方便在标准设备上校正。缺点是表的精度有限但这对于非医疗级别的参考设备来说完全够用。3.4 别忘了数字滤波原始 PPG 信号里不仅有脉搏波还有环境光噪声、运动伪影、50Hz/60Hz 工频干扰。这些噪声在时域里表现得很明显比如波形毛刺多、基线漂移、峰值检测异常。我在代码里加了两层滤波第一层是滑动平均滤波对原始 ADC 数据做 5 点平均可以有效去掉高频毛刺。第二层是移动平均基线消除。由于呼吸引起的基线漂移很缓慢会严重影响阈值判断。我维护一个 100 点的移动平均窗口估计 DC 基线然后把 raw - baseline 作为 AC 信号static int32_t baseline 0; static int32_t sum 0; static int32_t buf[100] {0}; static uint8_t idx 0; sum - buf[idx]; buf[idx] sample; sum sample; idx (idx 1) % 100; baseline sum / 100; int32_t ac sample - baseline;这个 100 点窗口在采样率 100Hz 下对应 1 秒的时间常数既能跟踪基线的缓慢漂移又不会把心跳的 AC 分量“吃”掉。4. OLED 显示与串口输出的双通道实现4.1 OLED 显示内容的组织与刷新策略0.96 寸 OLED 的显示区域是 128x64 像素SSD1306 驱动显存是 1KB。在单片机上驱动 OLED 不需要自己画像素只需要把想要显示的内容填充到显存里然后调用刷新函数把显存发到屏幕上。我设计的 UI 很简单第一行显示心率和血氧数值格式如HR: 72 bpm和SpO2: 98%。第二行画一个迷你滚动波形显示最近一次 PPG 波形的趋势线。第三行显示传感器状态比如 “Finger OK” 或 “No Finger”。关于刷新策略有个重要的教训OLED 的 I2C 刷新速度很慢不要每收到一个样本就刷新一次屏幕。SSD1306 在 I2C 400kHz 模式下传输 1KB 显存大约需要 20ms如果你以 100Hz 的频率刷新OLED 会占用 100% 的 I2C 带宽导致 MAX30102 数据读取被无限阻塞。正确做法是设置一个 10Hz~20Hz 的软件定时器也就是每 50ms~100ms 刷新一次屏幕。心率数值本身是平滑过的20Hz 的刷新率人眼看起来已经足够流畅。下面是精简的 OLED 显示函数void OLED_Display_Update(void) { char buf[16]; OLED_Clear(); sprintf(buf, HR:%d bpm, heart_rate); OLED_ShowString(0, 0, buf, 12); sprintf(buf, SpO2:%d%%, spo2); OLED_ShowString(0, 16, buf, 12); // 绘制微型波形 OLED_DrawWave(0, 32, ppg_waveform, WAVE_LEN); OLED_Refresh(); }注意 sprintf 在嵌入式上会占 flash而且浮点格式化很浪费资源可以换成自己封装的整数转字符串函数。我这边的代码为了简洁用了 sprintf实际项目里建议换掉。4.2 串口协议格式不是简单地 printf串口调试助手能收到数据但如果你直接printf(%d %d\n, red, ir)虽然也能看但对于后续的 PC 端波形处理、数据记录、算法验证来说可读性就很差了。我设计了一个简单的帧格式帧头(0xA5 0x5A) 数据长度(1字节) 数据类型(1字节) 数据负载(N字节) 校验(1字节)数据类型定义成 0x01 代表原始波形数据0x02 代表心率血氧结果。数据负载部分用大端序排列。发送函数如下void UART_SendWaveform(uint16_t red, uint16_t ir) { uint8_t buf[8]; buf[0] 0xA5; buf[1] 0x5A; buf[2] 4; // 数据长度 buf[3] 0x01; // 数据类型波形 buf[4] (uint8_t)(red 8); buf[5] (uint8_t)(red 0xFF); buf[6] (uint8_t)(ir 8); buf[7] (uint8_t)(ir 0xFF); HAL_UART_Transmit(huart1, buf, 8, 10); }使用串口调试助手时打开 HEX 显示就能看到完整的帧。如果想看波形可以在 PC 端写一个小 Python 脚本读取串口数据把红色和红外通道画出来或者直接使用串口调试助手自带的波形显示功能。SSCOM、XCOM 这类工具都支持波形显示选择通道格式时要对应上你发送的整数类型。4.3 串口波特率的选择MAX30102 的采样率我设定在 100Hz每个样本需要发送 4 字节有效数据红红外各 2 字节加上帧头校验一共 8 字节也就是每秒 800 字节波特率 115200 完全够用。如果你用 400Hz 采样率每秒就是 3200 字节115200 波特率每秒约 11520 字节依然没问题。但如果每个样本打包成 float 字符串比如 “1234.5678,”数据量翻倍就需要更高的波特率或者降低采样率。注意把串口波特率调高到 921600 时CH340 模块在长线传输下可能会出现误码建议使用 115200稳定性优先。5. 实测中的坑与调优从波形异常到数值稳定的经验记录5.1 坑一心率值疯狂翻倍现象明明心率 70OLED 上却显示 140 或者 210。排查过程一开始以为是峰值检测的阈值问题把阈值调高后依然翻倍。然后用串口把原始波形发到 PC 上发现波形上有大量高频毛刺波形看起来像锯齿一样。进一步检查发现红光通道和红外通道之间存在串扰——两个通道的 LED 交替点亮时光信号在手指内部发生了散射导致一个通道的采样值受到了另一个通道 LED 的影响。解决办法把 REG_SPO2_CONFIG 里的 LED_PW 从 215μs 改成 411μs让 ADC 在每个 LED 点亮后有更长的稳定时间同时把 FIFO 的平均次数从 1 次改成 4 次。修改后波形明显变得平滑。这个案例说明调整算法参数之前一定先看原始波形。波形是圆的还是尖的、有没有毛刺、有没有基线漂移这些信息比任何算法调参都重要。5.2 坑二血氧值固定在 99%~100%现象不管怎么测SpO2 一直是 99% 或 100%有时候甚至 100%。排查过程用手指挡住红光 LED 再测数值依然不变。这说明算法里算出来的 R 值非常小接近 0.4 的查表边界。原因很可能是红光和红外的 DC 分量计算方式不对。我当时犯的错误是直接用原始 ADC 值作为 DC直接用相邻两个采样点的差值作为 AC导致 AC 分量和 DC 分量不在同一个量纲上计算出的 R 值严重偏小。正确的做法是DC 应该是长时间平均值至少包含数十个脉搏周期AC 应该是在 DC 附近的峰峰值或 RMS 值。修正后// 每 5 秒更新一次 DC dc_red red_baseline; dc_ir ir_baseline; // AC 取最近一个脉搏周期的峰峰值 ac_red red_peak_to_peak; ac_ir ir_peak_to_peak;因为 AC 信号本身具备周期性用一个完整的脉搏周期内最大值减去最小值作为 AC 幅度比采样点差分要稳定得多。5.3 坑三手指放上去没反应现象初始化完成后OLED 上一直显示 “No Finger”但手指明明已经贴紧了。这个问题的根源在于环境光干扰和 LED 电流太小。MAX30102 虽然内部有环境光消除但如果你在强光环境下使用比如阳光直射、白炽灯底下环境光分量仍然可能超过内部消噪能力。我的处理方式是增加了一个简单的检测逻辑当红光和红外通道的 DC 值变化量超过一定阈值时判定为手指接触。这个阈值可以通过初始化后前几秒的基线数据自动标定。uint8_t MAX30102_IsFingerPresent(uint32_t ir) { static uint32_t baseline 0; static uint8_t init 0; if (!init) { baseline ir; init 1; return 0; } if (ir baseline 5000) return 1; // 手指遮住后红外值显著增加 return 0; }适当增加 LED 电流。如果你发现手指放上去之后红外值只有几千满量程是 262144说明 LED 电流太小需要把 LED 电流寄存器加到大一些。5.4 数值稳定性调优的几条实操经验在完成了基本功能之后我做了几个优化让数值稳定了很多第一心率显示加滑动平均。在瞬时心率基础上再做一次 10 点滑动平均。代价是心率变化响应变慢但观感上非常舒服不会出现数字来回跳 2~3 的情况。第二血氧显示加“有效区间”过滤。血氧值如果低于 70% 或者高于 100%直接显示 “--”不显示不可信的数据。因为如果 R 值计算出 80% 以下大概率是信号质量差或者手指没放好而不是真的血氧低。第三如果 MAX30102 的 FIFO 长时间没有数据自动重新初始化。有时候 I2C 总线会因为干扰进入异常状态导致传感器不再产生数据。我在主循环里加了一个超时计时器如果 3 秒内没有新数据就调用 MAX30102_Init() 重新初始化并把错误计数通过串口打印出来。这个“自动复位”机制让系统在长时间运行时的可靠性大幅提升。6. 后续可以怎么扩展这套工程跑通之后扩展的方向非常多。如果你想做成可穿戴设备可以把 0.96 寸 OLED 换成低功耗的墨水屏或者干脆去掉用蓝牙模块HC-08、JDY-31 等把心率血氧数据发送到手机 App。串口协议这部分直接把帧格式转发给蓝牙模块就行几乎不用改。如果你想提高测量精度可以在算法层面加入运动伪影消除。最常用的方法是用加速度计MPU6050 等检测运动状态当运动幅度过大时暂停心率计算而不是输出一个错误的数值。MAX30102 本身也支持 SpO2 和心率同时测量但要排除运动干扰只有传感器数据是不够的。如果你想做低功耗可以把 STM32 切换到睡眠模式用 MAX30102 的中断引脚唤醒 MCU。MAX30102 本身的功耗很低典型工作电流 600μA 左右整机的功耗瓶颈主要在 MCU 和 OLED。最后分享一个个人习惯每次调试传感器项目我都会先把原始波形通过串口拿到 PC 上看一眼确认信号质量没问题再开始写算法。跳过这一步后面大概率会走弯路。这比任何技巧都重要。本文还有配套的精品资源点击获取