[APM32F4] MAX30102与APM32F407

[复制链接]
 楼主| kai迪皮 发表于 2024-10-21 10:30 | 显示全部楼层 |阅读模式

最近在玩MAX30102,想实现一个简单的测量心率和血氧饱和度。本文就使用APM32F4驱动MAX30102

进行心率和血氧饱和度(SpO2)的测量的实现记录。

在本文我尝试着详细说明MAX30102的工作原理、配置、数据采集过程以及关键代码的实现。

1 MAX30102简介

MAX30102是一款集成式脉搏血氧传感器模块,常用于可穿戴设备和健康监测仪。它利用光电容积描记技术(PPG)测量血氧水平和心率。传感器通过红光和红外光LED,以不同的波长穿透皮肤组织。反射的光被传感器检测,从而计算出心率和SpO2。

心率测量原理:心率测量基于反射光的脉动变化。心脏每跳动一次,血液中的体积会有所增加,从而导致反射光的变化。通过检测反射光信号变化的频率,MAX30102可以计算心率。

血氧测量原理: 血氧测量利用的是氧合血红蛋白和还原血红蛋白对不同波长光的不同吸收特性。传感器发出红光和红外光,两种光穿过皮肤和血管,部分被吸收,部分被反射。通过比较这两种光的吸收比率,可以计算出血氧饱和度。

这里补充一下

1.1 PPG技术背景

工作原理:

PPG技术通过光源(通常是LED)发出光线,穿透皮肤后被血液和组织吸收和散射。

随着心脏泵血,血管内的血容量会周期性变化,这导致光吸收特性的变化。

检测器(通常是光电二极管)接收反射或透射的光,根据光强度变化来分析血容量的变化。

测量方法:

透射式PPG:光线穿过组织(如耳垂或指尖),适用于这些部位。

反射式PPG:光线在组织表面反射回来,适用于接触面大的区域(如手腕或额头)。

信号成分:

PPG信号由两个主要成分组成:

直流成分(DC):反映静态组织和平均血流。

交流成分(AC):反映心跳引起的动态血流变化。

PPG应用:

心率监测:通过分析AC成分的周期性变化(即心跳周期),可以计算心率。

血氧饱和度测量(SpO2):利用红光和红外光在氧合血红蛋白和去氧血红蛋白中的不同吸收特性,通过比率计算得到SpO2值。

优点:

非侵入性:不需要穿刺或采血。

简单便捷:只需要一个小型的LED和光电探测器。

低成本:广泛用于可穿戴设备和便携式健康监测设备。

一些缺点:

信号易受噪声影响:环境光、运动伪影和皮肤特性可影响PPG信号精度。

个体差异:皮肤颜色、厚度和血流特性因人而异,可能导致测量误差。


2 APM32F4与MAX30102

MAX30102与APM32F4通过I2C接口进行通信。APM32F4作为I2C主机,控制MAX30102传感器。我们将配置I2C接口,初始化传感器,并通过I2C读取传感器数据。

2.1 I2C配置

以下是用于配置I2C接口和MAX30102地址的部分代码:

  1. #define MAX30102_I2C_PORT       hi2c1
  2. #define MAX30102_ADDRESS        0xAE   //(0x57<<1)


*   MAX30102\_I2C\_PORT:定义用于通信的I2C端口,我们这里使用的是APM32F407的I2C1与MAX30102通信。

*   MAX30102\_ADDRESS:定义MAX30102的I2C地址。

与MAX30102通信的关键是:

1.  写入并读取其控制寄存器。

  1. MAX30102_STATUS Max30102_WriteReg(uint8_t uch_addr, uint8_t uch_data)
  2. {
  3.     if (DAL_I2C_Mem_Write(&MAX30102_I2C_PORT, MAX30102_ADDRESS, uch_addr, 1, &uch_data, 1, I2C_TIMEOUT) == DAL_OK)
  4.         return MAX30102_OK;
  5.     return MAX30102_ERROR;
  6. }

  7. MAX30102_STATUS Max30102_ReadReg(uint8_t uch_addr, uint8_t* puch_data)
  8. {
  9.     if (DAL_I2C_Mem_Read(&MAX30102_I2C_PORT, MAX30102_ADDRESS, uch_addr, 1, puch_data, 1, I2C_TIMEOUT) == DAL_OK)
  10.         return MAX30102_OK;
  11.     return MAX30102_ERROR;
  12. }


我这里直接使用了APM32F407的DAL库进行封装。

2.  读取其测量到的数据内容。

  1. MAX30102_STATUS Max30102_ReadFifo(volatile uint32_t* pun_red_led, volatile uint32_t* pun_ir_led)
  2. {
  3.     uint32_t un_temp;
  4.     *pun_red_led = 0;
  5.     *pun_ir_led = 0;
  6.     uint8_t ach_i2c_data[6];

  7.     if (DAL_I2C_Mem_Read(&MAX30102_I2C_PORT, MAX30102_ADDRESS, REG_FIFO_DATA, 1, ach_i2c_data, 6, I2C_TIMEOUT) != DAL_OK)
  8.     {
  9.         return MAX30102_ERROR;
  10.     }
  11.     un_temp = (unsigned char) ach_i2c_data[0];
  12.     un_temp <<= 16;
  13.     *pun_red_led += un_temp;
  14.     un_temp = (unsigned char) ach_i2c_data[1];
  15.     un_temp <<= 8;
  16.     *pun_red_led += un_temp;
  17.     un_temp = (unsigned char) ach_i2c_data[2];
  18.     *pun_red_led += un_temp;

  19.     un_temp = (unsigned char) ach_i2c_data[3];
  20.     un_temp <<= 16;
  21.     *pun_ir_led += un_temp;
  22.     un_temp = (unsigned char) ach_i2c_data[4];
  23.     un_temp <<= 8;
  24.     *pun_ir_led += un_temp;
  25.     un_temp = (unsigned char) ach_i2c_data[5];
  26.     *pun_ir_led += un_temp;
  27.     *pun_red_led &= 0x03FFFF; //Mask MSB [23:18]
  28.     *pun_ir_led &= 0x03FFFF; //Mask MSB [23:18]

  29.     return MAX30102_OK;
  30. }


1.  数据读取:

函数首先通过 I2C 接口从 MAX30102 的 FIFO 数据寄存器 `REG_FIFO_DATA` 中读取 6 字节的数据。

`ach_i2c_data[0]` 到 `ach_i2c_data[2]` 存储红光 LED 数据。

`ach_i2c_data[3]` 到 `ach_i2c_data[5]` 存储红外光 LED 数据。

2.  数据拼接:

    1.  红光 LED 数据的拼接:

`ach_i2c_data[0]` 左移 16 位,加到 `*pun_red_led`。

`ach_i2c_data[1]` 左移 8 位,加到 `*pun_red_led`。

`ach_i2c_data[2]` 加到 `*pun_red_led`。

2.  红外光 LED 数据的拼接:

`ach_i2c_data[3]` 左移 16 位,加到 `*pun_ir_led`。

`ach_i2c_data[4]` 左移 8 位,加到 `*pun_ir_led`。

`ach_i2c_data[5]` 加到 `*pun_ir_led`。

2.2 MAX30102初始化与配置

在初始化过程中,我们需要重置传感器并配置寄存器以设置测量模式和参数。以下是初始化过程中的关键代码:

初始化代码解析


  1. MAX30102_STATUS Max30102_Init(I2C_HandleTypeDef* i2c)

  2. {

  3.     uint8_t uch_dummy;

  4.     // 重置传感器
  5.     if (MAX30102_OK != Max30102_Reset())
  6.         return MAX30102_ERROR;


  7.     // 验证I2C连接是否正常
  8.     if (MAX30102_OK != Max30102_ReadReg(0, &uch_dummy))
  9.         return MAX30102_ERROR;

  10.     // 配置FIFO指针和溢出计数器
  11.     if (MAX30102_OK != Max30102_FifoWritePointer(0x00))
  12.         return MAX30102_ERROR;
  13.     if (MAX30102_OK != Max30102_FifoOverflowCounter(0x00))
  14.         return MAX30102_ERROR;
  15.     if (MAX30102_OK != Max30102_FifoReadPointer(0x00))
  16.         return MAX30102_ERROR;

  17.     // 设置FIFO采样平均
  18.     if (MAX30102_OK != Max30102_FifoSampleAveraging(FIFO_SMP_AVE_1))
  19.         return MAX30102_ERROR;

  20.     // 禁用FIFO回绕
  21.     if (MAX30102_OK != Max30102_FifoRolloverEnable(0))
  22.         return MAX30102_ERROR;

  23.     // 设置FIFO几乎满阈值
  24.     if (MAX30102_OK != Max30102_FifoAlmostFullValue(MAX30102_FIFO_ALMOST_FULL_SAMPLES))
  25.         return MAX30102_ERROR;

  26.     // 配置SpO2模式
  27.     if (MAX30102_OK != Max30102_SetMode(MODE_SPO2_MODE))
  28.         return MAX30102_ERROR;

  29.     // 设置SpO2 ADC范围和采样率
  30. if (MAX30102_OK != Max30102_SpO2AdcRange(SPO2_ADC_RGE_4096))
  31. return MAX30102_ERROR;
  32. if (MAX30102_OK != Max30102_SpO2SampleRate(SPO2_SAMPLE_RATE))
  33. return MAX30102_ERROR;

  34. // 配置LED脉冲宽度
  35. if (MAX30102_OK != Max30102_SpO2LedPulseWidth(SPO2_PULSE_WIDTH_411))
  36. return MAX30102_ERROR;

  37. // 配置LED电流
  38. if (MAX30102_OK != Max30102_Led1PulseAmplitude(MAX30102_RED_LED_CURRENT_LOW))
  39. return MAX30102_ERROR;
  40. if (MAX30102_OK != Max30102_Led2PulseAmplitude(MAX30102_IR_LED_CURRENT_LOW))
  41. return MAX30102_ERROR;

  42. // 启用中断
  43. if (MAX30102_OK != Max30102_SetIntAlmostFullEnabled(1))
  44. return MAX30102_ERROR;
  45. if (MAX30102_OK != Max30102_SetIntFifoDataReadyEnabled(1))
  46. return MAX30102_ERROR;

  47.     StateMachine = MAX30102_STATE_BEGIN;
  48. return MAX30102_OK;
  49. }


*   Max30102\_Reset:重置传感器,确保所有寄存器处于初始状态。

*   Max30102\_SetMode: 设置传感器为SpO2测量模式。

*   Max30102\_Led1PulseAmplitude:配置红光LED的电流强度。

程序调用关系图:

image-20241018151817493.png

3 数据读取与处理

在主循环中,我们定期读取传感器的数据,并计算心率和SpO2值。

3.1 主循环

以下代码段展示了主循环中如何获取心率和SpO2值:
  1. /* Infinite loop */
  2.     while (1)
  3.     {
  4.         Max30102_Task();

  5.         printf("HeartRate:%d,SpO2:%d\r\n",Max30102_GetHeartRate(), Max30102_GetSpO2Value()/100);

  6.         BOARD_LED_Toggle(LED2);
  7.         DAL_Delay(1000U);
  8.     }


*   Max30102\_Task: 处理传感器任务,包括数据采集。

*   Max30102\_GetHeartRate 和 Max30102\_GetSpO2Value: 获取当前计算的心率和血氧值。

MAX30102使用状态机管理测量流程,确保在不同状态下执行正确的操作。

  1. void Max30102_Task(void)
  2. {
  3.     switch (StateMachine)
  4.     {
  5.         case MAX30102_STATE_BEGIN:
  6.             HeartRate = 0;
  7.             Sp02Value = 0;
  8.             if (IsFingerOnScreen)
  9.             {
  10.                 CollectedSamples = 0;
  11.                 BufferTail = BufferHead;
  12.                 Max30102_Led1PulseAmplitude(MAX30102_RED_LED_CURRENT_HIGH);
  13.                 Max30102_Led2PulseAmplitude(MAX30102_IR_LED_CURRENT_HIGH);
  14.                 StateMachine = MAX30102_STATE_CALIBRATE;
  15.             }
  16.             break;

  17.         case MAX30102_STATE_CALIBRATE:
  18.             if (IsFingerOnScreen)
  19.             {
  20.                 if (CollectedSamples > (MAX30102_BUFFER_LENGTH - MAX30102_SAMPLES_PER_SECOND))
  21.                 {
  22.                     StateMachine = MAX30102_STATE_CALCULATE_HR;
  23.                 }
  24.             }
  25.             else
  26.             {
  27.                 // 手指不在传感器上,降低LED电流,返回初始状态
  28.                 Max30102_Led1PulseAmplitude(MAX30102_RED_LED_CURRENT_LOW);
  29.                 Max30102_Led2PulseAmplitude(MAX30102_IR_LED_CURRENT_LOW);
  30.                 StateMachine = MAX30102_STATE_BEGIN;
  31.             }
  32.             break;

  33.         case MAX30102_STATE_CALCULATE_HR:
  34.             if (IsFingerOnScreen)
  35.             {
  36.                 // 计算心率和SpO2值
  37.                 maxim_heart_rate_and_oxygen_saturation(IrBuffer, RedBuffer, MAX30102_BUFFER_LENGTH - MAX30102_SAMPLES_PER_SECOND, BufferTail, &Sp02Value, &Sp02IsValid, &HeartRate, &IsHrValid);
  38.                 BufferTail = (BufferTail + MAX30102_SAMPLES_PER_SECOND) % MAX30102_BUFFER_LENGTH;
  39.                 CollectedSamples = 0;
  40.                 StateMachine = MAX30102_STATE_COLLECT_NEXT_PORTION;
  41.             }
  42.             else
  43.             {
  44.                 // 手指不在传感器上,降低LED电流,返回初始状态
  45.                 Max30102_Led1PulseAmplitude(MAX30102_RED_LED_CURRENT_LOW);
  46.                 Max30102_Led2PulseAmplitude(MAX30102_IR_LED_CURRENT_LOW);
  47.                 StateMachine = MAX30102_STATE_BEGIN;
  48.             }
  49.             break;

  50.         case MAX30102_STATE_COLLECT_NEXT_PORTION:
  51.             if (IsFingerOnScreen)
  52.             {
  53.                 if (CollectedSamples > MAX30102_SAMPLES_PER_SECOND)
  54.                 {
  55.                     StateMachine = MAX30102_STATE_CALCULATE_HR;
  56.                 }
  57.             }
  58.             else
  59.             {
  60.                 // 手指不在传感器上,降低LED电流,返回初始状态
  61.                 Max30102_Led1PulseAmplitude(MAX30102_RED_LED_CURRENT_LOW);
  62.                 Max30102_Led2PulseAmplitude(MAX30102_IR_LED_CURRENT_LOW);
  63.                 StateMachine = MAX30102_STATE_BEGIN;
  64.             }
  65.             break;
  66.     }
  67. }


函数调用关系图:

image-20241018152041716.png

3.2 计算心率和血氧饱和度

`maxim_heart_rate_and_oxygen_saturation`函数是MAX30102传感器用于计算心率和血氧饱和度(SpO2)的核心算法之一。下面是对该函数的详细说明:

函数原型

  1. void maxim_heart_rate_and_oxygen_saturation(
  2.     volatile uint32_t* pun_ir_buffer,
  3.     volatile uint32_t* pun_red_buffer,
  4.     int32_t n_buffer_length,
  5.     uint16_t un_offset,
  6.     int32_t* pn_spo2,
  7.     int8_t* pch_spo2_valid,
  8.     int32_t* pn_heart_rate,
  9.     int8_t* pch_hr_valid
  10. )
参数说明

1.  pun\_ir\_buffer: 指向红外LED采集的数据缓冲区。

2.  pun\_red\_buffer: 指向红光LED采集的数据缓冲区。

3.  n\_buffer\_length: 缓冲区长度,即可用的数据点数量。

4.  un\_offset: 缓冲区的起始偏移量,通常用于循环缓冲区中。

5.  pn\_spo2: 指向存储计算结果的SpO2值的指针。

6.  pch\_spo2\_valid: 指向一个标志位,表示计算的SpO2值是否有效。

7.  pn\_heart\_rate: 指向存储计算结果的心率值的指针。

8.  pch\_hr\_valid: 指向一个标志位,表示计算的心率值是否有效。

功能描述

1.  去除直流成分:

    1.  首先从红外信号中移除直流成分,这是通过计算信号均值并从每个样本中减去该均值来实现的。

2.  信号平滑:

    1.  使用移动平均滤波器(4点)对信号进行平滑处理,以减少高频噪声的影响。

3.  峰谷检测:

    1.  反转波形以便使用峰值检测算法识别原始信号中的谷值。

    2.  计算峰值之间的间隔,确定心率。

4.  AC/DC成分计算:

    1.  计算红光和红外光信号的交流(AC)和直流(DC)成分。

    2.  使用AC/DC比率来计算血氧饱和度。

5.  SpO2计算:

    1.  根据计算得到的比率,使用预定义的查找表或公式来计算SpO2值。

    2.  验证计算结果的有效性。

6.  输出结果:

    1.  将计算得到的心率和SpO2值存储到相应的输出参数中。

细节说明

1.  峰谷检测算法: 通过检测信号中的最大/最小值来识别心跳周期,以计算心率。

2.  AC/DC比率计算: 通过测量红光和红外光的AC和DC成分,利用其比率计算出SpO2值。

3.  查找表使用: 为了提高效率和简化计算,常常使用查找表而不是实时计算复杂的数学公式。

3.3 中断处理

当传感器检测到特定事件(如数据准备就绪)时,会触发中断。以下是中断回调的实现:

  1. void DAL_GPIO_EINT_Callback(uint16_t GPIO_Pin)
  2. {
  3.     if (GPIO_Pin == INT_Pin)
  4.     {
  5.         Max30102_InterruptCallback();
  6.     }
  7. }


*   Max30102\_InterruptCallback:在中断触发时调用,处理数据读取和状态更新。

以下是对该函数中不同中断处理流程的详细解释:读取传感器的中断状态寄存器,以确定哪种中断事件已经发生。

  1. uint8_t Status;
  2. while (MAX30102_OK != Max30102_ReadInterruptStatus(&Status));


3.3.1 FIFO几乎满中断处理

  1.     // Almost Full FIFO Interrupt handle
  2.     if (Status & (1 << INT_A_FULL_BIT))
  3.     {
  4.         for (uint8_t i = 0; i < MAX30102_FIFO_ALMOST_FULL_SAMPLES; i++)
  5.         {
  6.             while (MAX30102_OK != Max30102_ReadFifo((RedBuffer + BufferHead), (IrBuffer + BufferHead)));
  7.             if (IsFingerOnScreen)
  8.             {
  9.                 if (IrBuffer[BufferHead] < MAX30102_IR_VALUE_FINGER_OUT_SENSOR) IsFingerOnScreen = 0;
  10.             }
  11.             else
  12.             {
  13.                 if (IrBuffer[BufferHead] > MAX30102_IR_VALUE_FINGER_ON_SENSOR) IsFingerOnScreen = 1;
  14.             }
  15.             BufferHead = (BufferHead + 1) % MAX30102_BUFFER_LENGTH;
  16.             CollectedSamples++;
  17.         }
  18.     }


1.  功能:当FIFO缓冲区接近满时触发此中断,确保及时读取数据以防止溢出。

2.  流程:

    1.  读取FIFO中的数据,并存储在\`RedBuffer\`和\`IrBuffer\`中。

    2.  根据红外光信号判断手指是否在传感器上,并更新\`IsFingerOnScreen\`标志。

    3.  更新缓冲区头指针\`BufferHead\`和采集的样本数\`CollectedSamples\`。

3.3.2 新FIFO数据就绪中断处理

  1. // New FIFO Data Ready Interrupt handle
  2.     if (Status & (1 << INT_PPG_RDY_BIT))
  3.     {
  4.         while (MAX30102_OK != Max30102_ReadFifo((RedBuffer + BufferHead), (IrBuffer + BufferHead)));
  5.         if (IsFingerOnScreen)
  6.         {
  7.             if (IrBuffer[BufferHead] < MAX30102_IR_VALUE_FINGER_OUT_SENSOR) IsFingerOnScreen = 0;
  8.         }
  9.         else
  10.         {
  11.             if (IrBuffer[BufferHead] > MAX30102_IR_VALUE_FINGER_ON_SENSOR) IsFingerOnScreen = 1;
  12.         }
  13.         BufferHead = (BufferHead + 1) % MAX30102_BUFFER_LENGTH;
  14.         CollectedSamples++;
  15.     }


1.  功能: 当新的测量数据准备好时触发此中断,确保APM32F4能够及时获取最新数据。

2.  流程: 类似于FIFO几乎满的处理中,读取数据并更新状态。

4 总结

通过本文所述的配置和代码实现,你可以成功地使用APM32F4微控制器驱动MAX30102传感器,实现心率和血氧的实时测量。在实际应用中,可以根据需求调整传感器的采样率、LED电流等参数,以优化性能和功耗。

这里是串口打印效果:

image-20241021101433880.png

这里是代码: APM32F4xx_DAL_SDK_V1.1.1_MAX30102.zip (5.77 MB, 下载次数: 9)



希望这篇博客能帮助你更好地理解和实现MAX30102的应用。如果你有任何问题或建议,欢迎留言讨论!

foxhotel 发表于 2025-3-28 20:52 | 显示全部楼层
能单独做出来氧合血红蛋白和血红蛋白值吗
jobszheng 发表于 2025-3-28 22:31 | 显示全部楼层
原来心率是通过波峰与波谷的差值来计算的。
我一直以为也是计算一个时间内的跳动次数呢
星云避风港 发表于 2025-4-20 08:40 | 显示全部楼层
楼主的软件设计层次还是蛮丰富的。
思路好清晰,膜拜一下楼主
您需要登录后才可以回帖 登录 | 注册

本版积分规则

43

主题

292

帖子

11

粉丝
快速回复 在线客服 返回列表 返回顶部