文章目录
- 前言
- 一、基本工作原理
-
- [1.1 发射与接收](#1.1 发射与接收)
- [1.2 心率测量原理](#1.2 心率测量原理)
- [1.3 血氧饱和度(SpO2)测量原理](#1.3 血氧饱和度(SpO2)测量原理)
- 二、接口及基本寄存器
-
- [2.1 硬件连接](#2.1 硬件连接)
- [2.2 基本寄存器](#2.2 基本寄存器)
- 三、代码实现
-
- [3.1 初始化设置](#3.1 初始化设置)
- [3.2 读数据](#3.2 读数据)
- [3.3 心率计算](#3.3 心率计算)
- [3.4 血氧饱和度计算](#3.4 血氧饱和度计算)
- [3.5 打印心率血氧数据](#3.5 打印心率血氧数据)
- 总结
前言
MAX30102 是一款常用于心率、血氧饱和度(SpO₂)检测的集成式生物传感器芯片,由 Maxim Integrated(现已并入 Analog Devices)推出。它在可穿戴设备、智能手环、健康监测、嵌入式医疗电子中应用非常广泛。
根据数据手册和用户手册 的介绍,基于HAL库实现MAX30102模块的驱动。参考了B站UP bugyu_ld 的视频内容,它把MAX30102的原理讲的很清楚。
代码和资料放在后面的百度网盘链接中!
一、基本工作原理
MAX30102 是一款集成脉搏血氧仪和心率监测仪模块。它将发射端、接收端以及信号处理电路集成在一个封装内,广泛应用于可穿戴设备中。其基本工作原理可以概括为光电容积脉搏波描记法(Photoplethysmogram, PPG)。简单来说,就是通过光学方法,利用血液对光吸收的特性来测量心率和血氧。
1.1 发射与接收

MAX30102 内部集成了两个发光二极管(LED)和一个光电探测器(光电二极管):
- 红光 LED(Red LED): 发射波长约为 660nm 的红光。
- 红外光 LED(IR LED): 发射波长约为 880nm 的红外光。
- 光电探测器: 用于接收穿透手指或皮肤后反射回来的光线,并将其转化为电信号。

信号处理电路: 包括环境光消除电路、18 位高分辨率 ADC(模数转换器)和数字滤波器。
MAX30102 的光电探测器会捕捉到这种随着心跳呈现周期性起伏的光强度变化,形成一个波形(PPG信号)。
1.2 心率测量原理
虽然血液中存在多种血红蛋白化合物,但在计算血氧饱和度(SpO2)时,通常假设氧合血红蛋白和脱氧血红蛋白是唯一重要的因素。 在反射式脉搏血氧仪的检测系统中,发光二极管(LED)照射皮肤组织,光电二极管则检测反射信号。
该反射信号包含因动脉和毛细血管容积变化而产生光学调制的光。这种光体积描记法(PPG)信号对于测定心率和SpO2水平至关重要。PPG信号包含直流分量和与其结合的交流分量,如下图所示。

DC分量源于非搏动性组织(静脉毛细血管和动脉血)对光的吸收 。 另一方面,AC分量则源于动脉血的搏动性。由于动脉与心脏直接相连,因此动脉血会随着心脏的搏动而搏动。
通过测量连续两个收缩期峰值之间的时间间隔,即可计算出瞬时心率。
需要特别指出的是,仅使用一颗LED(例如红光LED)即可测量心率,因为只需获取交流分量信号即可。
简单讲就是 :
心脏在每次跳动(收缩和舒张)时,会向外周血管(如指尖、手腕的毛细血管)泵入和泵出血液。
- 当心脏收缩 时,血管内血液量增加,血液对光的吸收量变大,被光电探测器接收到的反射光量就会减少。
- 当心脏舒张 时,血管内血液量减少,吸收的光变少,接收到的反射光量就会增加。
通过计算PPG信号的频率 (两个波峰之间的时间间隔),就可以得出心率(BPM,每分钟心跳次数)。
1.3 血氧饱和度(SpO2)测量原理
血液中的血红蛋白分为两种状态,它们对红光和红外光的吸收特性截然不同:
- 氧合血红蛋白( H b O 2 HbO_2 HbO2,携带氧气的血液) :吸收较多的 红外光,让更多的红光穿透或反射。
- 脱氧血红蛋白( H b Hb Hb,未携带氧气的血液) :吸收较多的 红光,让更多的红外光穿透或反射。
测量过程:
- MAX30102 会极快地交替点亮红光 LED 和红外光 LED(肉眼看起来像是一直亮着红光,其实内部在以几百赫兹的频率闪烁)。
- 光电探测器分别记录下皮肤反射回来的红光强度和红外光强度。
- 传感器测量出红光和红外光信号的交流分量 (AC) (由心跳引起的脉动变化)和直流分量 (DC)(由皮肤、骨骼、肌肉和静脉等固定组织引起的基础光吸收)。
- 通过计算红光和红外光的吸收比率: R = A C R e d / D C R e d A C I R / D C I R R = \frac{AC_{Red}/DC_{Red}}{AC_{IR} / DC_{IR}} R=ACIR/DCIRACRed/DCRed
- 微控制器(如 STM32 )读取到这个比例数据后,通过查表法或经验公式,就可以换算得到血氧饱和度(SpO2 的百分比)。
小结:MAX30102 的工作原理就是:"打光 ➔ 测反射光 ➔ 算波动算比率"。
- 通过观察反射光强度的周期性波动 ,得出心率。
- 通过比较红光和红外光 被血液吸收的差异比率 ,得出血氧饱和度。
二、接口及基本寄存器
2.1 硬件连接

| MAX30102 | 功能描述 | 注意事项 |
|---|---|---|
| SCL | I2C 时钟线 | PB6(我用硬件I2C1) |
| SDA | I2C 数据线 | PB7 |
| INT | 中断输出引脚 | 低电平有效。当内部 FIFO 数据准备好时,拉低通知 MCU 来读取,节省 MCU 资源 |
| GND | 电源地 | 必须与单片机共地 |
| VIN | 电源正极 |
I2C 设备地址 8位写地址:0xAE
2.2 基本寄存器
MAX30102 内部有多个 8 位寄存器,控制着采样率、LED 亮度、工作模式等。了解以下几个核心寄存器即可掌握基本驱动:
- 模式配置寄存器 (Mode Configuration)
Bit 7: Reset 位。写入 1 可软件重启传感器,重启后自动清零。
Bit 2:0: 模式选择。
- 写入 0x02: 心率模式 (HR Mode),仅点亮红光 LED。
- 写入 0x03: 血氧模式 (SpO2 Mode),交替点亮红光和红外光 LED(最常用)。

- 血氧配置寄存器 (SpO2 Configuration)
- SpO2 ADC Range (ADC 量程): 决定 ADC 的满量程电流。通常选 4096nA。
- SpO2 Sample Rate (采样率): 每秒采集多少次。可选 50Hz 到 3200Hz。通常选 100Hz 或 400Hz。
- LED Pulse Width (LED 脉冲宽度): LED 每次点亮的时间(69us - 411us)。脉宽越长,ADC分辨率越高(最高可达 18位),但功耗也越大。通常选 411us(对应 18 位分辨率)。
采样率和脉冲的设置不能随意,要参考右边小图

- LED 电流配置寄存器 (LED Pulse Amplitude)
LED1_PA (0x0C): 红光 LED (Red) 的驱动电流大小。
LED2_PA (0x0D): 红外光 LED (IR) 的驱动电流大小。
取值范围是 0x00 到 0xFF。
步进约为 0.2mA。比如写入 0x24 (36十进制),则电流约为 7.2mA。
调试技巧: 如果手指放上去测不到波形,或者 ADC 读数达到最大值(饱和了),就需要调整这两个寄存器的值来改变 LED 亮度。

再往下要进行读数
- 状态与中断寄存器 (Interrupt Status & Enable)
Interrupt Status 1 (0x00) / Interrupt Status 2 (0x01)
读取这些寄存器可以知道是什么触发了中断(比如:FIFO 快满了?还是采集到了新数据?)。读取后会自动清除中断标志。
Interrupt Enable 1 (0x02) / Interrupt Enable 2 (0x03)
用于开启或关闭特定的中断。例如,往 0x02 写入 0x40 可以开启"新数据就绪 (Data Ready)" 中断。

- FIFO 缓冲寄存器 (FIFO Registers - 数据存储区)
MAX30102 包含一个深度为 32 个样本的 FIFO(先进先出队列),可以暂存数据,MCU 不必一直盯着它读。
- FIFO Write Pointer (0x04): 写指针,传感器每采集一个新数据,写指针加1。
- FIFO Read Pointer (0x06): 读指针,MCU 每读走一个数据,读指针加1。
- FIFO Data Register (0x07): 最核心的数据寄存器! MCU 连续读取这个地址,就可以依次把 FIFO 里的红光(Red) 和红外光 (IR) 的 ADC 数值取出来。

前面在 S p O 2 SpO_2 SpO2说过ADC采样率通常设置为18位,18比特3个字节,每个样本是六个字节;FIFO一共缓冲 2 5 = 32 2^5=32 25=32 个字节。
- FIFO 配置寄存器(FIFO Configuration)
它直接决定了单片机读取数据的效率、功耗以及数据是否会丢失。
a.硬件样本平均:SMP_AVE[2:0] (Bits 7:5)
作用:开启内部数据平均,降低单片机负担并减少噪声。
如果采样率设置得很高(比如 400Hz),单片机每秒要读 400 次数据,压力会很大。开启硬件平均后,MAX30102 会在内部将连续的几个采样点取平均值,然后把这个平均值作为一个数据放入 FIFO 中。
这不仅极大降低了输出的数据量(相当于变相降低了数据输出频率),还能起到硬件低通滤波的作用,让波形更平滑。
b.FIFO 溢出覆盖使能:FIFO_ROLLOVER_EN (Bit 4)
作用:决定当 32 个深度的 FIFO 装满时,传感器该怎么办。
写入 0(默认值):停止写入。 当 FIFO 存满 32 个数据后,传感器会抛弃所有新的采样数据,直到单片机过来把 FIFO 里的数据读走。这能保证获取到的数据是绝对连续的旧数据,但可能丢失最新数据。
写入 1:环形缓冲区模式(覆盖旧数据)。 当 FIFO 存满后,传感器会将最新的数据覆盖掉最老的数据 (先进先出变成环形覆盖)。这通常是我们更推荐的设置,因为计算心率/血氧通常需要的是"最新"的波形数据。
c.FIFO 几乎满中断阈值:FIFO_A_FULL[3:0] (Bits 3:0)
作用:设置何时触发 "FIFO 将满 (A_FULL_INT)" 中断。
为了极大地节省单片机资源,我们不需要单片机死循环去查有没有新数据,也不需要每采一个点就进一次中断。我们可以设置一个阈值:"当 FIFO 里快要装满时,再通过 INT 引脚叫醒单片机,让单片机一次性把几十个数据全部读走。 "

三、代码实现
3.1 初始化设置
根据前面寄存器的介绍,下面对MAX30102进行初始化配置,这里设置采样率400Hz,样本8次平均,实际输出给STM32的数据速率为 50Hz.
c
void MAX30102_Init(void)
{
// 1. 复位MAX30102
MAX30102_WriteReg(REG_MODE_CONFIG, 0x40);
HAL_Delay(50); // 等待复位完成
//读取/清除中断状态
MAX30102_ReadReg(REG_INTR_STATUS_1);
// 2. 配置中断使能
MAX30102_WriteReg(REG_INTR_ENABLE_1, 0xC0); // FIFO满和数据就绪中断
MAX30102_WriteReg(REG_INTR_ENABLE_2, 0x00); // 禁用温度中断
// 3. 重置FIFO指针
MAX30102_WriteReg(REG_FIFO_WR_PTR, 0x00);
MAX30102_WriteReg(REG_OVF_COUNTER, 0x00);
MAX30102_WriteReg(REG_FIFO_RD_PTR, 0x00);
// 4. FIFO配置
MAX30102_WriteReg(REG_FIFO_CONFIG, 0x7F);//8次平均,开启溢出覆盖
// 5. 模式配置: SpO2模式 (RED + IR)
MAX30102_WriteReg(REG_MODE_CONFIG, 0x03);
// 6. SpO2配置: ADC=4096nA, 采样率=400Hz, 脉冲宽度=411us
// 400Hz 配合 8次平均,实际输出给 STM32 的数据速率仅为 50Hz
MAX30102_WriteReg(REG_SPO2_CONFIG, 0x2F);
// 7. 配置LED驱动电流
// 7mA 左右通常足够,如果佩戴在手指上数值仍太小,可调至 0x32 (10mA)
MAX30102_WriteReg(REG_LED1_PA, 0x24); // RED LED ~ 7mA
MAX30102_WriteReg(REG_LED2_PA, 0x24); // IR LED ~ 7mA
// 接近检测(可选),如果不用可不配置
MAX30102_WriteReg(REG_PILOT_PA, 0x7F); // 导航LED ~ 25mA
}
3.2 读数据
c
/**
* @brief 读取FIFO数据 - 修正版 (使用正确的I2C读取方式)
* @note 一次性读取6字节:3字节RED + 3字节IR
*/
uint8_t MAX30102_ReadFIFO(void)
{
uint8_t auc_fifo_data[6];
uint32_t un_temp;
//读取中断状态,确保INT引脚复位为高电平
MAX30102_ReadReg(REG_INTR_STATUS_1);
MAX30102_ReadReg(REG_INTR_STATUS_2);
// 直接使用 HAL_I2C_Mem_Read 读取 6 字节 FIFO 数据
if (HAL_I2C_Mem_Read(&hi2c1,MAX30102_ADDRESS,REG_FIFO_DATA,I2C_MEMADD_SIZE_8BIT,auc_fifo_data,6,100) != HAL_OK)
{
return 0;
}
// FIFO 数据格式:3字节 RED + 3字节 IR
un_temp = ((uint32_t)auc_fifo_data[0] << 16) |
((uint32_t)auc_fifo_data[1] << 8) |
(uint32_t)auc_fifo_data[2];
un_red_led_dc = un_temp & 0x03FFFF;
un_temp = ((uint32_t)auc_fifo_data[3] << 16) |
((uint32_t)auc_fifo_data[4] << 8) |
(uint32_t)auc_fifo_data[5];
un_ir_led_dc = un_temp & 0x03FFFF;
return 1;// 成功读取1个样本
}

手指放上去打印出来的红光数据
3.3 心率计算
后处理需要经过多个步骤,包括滤波、峰峰值检测、归一化以及数字信号处理。
Maxim 官方 MAX30102 参考算法实现流程:
PPG波形预处理 ------> 波谷检测 ------> 周期计算 ------>BPM换算

如上图所示,MAX30102 输出的是PPG(光电容积脉搏波),本质上心脏搏动 ------> 血液体积变化 ------> 光吸收变化 ------> ADC数字量变化。于是有上图所示的周期波,每个周期对应一次心跳,所以只要找到周期长度就能算心率。
代码的心率流程:
原始IR数据 ------> 去DC ------> 信号翻转 ------> 移动平均滤波 ------> 动态阈值 ------> 波谷检测 ------> 计算相邻波谷间隔 ------> 换算 BPM
第一步:去DC分量,反转信号
PPG信号 = DC + AC,DC(静态组织/环境光),AC(心跳引起的微小变化),而心率信息只在AC里,因此去掉直流分量,得到纯脉搏波。
由上图可知,如果寻找波峰,中间部分还有一些小尖峰,干扰大,但是如果去找波谷,干扰就小了。
c
// 计算DC均值,并从IR中减去DC
un_ir_mean = 0;
for (k = 0; k < n_ir_buffer_length; k++)
un_ir_mean += pun_ir_buffer[k];
un_ir_mean = un_ir_mean / n_ir_buffer_length;
// 移除DC并反转信号,以便将峰值检测器用作谷值检测器
for (k = 0; k < n_ir_buffer_length; k++)
an_x[k] = -1 * (pun_ir_buffer[k] - un_ir_mean);
因此,在官方算法中使用的是"谷值检测",把波形翻转。
第二步:移动平均滤波
4点滑动平均,去除高频噪声,ADC抖动,环境干扰。相当于低通滤波器
c
//进行均值滤波(4点移动平均)
for (k = 0; k < BUFFER_SIZE - MA4_SIZE; k++)
{
an_x[k] = (an_x[k] + an_x[k+1] + an_x[k+2] + an_x[k+3]) / (int)4;
}
第三步:动态阈值
因为PPG幅值会变化,所以不能用固定阈值,防止噪声误触发。官方给的是均值限幅
c
// 计算阈值,将阈值设置在30-60之间
n_th1 = 0;
for (k = 0; k < BUFFER_SIZE; k++)
n_th1 += an_x[k];
n_th1 = n_th1 / BUFFER_SIZE;
if (n_th1 < 30) n_th1 = 30;
if (n_th1 > 60) n_th1 = 60;
第四步:波谷检测
找一个点比左右都大,由于已经翻转,实际是在找波谷
c
// 峰值检测(获取信号中波谷的个数,以及波谷的位置)
maxim_find_peaks(an_ir_valley_locs, &n_npks, an_x, BUFFER_SIZE, n_th1, 4, 15);//peak_height, peak_distance, max_num_peaks
第五步:峰值检测逻辑
开始上升
c
if (pn_x[i] > n_min_height && pn_x[i] > pn_x[i-1])
开始下降
c
if ((pn_x[i] < n_min_height) && (pn_x[i-1] >= n_min_height))
上升 ------>下降,形成一个峰
进行峰值消抖
第六步:maxim_remove_close_peaks
PPG里经常有双峰,不是所有的峰都算,距离太近的峰只保留最高的
第七步:心率计算
c
// 计算各个波谷的间距之和
n_peak_interval_sum = 0;
if (n_npks >= 2)
{
for (k = 1; k < n_npks; k++)
n_peak_interval_sum += (an_ir_valley_locs[k] - an_ir_valley_locs[k-1]);
n_peak_interval_sum = n_peak_interval_sum / (n_npks - 1);
*pn_heart_rate = (int32_t)((FS * 60) / n_peak_interval_sum);
*pch_hr_valid = 1;
}
else
{
*pn_heart_rate = -999;//无法计算,因为峰数太少
*pch_hr_valid = 0;
}
因为PPG有抖动,不能只看一次间隔,而是多个周期求平均,以提高稳定性,所以使用"平均峰距"。
心率 = 采样率*60/相邻峰间样本点
输出-999,说明峰值不足,可能手指没放好,信号太弱
3.4 血氧饱和度计算
血氧饱和度算法不是数峰值,而是在计算红光与红外光的吸收比例。
氧合血红蛋白(HbO2)和脱氧血红蛋白(Hb)对不同波长光的吸收不同,缺氧时RED衰减更明显,两种光的比例变化反映氧合程度。

SpO2 真正需要的是 AC/DC
下一步是将这两者进行归一化(即计算交流分量与直流分量的比值),然后求出一个值"R",该值是归一化红光数据与归一化红外数据的比值。最后,可以使用线性近似法来计算SpO2。
这个R是RED的脉动强度/IR的脉动强度,与血氧浓度近似线性相关。

DC(直流分量)是通过在两个谷值之间使用直线近似法得出的。
首先对这些点进行标记。将 50 到 100 之间的谷值标记为"1",将 150 到 200 之间的谷值标记为"3"。150 到 200 之间的峰值标记为"2"。DC(直流分量)是AC(交流信号)的偏移量,可通过先在1和3之间画一条直线,然后从2点画一条平行于y轴的直线至连接1和3的直线来确定。直流值即为两条直线的交点。
另一方面,AC(交流分量)则是峰值("2")与直流值之间的距离。
血氧算法依赖心率,因为SpO2算法必须先找波谷,血氧必须知道"一个完整心搏",否则没法计算AC振幅。
因此,代码中的逻辑是:先找波谷求心率 -> 得到波谷坐标数组 -> 拿波谷坐标去截取每一拍的红光/红外光波形 -> 计算血氧。
求波谷,前半部分和求心率相同,因此在心率算法后面接着写血氧算法
第一步:恢复原始数据
c
// 重新加载原始值用于SpO2计算:RED(=y) and IR(=x)
for (k = 0; k < n_ir_buffer_length; k++){
an_x[k] = pun_ir_buffer[k];
an_y[k] = pun_red_buffer[k];
}
在前面的心率计算中,为了找波谷,代码对红外数据做了反转和去直流(去掉了DC分量)。但血氧公式里必须要用到真正的 DC 值,所以这里必须重新把纯净的原始数据装载进来。
第二步:找到两个波谷之间的最大值
c
for (i = an_ir_valley_locs[k]; i < an_ir_valley_locs[k+1]; i++) { ... }
它利用了心率算法传过来的两个相邻波谷位置(valley_locs[k] 和 valley_locs[k+1]),在这个区间内遍历,找到了最高点的值(DC值)和最高点所在的时间索引(n_y_dc_max_idx)。
第三步:计算 AC 分量
c
// 计算AC分量 (以RED为例)
n_y_ac = (an_y[an_ir_valley_locs[k+1]] - an_y[an_ir_valley_locs[k]]) * (n_y_dc_max_idx - an_ir_valley_locs[k]);
n_y_ac = an_y[an_ir_valley_locs[k]] + n_y_ac / (an_ir_valley_locs[k+1] - an_ir_valley_locs[k]);
n_y_ac = an_y[n_y_dc_max_idx] - n_y_ac; // 从原始数据中减去线性DC分量
正常理想情况下, A C = 峰值 − 谷值 AC=峰值-谷值 AC=峰值−谷值 就行了。但是人呼吸或者手轻微抖动,会导致波形的基线是斜着的(基线漂移)。
这三行代码是在做一次线性插值(初中数学:已知两点求直线方程):它假定两个波谷之间连一条直线,算出"在波峰那一瞬间,正下方的基线高度是多少"。
最后用 峰值-这个估算的基线高度,得到了最纯粹、剥离了基线漂移的 A C r e d AC_{red} ACred 和 A C i r AC_{ir} ACir
第四步:计算R值
c
n_nume = (n_y_ac * n_x_dc_max) >> 7; // 分子 = AC_red * DC_ir
n_denom = (n_x_ac * n_y_dc_max) >> 7; // 分母 = AC_ir * DC_red
为什么右移7位?因为原始数据都是 18 位 ADC 数据,两个一乘会达到 36 位,直接导致 32 位单片机寄存器溢出。>> 7 相当于除以 128,损失一点精度换取不溢出。
第五步:计算比率并放大100倍
c
an_ratio[n_i_ratio_count] = (n_nume * 100) / n_denom;
为什么这么写: 因为在没有浮点运算单元 (FPU) 的单片机上算小数是很浪费时间的。算出 R 值比如是 0.95,单片机存不了小数,就乘以 100 变成 95,当整数处理。
第六步:中值滤波,查表得出最终血氧
PPG 信号对运动极度敏感,算出的 5 个周期的 R 值可能有一个是离谱的错值。把这 5 个值排序,取中间那个,是单片机里最简单粗暴且极其有效的抗干扰滤波方法。
官方提前用电脑把 R 值从 0 到 1.84 的结果全算好,写死在数组 uch_spo2_table 里。你算出 R 值是 95(代表0.95),直接去数组里拿第 95 个数据(查表),这个数据就是最终的血氧值!这种做法被称为空间换时间,是底层 C 语言极其优美的操作。

3.5 打印心率血氧数据
MAX30102.c 读寄存器/FIFO,algrithm.c 核心 HR/SpO2算法,max30102_read.c 调度 + 平滑
这个 max30102_read.c 文件的核心思想是:"滑动窗口 (Sliding Window)" 与 "数据平滑滤波"。
- 初始化启动 (Init_MAX30102)
一开始,数组是空的。必须先死等 MAX30102 采集满 150 个样本(按常见采样率,大约需要 3 秒钟)。这 150 个原始数据被喂给算法,得出"第一口"心率和血氧数据。
- 核心机制:滑动窗口 (ReadHeartRateSpO2 的前半部分)
心率是需要实时更新的,不能每次都等 3 秒重新采 150 个点。所以代码采用了滑动窗口:
保留老数据: 把数组里后 100 个旧数据,整体往前挪(平移到 0~99 的位置)。
采集新数据: 再去读取 50 个新样本,放在数组最后面(100~149 的位置)。
再次计算: 拿着这拼凑好的(100个老数据 + 50个新数据)再次去调用核心算法。
- 结果的平滑与异常剔除 (ReadHeartRateSpO2 的后半部分)
核心算法返回的 n_heart_rate 和 n_spo2 是瞬间值,非常容易受到手抖的干扰而剧烈跳动(比如上一秒80,下一秒突然120)。
所以代码写了一大段复杂的逻辑:
剔除离群值: 如果当前心率和之前保存的心率相差超过 10 (hr_buf[i] + 10),就认为是手抖产生的废数据,直接丢弃。
移动平均滤波: 把有效的、稳定的心率值存进一个 16 个元素的数组 hr_buf 中。根据当前收集到了几个有效值,除以相应的倍数(>>1, >>2, >>4 其实就是除以 2, 4, 16),求出平均值 hrAvg 和 spo2Avg。
最终通过 Get_Heart_Rate() 拿到的,其实是经过这层层筛选和平均后的平滑值。

这份代码现象是手放上去一段时间之后,先显示血氧,然后显示心率;手拿开后也要过一段时间才会清0.
但是我把它加入其它模块,心率是正常的,但是血氧有时一直是0,我没懂是哪出了问题!
串口使用USART1,PA9和PA10引脚;OLED使用PB9和PB10,软I2C;MAX30102,SCL->PB6,SDA->PB7,INT->PB5
总结
终于把这个模块搞完了,折磨了我两个周!
通过网盘分享的文件:MAX30102资料和代码.zip
链接: https://pan.baidu.com/s/19KQ3C4BbC4DTNKfk63gm3A?pwd=fycc 提取码: fycc