MAX30102模块原理及代码实现

文章目录

  • 前言
  • 一、基本工作原理
    • [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,未携带氧气的血液) :吸收较多的 红光,让更多的红外光穿透或反射。

测量过程:

  1. MAX30102 会极快地交替点亮红光 LED 和红外光 LED(肉眼看起来像是一直亮着红光,其实内部在以几百赫兹的频率闪烁)。
  2. 光电探测器分别记录下皮肤反射回来的红光强度和红外光强度。
  3. 传感器测量出红光和红外光信号的交流分量 (AC) (由心跳引起的脉动变化)和直流分量 (DC)(由皮肤、骨骼、肌肉和静脉等固定组织引起的基础光吸收)。
  4. 通过计算红光和红外光的吸收比率: 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
  5. 微控制器(如 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 亮度、工作模式等。了解以下几个核心寄存器即可掌握基本驱动:

  1. 模式配置寄存器 (Mode Configuration)
    Bit 7: Reset 位。写入 1 可软件重启传感器,重启后自动清零。
    Bit 2:0: 模式选择。
  • 写入 0x02: 心率模式 (HR Mode),仅点亮红光 LED。
  • 写入 0x03: 血氧模式 (SpO2 Mode),交替点亮红光和红外光 LED(最常用)。
  1. 血氧配置寄存器 (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 位分辨率)。

采样率和脉冲的设置不能随意,要参考右边小图

  1. LED 电流配置寄存器 (LED Pulse Amplitude)

LED1_PA (0x0C): 红光 LED (Red) 的驱动电流大小。

LED2_PA (0x0D): 红外光 LED (IR) 的驱动电流大小。

取值范围是 0x00 到 0xFF。

步进约为 0.2mA。比如写入 0x24 (36十进制),则电流约为 7.2mA。

调试技巧: 如果手指放上去测不到波形,或者 ADC 读数达到最大值(饱和了),就需要调整这两个寄存器的值来改变 LED 亮度。

再往下要进行读数

  1. 状态与中断寄存器 (Interrupt Status & Enable)

Interrupt Status 1 (0x00) / Interrupt Status 2 (0x01)

读取这些寄存器可以知道是什么触发了中断(比如:FIFO 快满了?还是采集到了新数据?)。读取后会自动清除中断标志。

Interrupt Enable 1 (0x02) / Interrupt Enable 2 (0x03)

用于开启或关闭特定的中断。例如,往 0x02 写入 0x40 可以开启"新数据就绪 (Data Ready)" 中断。

  1. 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 个字节。

  1. 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)" 与 "数据平滑滤波"。

  1. 初始化启动 (Init_MAX30102)

一开始,数组是空的。必须先死等 MAX30102 采集满 150 个样本(按常见采样率,大约需要 3 秒钟)。这 150 个原始数据被喂给算法,得出"第一口"心率和血氧数据。

  1. 核心机制:滑动窗口 (ReadHeartRateSpO2 的前半部分)

心率是需要实时更新的,不能每次都等 3 秒重新采 150 个点。所以代码采用了滑动窗口:

保留老数据: 把数组里后 100 个旧数据,整体往前挪(平移到 0~99 的位置)。

采集新数据: 再去读取 50 个新样本,放在数组最后面(100~149 的位置)。

再次计算: 拿着这拼凑好的(100个老数据 + 50个新数据)再次去调用核心算法。

  1. 结果的平滑与异常剔除 (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

相关推荐
星夜夏空991 小时前
STM32单片机学习(3)——前置知识学习
stm32·单片机·学习
星夜夏空993 小时前
STM32单片机学习(5) —— STM32的一些名词解释
stm32·单片机·学习
yuan199973 小时前
STM32 速度控制器:PWM + PID 无级调速实现
stm32·单片机·嵌入式硬件
czwxkn3 小时前
pcb设计-器件:稳压二极管
单片机·嵌入式硬件
灵哎惹,凌沃敏4 小时前
CM3/CM4内核总线知识总结
c语言·arm开发·单片机
森旺电子4 小时前
candence操作
单片机·嵌入式硬件·cadence
czwxkn5 小时前
pcb设计-电路:基准电压电路(TL431)
单片机·嵌入式硬件
三佛科技-134163842125 小时前
LED阅读灯方案开发,LED护眼读书灯单片机选择(FT60F010A,FT61F023,FT62F211,FT62F0MBA,FT32F103)
单片机·嵌入式硬件·智能家居·pcb工艺