MAX30102血氧心率模块讲解二:驱动代码及计算算法

MAX30102血氧心率模块讲解二:驱动代码及计算算法

目录

一、摘要

二、iic库

三、max30102的驱动层函数

1. MAX30102 I2C设备地址 

2.向MAX30102寄存器写入数据

3.从MAX30102寄存器读取数据

4.初始化MAX30102传感器

 5.重置MAX30102传感器,读取MAX30102的设备ID,清空MAX30102 FIFO缓冲区,启用或禁用MAX30102的低功耗模式

四、max30102应用层函数

1.初始化MAX30102传感器和数据缓冲区的函数

2.常态化读取并计算心率和血氧值的函数

五、MAX30102心率和血氧计算函数

1.心率监测(HR)

2.血氧测量(SpO2)

3.心率计算原理:

4.血氧饱和度(SpO2)计算

5.数据过滤与平均

6.MAX30102心率和血氧计算函数讲解:

代码和资料链接:


一、摘要

 第一篇文章有详细讲解MAX30102血氧心率模块引脚定义、典型应用电路和寄存器的详细讲解MAX30102血氧心率模块讲解一:测量原理,硬件介绍及寄存器详细解析-ZEEKLOG博客

这是第二篇讲解MAX30102血氧心率模块的文章,主要包含软件模拟IIC库,max30102的驱动层函数,max30102的应用层函数以及心率和血氧的解算函数,文末网盘链接会有完整的示例代码和项目,有需要可以直接获取,基于标准库写的。

代码和资料链接:

通过网盘分享的文件:MAX30102心率血氧传感器资料
链接: https://pan.baidu.com/s/1u_J5HX3-fc0obVjtVtk0Vg?pwd=wgti 提取码: wgti 
--来自百度网盘超级会员v7的分享


二、iic库

因为MAX30102血氧心率模块来说,通过I2C接口可以读取其内部寄存器数据,如配置寄存器、状态寄存器以及读取数据缓冲区等,从而实现对传感器的控制和数据采集,所以使用微控制器提供的硬件I2C模块或者软件模拟的I2C协议来与MAX30102进行通信,下面简单讲讲我常用的软件模拟IIC库(文中末尾会放网盘链接,包含完整代码和项目)


包含

  1. 配置SDA引脚为输出方向
  2. 配置SDA引脚为输入方向
  3. 初始化I²C总线对应的GPIO引脚
  4. 生成I²C总线起始信号
  5. 生成I²C总线停止信号
  6. 主机产生一个应答信号(ACK)
  7. 主机产生一个非应答信号(NACK)
  8. 等待从机应答信号
  9. 向I²C总线发送一个字节数据
  10. 从I²C总线读取一个字节数据
     

这里简单讲解以下IIC的硬件层

以下是示例代码:

/** * @file iic.c * @brief 软件模拟I²C总线驱动实现 * @details 通过GPIO直接控制实现I²C总线通信协议,用于与加速度传感器等外设通信 * @note 硬件连接:SCL接PA6,SDA接PA7 */ #include "iic.h" #include "delay.h" /** * @brief 配置SDA引脚为输出方向 * @note 在I²C通信中需要动态切换SDA方向,发送数据时设为输出模式 */ void I2C_SDA_OUT(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; // PA7作为SDA线 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置为50MHz GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // SDA推挽输出模式 GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIO } /** * @brief 配置SDA引脚为输入方向 * @note 在I²C通信中接收数据或等待应答信号时需将SDA设为输入模式 */ void I2C_SDA_IN(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; // PA7作为SDA线 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置为50MHz GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // SDA上拉输入模式 GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIO } /** * @brief 初始化I²C总线对应的GPIO引脚 * @note 设置SCL和SDA引脚为输出模式,并初始为高电平(空闲状态) */ void IIC_init() { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; // PA6(SCL), PA7(SDA) GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出模式 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 50MHz速度 GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIO GPIO_SetBits(GPIOA, GPIO_Pin_6 | GPIO_Pin_7); // SCL和SDA默认为高电平 } /** * @brief 生成I²C总线起始信号 * @note 时序:SCL高电平期间,SDA由高变低,表示通信开始 */ void IIC_start() { I2C_SDA_OUT(); // 设置SDA为输出方向 IIC_SDA = 1; // SDA初始为高 IIC_SCL = 1; // SCL初始为高 DelayUs(5); // 保持稳定时间 IIC_SDA = 0; // SDA拉低(高到低的跳变)产生起始信号 DelayUs(5); // 保持稳定时间 IIC_SCL = 0; // SCL拉低,准备发送或接收数据 } /** * @brief 生成I²C总线停止信号 * @note 时序:SCL高电平期间,SDA由低变高,表示通信结束 */ void IIC_stop() { I2C_SDA_OUT(); // 设置SDA为输出方向 IIC_SCL = 0; // SCL初始为低 IIC_SDA = 0; // SDA初始为低 DelayUs(5); // 保持稳定时间 IIC_SCL = 1; // SCL拉高 IIC_SDA = 1; // SDA由低变高(产生停止信号) DelayUs(5); // 保持稳定时间 } /** * @brief 主机产生一个应答信号(ACK) * @note 时序:SCL低电平期间,SDA拉低,然后SCL拉高一个时钟周期 */ void IIC_ack() { IIC_SCL = 0; // SCL拉低 I2C_SDA_OUT(); // 设置SDA为输出方向 IIC_SDA = 0; // SDA拉低表示应答 DelayUs(2); // 保持稳定时间 IIC_SCL = 1; // SCL拉高产生脉冲 DelayUs(5); // 保持高电平时间 IIC_SCL = 0; // SCL恢复低电平 } /** * @brief 主机产生一个非应答信号(NACK) * @note 时序:SCL低电平期间,SDA保持高,然后SCL拉高一个时钟周期 */ void IIC_noack() { IIC_SCL = 0; // SCL拉低 I2C_SDA_OUT(); // 设置SDA为输出方向 IIC_SDA = 1; // SDA拉高表示非应答 DelayUs(2); // 保持稳定时间 IIC_SCL = 1; // SCL拉高产生脉冲 DelayUs(2); // 保持高电平时间 IIC_SCL = 0; // SCL恢复低电平 } /** * @brief 等待从机应答信号 * @return 0:接收到应答成功; 1:接收应答失败(超时) * @note 主机发送完一个字节后,释放SDA线,等待从机拉低SDA表示应答 */ u8 IIC_wait_ack() { u8 tempTime = 0; // 超时计数器 I2C_SDA_IN(); // 设置SDA为输入方向 IIC_SDA = 1; // 释放SDA线(内部上拉) DelayUs(1); // 短暂延时 IIC_SCL = 1; // SCL拉高,从机可以发出应答信号 DelayUs(1); // 短暂延时 // 等待SDA被从机拉低(应答),带超时检测 while(READ_SDA) { tempTime++; if(tempTime > 250) // 超时判断(约250us) { IIC_stop(); // 总线出错,发送停止信号 return 1; // 返回应答失败 } } IIC_SCL = 0; // SCL拉低,结束应答周期 return 0; // 返回应答成功 } /** * @brief 向I²C总线发送一个字节数据 * @param txd 要发送的字节 * @note 从高位到低位逐位发送,SCL低电平时改变SDA,高电平时保持SDA稳定 */ void IIC_send_byte(u8 txd) { u8 i = 0; I2C_SDA_OUT(); // 设置SDA为输出方向 IIC_SCL = 0; // 拉低时钟开始数据传输 // 循环发送8位数据,从高位(MSB)开始 for(i = 0; i < 8; i++) { IIC_SDA = (txd & 0x80) >> 7; // 取出最高位 txd <<= 1; // 数据左移一位,准备发送次高位 IIC_SCL = 1; // SCL拉高,数据有效 DelayUs(2); // 保持高电平时间 IIC_SCL = 0; // SCL拉低,准备改变SDA DelayUs(2); // 保持低电平时间 } } /** * @brief 从I²C总线读取一个字节数据 * @param ack 读取后是否发送应答信号(1:发送ACK, 0:发送NACK) * @return 读取到的字节数据 * @note 由高位到低位依次读取,每位在SCL高电平期间采样SDA */ u8 IIC_read_byte(u8 ack) { u8 i = 0, receive = 0; I2C_SDA_IN(); // 设置SDA为输入方向 // 循环接收8位数据 for(i = 0; i < 8; i++) { IIC_SCL = 0; // SCL拉低 DelayUs(2); // 低电平时间 IIC_SCL = 1; // SCL拉高,准备采样 receive <<= 1; // 数据左移,为接收新的一位腾出位置 if(READ_SDA) // 读取SDA电平 receive++; // SDA为高电平则置1 DelayUs(1); // 保持稳定时间 } // 根据参数决定是否发送应答 if(!ack) IIC_noack(); // 发送非应答信号 else IIC_ack(); // 发送应答信号 return receive; // 返回读取到的字节 }


三、max30102的驱动层函数

实现通过I2C总线与MAX30102通信,配置和读取心率血氧数据,会结合数据手册一步步的解释具体的驱动函数,最后会有总体代码



1. MAX30102 I2C设备地址 

/** MAX30102 I2C设备地址 */ #define MAX30102_WR_ADDRESS 0xAE // 写地址

2.向MAX30102寄存器写入数据

最开始发起I2C总线启动信号后,从图中的时序可以看到需要三次周期的写入,分别发送写操作的设备地址,寄存器地址和寄存器数据,每一步都有ACK等待回应,最后发送I2C总线停止信号。

代码如下:

/** * @brief 向MAX30102寄存器写入数据 * @param uch_addr 寄存器地址 * @param uch_data 要写入的数据 * @return true:成功; false:失败 */ bool maxim_max30102_write_reg(uint8_t uch_addr, uint8_t uch_data) { /* 第1步:发起I2C总线启动信号 */ i2c_Start(); /* 第2步:发送设备地址和写控制位 */ i2c_SendByte(MAX30102_WR_ADDRESS | I2C_WR); // 写操作 /* 第3步:等待设备应答 */ if (i2c_WaitAck() != 0) { goto cmd_fail; // 设备无应答 } /* 第4步:发送寄存器地址 */ i2c_SendByte(uch_addr); if (i2c_WaitAck() != 0) { goto cmd_fail; // 设备无应答 } /* 第5步:发送寄存器数据 */ i2c_SendByte(uch_data); if (i2c_WaitAck() != 0) { goto cmd_fail; // 设备无应答 } /* 第6步:发送I2C总线停止信号 */ i2c_Stop(); return true; // 执行成功 cmd_fail: /* 命令执行失败,发送停止信号释放总线 */ i2c_Stop(); return false; } 

3.从MAX30102寄存器读取数据

图中分别是从MAX30102读取一个字节的数据和读取多个字节的时序图,在前三个周期中的协议都是一样的,
首先发起I2C总线启动信号,然后依次发送写操作的设备地址,要读取的寄存器地址,下面要重新读取寄存器数据,接着继续发送读操作的设备地址,下一步读取寄存器数据

下面是详细代码:

/** * @brief 从MAX30102寄存器读取数据 * @param uch_addr 寄存器地址 * @param puch_data 读取数据存储指针 * @return true:成功; false:失败 */ bool maxim_max30102_read_reg(uint8_t uch_addr, uint8_t *puch_data) { /* 第1步:发起I2C总线启动信号 */ i2c_Start(); /* 第2步:发送设备地址和写控制位(先写入要读取的寄存器地址) */ i2c_SendByte(MAX30102_WR_ADDRESS | I2C_WR); if (i2c_WaitAck() != 0) { goto cmd_fail; // 设备无应答 } /* 第3步:发送寄存器地址 */ i2c_SendByte((uint8_t)uch_addr); if (i2c_WaitAck() != 0) { goto cmd_fail; // 设备无应答 } /* 第4步:重新启动I2C总线,准备读取数据 */ i2c_Start(); /* 第5步:发送设备地址和读控制位 */ i2c_SendByte(MAX30102_WR_ADDRESS | I2C_RD); // 读操作 if (i2c_WaitAck() != 0) { goto cmd_fail; // 设备无应答 } /* 第6步:读取寄存器数据 */ *puch_data = i2c_ReadByte(); // 读取一个字节 i2c_NAck(); // 发送NACK,表示读取结束 /* 第7步:发送I2C总线停止信号 */ i2c_Stop(); return true; // 执行成功 cmd_fail: /* 命令执行失败,发送停止信号释放总线 */ i2c_Stop(); return false; }

4.初始化MAX30102传感器

初始化中会 

  1. 配置中断使能寄存器
  2. 配置FIFO寄存器
  3. 模式配置
  4. SpO2配置
  5. 配置LED驱动电流

其中的寄存器地址和使用方法可以看我关于MAX30102的第一篇文章
MAX30102血氧心率模块讲解一:测量原理,硬件介绍及寄存器详细解析-ZEEKLOG博客

/** * @brief 初始化MAX30102传感器 * @return true:初始化成功; false:初始化失败 * @note 配置传感器工作模式、采样率、LED电流等参数 */ bool maxim_max30102_init(void) { /* 配置中断使能寄存器 */ if(!maxim_max30102_write_reg(REG_INTR_ENABLE_1, 0xC0)) // 0xC0: 只使能FIFO满和数据就绪中断 return false; if(!maxim_max30102_write_reg(REG_INTR_ENABLE_2, 0x00)) // 禁用温度就绪中断 return false; /* 配置FIFO寄存器 */ if(!maxim_max30102_write_reg(REG_FIFO_WR_PTR, 0x00)) // 重置FIFO写指针 return false; if(!maxim_max30102_write_reg(REG_OVF_COUNTER, 0x00)) // 清零溢出计数器 return false; if(!maxim_max30102_write_reg(REG_FIFO_RD_PTR, 0x00)) // 重置FIFO读指针 return false; /* FIFO配置: 样本平均数=8, 禁用溢出回滚, FIFO满阈值=17 */ if(!maxim_max30102_write_reg(REG_FIFO_CONFIG, 0x6F)) return false; /* 模式配置: SpO2模式 (心率+血氧) */ if(!maxim_max30102_write_reg(REG_MODE_CONFIG, 0x03)) return false; /* SpO2配置: ADC量程=4096nA, 采样率=400Hz, LED脉冲宽度=411μs */ if(!maxim_max30102_write_reg(REG_SPO2_CONFIG, 0x2F)) return false; /* 配置LED驱动电流 */ if(!maxim_max30102_write_reg(REG_LED1_PA, 0x17)) // LED1(红光)电流~4.5mA return false; if(!maxim_max30102_write_reg(REG_LED2_PA, 0x17)) // LED2(红外光)电流~4.5mA return false; if(!maxim_max30102_write_reg(REG_PILOT_PA, 0x7F)) // 导航LED电流~25mA return false; return true; // 所有配置成功 } 

 5.重置MAX30102传感器,读取MAX30102的设备ID,清空MAX30102 FIFO缓冲区,启用或禁用MAX30102的低功耗模式

/** * @brief 重置MAX30102传感器 * @return true:重置成功; false:重置失败 * @note 向模式配置寄存器写入重置位(0x40),触发软件重置 */ bool maxim_max30102_reset(void) { return maxim_max30102_write_reg(REG_MODE_CONFIG, 0x40); } /** * @brief 读取MAX30102的设备ID * @param id 存储设备ID的指针 * @return true:读取成功; false:读取失败 */ bool maxim_max30102_read_id(uint8_t *id) { return maxim_max30102_read_reg(REG_PART_ID, id); } /** * @brief 清空MAX30102 FIFO缓冲区 * @return true:成功; false:失败 * @note 清空FIFO的方法是将读写指针都设为相同值 */ bool maxim_max30102_clear_fifo(void) { if(!maxim_max30102_write_reg(REG_FIFO_WR_PTR, 0x00)) return false; if(!maxim_max30102_write_reg(REG_OVF_COUNTER, 0x00)) return false; if(!maxim_max30102_write_reg(REG_FIFO_RD_PTR, 0x00)) return false; return true; } /** * @brief 启用或禁用MAX30102的低功耗模式 * @param enable true:启用低功耗; false:禁用低功耗 * @return true:设置成功; false:设置失败 */ bool maxim_max30102_set_low_power(bool enable) { uint8_t reg_value; if(!maxim_max30102_read_reg(REG_MODE_CONFIG, &reg_value)) return false; if(enable) reg_value |= 0x20; // 设置低功耗模式位 else reg_value &= ~0x20; // 清除低功耗模式位 return maxim_max30102_write_reg(REG_MODE_CONFIG, reg_value); }


四、max30102应用层函数

包含

1.初始化MAX30102传感器和数据缓冲区的函数

2.常态化读取并计算心率和血氧值的函数

1.初始化MAX30102传感器和数据缓冲区的函数

首先要再次说明,使用IIC协议从MAX30102传感器读取到的是,传感器采集到反射的红光和红外光的光照强度,这个红光和红外光从传感器发出的


最开始硬件初始化,包含IIC接口初始化,复位MAX30102,读取/清除中断状态,初始化MAX30102
我们会设置采样率为50Hz,获得缓冲区150个样本约3秒数据,读取初始150个样本作为基准数据(这个是为后续解算血氧做准备),并进行第一次初始心率和血氧计算(即maxim_heart_rate_and_oxygen_saturation(……)函数,这个第五章会详细讲解)

/** * @brief 初始化MAX30102传感器和数据缓冲区 * @note 会采集约150个样本(约3秒数据)作为初始化数据进行基准计算 */ void Init_MAX30102(void) { int32_t i; /* 初始化亮度相关变量 */ un_brightness = 0; un_min = 0x3FFFF; // 设置初始最小值 un_max = 0; // 设置初始最大值 /* 硬件初始化 */ bsp_InitI2C(); // IIC接口初始化 maxim_max30102_reset(); // 复位MAX30102 maxim_max30102_read_reg(REG_INTR_STATUS_1, &uch_dummy); // 读取/清除中断状态 maxim_max30102_init(); // 初始化MAX30102 /* 设置采样率为50Hz,缓冲区150个样本约3秒数据 */ n_ir_buffer_length = 150; /* 读取初始150个样本作为基准数据 */ for(i = 0; i < n_ir_buffer_length; i++) { // 从MAX30102 FIFO中读取数据,不同版本传感器数据通道可能不同 #if (MAX_VERSION == VERSION_1_) maxim_max30102_read_fifo((aun_ir_buffer+i), (aun_red_buffer+i)); #elif (MAX_VERSION == VERSION_2_) maxim_max30102_read_fifo((aun_red_buffer+i), (aun_ir_buffer+i)); #endif /* 更新亮度范围 */ if(un_min > aun_red_buffer[i]) un_min = aun_red_buffer[i]; // 更新最小值记录 if(un_max < aun_red_buffer[i]) un_max = aun_red_buffer[i]; // 更新最大值记录 } un_prev_data = aun_red_buffer[i]; /* 进行初始心率和血氧计算 */ maxim_heart_rate_and_oxygen_saturation( aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_spo2, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid ); }

2.常态化读取并计算心率和血氧值的函数

该函数基于滑动窗口技术,保留100个历史样本,每次采集50个新样本,形成连续性数据序列,既确保了数据的实时性,又保留了必要的数据延续性,有效抑制了瞬态干扰。

在数据处理方面,采用了多级验证策略:

  • 有效性验证:心率必须在60-150BPM范围内,血氧必须高于80%
  • 连续性验证:需达到连续五次有效采样才确认为真实数据
  • 幅度验证:与历史数据比较,剔除波动过大的异常值

为提高结果稳定性,设计了基于缓冲区深度自适应的平均算法,随着有效样本增加逐步扩大平均范围,从最初的2点平均逐步过渡到16点平均。

函数还包含超时机制,当持续8次无有效数据时,自动将显示清零,避免显示过时或错误数据,提高用户体验。

具体函数看下面

/** * @brief 读取并计算心率和血氧值 * @note 采用滑动窗口方式,保留100个历史样本,新增50个新样本进行计算 */ void ReadHeartRateSpO2(void) { int32_t i; float f_temp; static u8 COUNT = 8; // 数据处理倍率控制 /* 变量初始化 */ i = 0; un_min = 0x3FFFF; un_max = 0; /* 数据滑动:保留后50个样本,向前移动100个位置,为新数据腾出空间 */ for(i = 50; i < 150; i++) { aun_red_buffer[i - 50] = aun_red_buffer[i]; aun_ir_buffer[i - 50] = aun_ir_buffer[i]; /* 更新亮度范围 */ if(un_min > aun_red_buffer[i]) un_min = aun_red_buffer[i]; if(un_max < aun_red_buffer[i]) un_max = aun_red_buffer[i]; } /* 读取50个新样本填充缓冲区末尾 */ for(i = 100; i < 150; i++) { un_prev_data = aun_red_buffer[i - 1]; // 保存前一个数据 /* 从MAX30102读取新数据 */ #if (MAX_VERSION == VERSION_1_) maxim_max30102_read_fifo((aun_ir_buffer+i), (aun_red_buffer+i)); #elif (MAX_VERSION == VERSION_2_) maxim_max30102_read_fifo((aun_red_buffer+i), (aun_ir_buffer+i)); #endif /* 自适应LED亮度控制 - 根据信号强度调整 */ if(aun_red_buffer[i] > un_prev_data) // 信号上升 { f_temp = aun_red_buffer[i] - un_prev_data; f_temp /= (un_max - un_min); // 归一化 f_temp *= MAX_BRIGHTNESS; // 缩放到亮度范围 f_temp = un_brightness - f_temp; // 计算新亮度 if(f_temp < 0) un_brightness = 0; else un_brightness = (int)f_temp; } else // 信号下降 { f_temp = un_prev_data - aun_red_buffer[i]; f_temp /= (un_max - un_min); // 归一化 f_temp *= MAX_BRIGHTNESS; // 缩放到亮度范围 un_brightness += (int)f_temp; // 计算新亮度 if(un_brightness > MAX_BRIGHTNESS) un_brightness = MAX_BRIGHTNESS; } } /* 计算心率和血氧饱和度 */ maxim_heart_rate_and_oxygen_saturation( aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_spo2, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid ); /* 每8次计算更新一次显示数据 */ if(COUNT++ > 8) { COUNT = 0; /* 处理心率数据 */ if ((ch_hr_valid == 1) && (n_heart_rate < 150) && (n_heart_rate > 60)) // 心率值有效且在合理范围 { hrTimeout = 0; // 重置超时计数 /* 连续收到五个有效样本算一次有效心率 */ if (hrValidCnt == 4) { hrThrowOutSamp = 1; // 标记为可能的异常值 hrValidCnt = 0; /* 与缓冲区内历史数据比较判断异常 */ for (i = 12; i < 16; i++) { if (n_heart_rate < hr_buf[i] + 10) // 与历史数据差值在合理范围 { hrThrowOutSamp = 0; hrValidCnt = 4; } } } else { hrValidCnt = hrValidCnt + 1; // 有效样本计数 } /* 将合格的心率数据加入缓冲区 */ if (hrThrowOutSamp == 0) { /* 更新心率环形缓冲区 */ for(i = 0; i < 15; i++) { hr_buf[i] = hr_buf[i + 1]; // 数据前移 } hr_buf[15] = n_heart_rate; // 添加新的心率值 /* 更新缓冲区填充量 */ if (hrBuffFilled < 16) { hrBuffFilled = hrBuffFilled + 1; } /* 根据缓冲区填充量选择平均算法 */ hrSum = 0; if (hrBuffFilled < 2) // 数据太少不计算 { //hrAvg = 0; } else if (hrBuffFilled < 4) // 2-3个样本取最近2个平均 { for(i = 14; i < 16; i++) { hrSum = hrSum + hr_buf[i]; } hrAvg = hrSum >> 1; // 除以2 } else if (hrBuffFilled < 8) // 4-7个样本取最近4个平均 { for(i = 12; i < 16; i++) { hrSum = hrSum + hr_buf[i]; } hrAvg = hrSum >> 2; // 除以4 } else if (hrBuffFilled < 16) // 8-15个样本取最近8个平均 { for(i = 8; i < 16; i++) { hrSum = hrSum + hr_buf[i]; } hrAvg = hrSum >> 3; // 除以8 } else // 缓冲区已满,取全部16个平均 { for(i = 0; i < 16; i++) { hrSum = hrSum + hr_buf[i]; } hrAvg = hrSum >> 4; // 除以16 } } hrThrowOutSamp = 0; // 重置异常标志 } else // 心率测量值无效或超范围 { hrValidCnt = 0; if (hrTimeout == 8) // 连续8次无有效数据则清零 { hrAvg = 0; // 心率归零 hrBuffFilled = 0; // 缓冲清零 } else { hrTimeout++; // 超时计数增加 } } /* 处理血氧数据 - 算法逻辑与心率类似 */ if ((ch_spo2_valid == 1) && (n_spo2 > 80)) // 血氧值有效且在合理范围 { spo2Timeout = 0; // 重置超时计数 /* 连续收到五个有效样本算一次有效血氧值 */ if (spo2ValidCnt == 4) { spo2ThrowOutSamp = 1; // 标记为可能的异常值 spo2ValidCnt = 0; /* 与缓冲区内历史数据比较判断异常 */ for (i = 12; i < 16; i++) { if (n_spo2 > spo2_buf[i] - 10) // 与历史数据差值在合理范围 { spo2ThrowOutSamp = 0; spo2ValidCnt = 4; } } } else { spo2ValidCnt = spo2ValidCnt + 1; // 有效样本计数 } /* 将合格的血氧数据加入缓冲区 */ if (spo2ThrowOutSamp == 0) { /* 更新血氧环形缓冲区 */ for(i = 0; i < 15; i++) { spo2_buf[i] = spo2_buf[i + 1]; // 数据前移 } spo2_buf[15] = n_spo2; // 添加新的血氧值 /* 更新缓冲区填充量 */ if (spo2BuffFilled < 16) { spo2BuffFilled = spo2BuffFilled + 1; } /* 根据缓冲区填充量选择平均算法 */ spo2Sum = 0; if (spo2BuffFilled < 2) // 数据太少不计算 { //spo2Avg = 0; } else if (spo2BuffFilled < 4) // 2-3个样本取最近2个平均 { for(i = 14; i < 16; i++) { spo2Sum = spo2Sum + spo2_buf[i]; } spo2Avg = spo2Sum >> 1; // 除以2 } else if (spo2BuffFilled < 8) // 4-7个样本取最近4个平均 { for(i = 12; i < 16; i++) { spo2Sum = spo2Sum + spo2_buf[i]; } spo2Avg = spo2Sum >> 2; // 除以4 } else if (spo2BuffFilled < 16) // 8-15个样本取最近8个平均 { for(i = 8; i < 16; i++) { spo2Sum = spo2Sum + spo2_buf[i]; } spo2Avg = spo2Sum >> 3; // 除以8 } else // 缓冲区已满,取全部16个平均 { for(i = 0; i < 16; i++) { spo2Sum = spo2Sum + spo2_buf[i]; } spo2Avg = spo2Sum >> 4; // 除以16 } } spo2ThrowOutSamp = 0; // 重置异常标志 } else // 血氧测量值无效或超范围 { spo2ValidCnt = 0; if (spo2Timeout == 8) // 连续8次无有效数据则清零 { spo2Avg = 0; // 血氧归零 spo2BuffFilled = 0; // 缓冲清零 } else { spo2Timeout++; // 超时计数增加 } } } }


五、MAX30102心率和血氧计算函数


前情提要(详细讲解看第一篇文章,这里简单讲讲):
MAX30102血氧心率模块讲解一:测量原理,硬件介绍及寄存器详细解析-ZEEKLOG博客

1.心率监测(HR)

心率测量基于以下原理:

  1. 心脏跳动导致动脉血管扩张和收缩
  2. 血管容积的周期性变化影响光的反射和吸收
  3. 光电二极管捕获这种周期性的光强变化,产生PPG信号
  4. 通过数字滤波去除噪声和运动伪影

计算处理后的PPG波形峰值间隔得到心率

2.血氧测量(SpO2)


血氧饱和度测量基于光学原理和Beer-Lambert定律,过程如下:

1.在测量过程中,芯片使用18位ADC采集两种波长下的反射光强度

2.每个波长的PPG信号包含两个主要成分:

  •                 a. 静态成分(DC):来自组织、骨骼和静脉血的稳定反
  •                 b.动态成分(AC):由于心脏搏动引起的动脉血容量变化产生的脉动信号

3.血氧饱和度计算:

a. 首先计算比率R:

  • R = (红光AC/红光DC)/(红外AC/红外DC)

b. 使用经验拟合公式转换为SpO2百分比:

  • SpO2 = -45.060R² + 30.354R + 94.845

3.心率计算原理:
 

  1. 信号预处理
    • 计算DC均值并从原始信号中移除
    • 反转信号以便使用峰值检测器作为谷值检测器
    • 应用4点移动平均滤波平滑信号
  2. 峰值检测
    • 设定阈值(通常在30-60之间)
    • 检测高于阈值的信号峰值
    • 移除间距太近的峰值
  3. 心率计算
    • 计算相邻峰值间的时间间隔
    • 应用公式:心率(BPM) = (采样频率 * 60) / 峰值间隔平均值
    • 例如:若采样率为50Hz,峰值间隔为50个样本,则心率为60BPM

4.血氧饱和度(SpO2)计算

血氧计算基于不同波长光在含氧和不含氧血红蛋白中吸收率的差异:

  1. 比率计算
    • 对于每对谷值之间:
      • 找出红光和红外信号的DC最大值
      • 计算AC分量(交流部分)
      • 计算比率:R = (RED_AC * IR_DC) / (IR_AC * RED_DC)
  2. SpO2值查表
    • 将计算出的比率R排序并取中值
    • 通过查找表转换为SpO2值
    • 实际公式近似为:SpO2 ≈ -45.060 * R² + 30.354 * R + 94.845

5.数据过滤与平均

为提高测量可靠性,代码实现了多种数据处理机制:

  1. 有效性验证
    • 心率必须在60-150范围内才被视为有效
    • 血氧必须大于80%才被视为有效
  2. 连续验证
    • 需要连续5个有效样本才确认为有效测量
    • 与历史数据比较以排除异常波动
  3. 多级平均
    • 使用16元素环形缓冲区保存历史数据
    • 根据有效数据量采用不同平均策略:
      • 2-3个样本:取最近2个平均
      • 4-7个样本:取最近4个平均
      • 8-15个样本:取最近8个平均
      • 16个样本:全部16个平均
  4. 超时处理
  • 连续8次无有效数据则清零心率/血氧显示

6.MAX30102心率和血氧计算函数讲解:

输入:红外LED信号数据,红光LED信号数据和缓冲区长度

输出:血氧值,血氧值有效标志,心率值,心率值有效标志

void maxim_heart_rate_and_oxygen_saturation( uint32_t *pun_ir_buffer, // 红外LED信号数据 int32_t n_ir_buffer_length, // 缓冲区长度 uint32_t *pun_red_buffer, // 红光LED信号数据 int32_t *pn_spo2, // 输出:血氧值 int8_t *pch_spo2_valid, // 输出:血氧值有效标志 int32_t *pn_heart_rate, // 输出:心率值 int8_t *pch_hr_valid // 输出:心率值有效标志 )

下面是详细代码:

/** \file algorithm.cpp ****************************************************** * * Project: MAXREFDES117# * Filename: algorithm.cpp * Description: This module calculates the heart rate/SpO2 level * * * -------------------------------------------------------------------- * * This code follows the following naming conventions: * * char ch_pmod_value * char (array) s_pmod_s_string[16] * float f_pmod_value * int32_t n_pmod_value * int32_t (array) an_pmod_value[16] * int16_t w_pmod_value * int16_t (array) aw_pmod_value[16] * uint16_t uw_pmod_value * uint16_t (array) auw_pmod_value[16] * uint8_t uch_pmod_value * uint8_t (array) auch_pmod_buffer[16] * uint32_t un_pmod_value * int32_t * pn_pmod_value * * ------------------------------------------------------------------------- */ /******************************************************************************* * Copyright (C) 2016 Maxim Integrated Products, Inc., All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL MAXIM INTEGRATED BE LIABLE FOR ANY CLAIM, DAMAGES * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * Except as contained in this notice, the name of Maxim Integrated * Products, Inc. shall not be used except as stated in the Maxim Integrated * Products, Inc. Branding Policy. * * The mere transfer of this software does not imply any licenses * of trade secrets, proprietary technology, copyrights, patents, * trademarks, maskwork rights, or any other form of intellectual * property whatsoever. Maxim Integrated Products, Inc. retains all * ownership rights. ******************************************************************************* */ #include "algorithm.h" //uch_spo2_table is approximated as -45.060*ratioAverage* ratioAverage + 30.354 *ratioAverage + 94.845 ; const uint8_t uch_spo2_table[184] = { 95, 95, 95, 96, 96, 96, 97, 97, 97, 97, 97, 98, 98, 98, 98, 98, 99, 99, 99, 99, 99, 99, 99, 99, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 99, 99, 99, 99, 99, 99, 99, 99, 98, 98, 98, 98, 98, 98, 97, 97, 97, 97, 96, 96, 96, 96, 95, 95, 95, 94, 94, 94, 93, 93, 93, 92, 92, 92, 91, 91, 90, 90, 89, 89, 89, 88, 88, 87, 87, 86, 86, 85, 85, 84, 84, 83, 82, 82, 81, 81, 80, 80, 79, 78, 78, 77, 76, 76, 75, 74, 74, 73, 72, 72, 71, 70, 69, 69, 68, 67, 66, 66, 65, 64, 63, 62, 62, 61, 60, 59, 58, 57, 56, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 31, 30, 29, 28, 27, 26, 25, 23, 22, 21, 20, 19, 17, 16, 15, 14, 12, 11, 10, 9, 7, 6, 5, 3, 2, 1 } ; void maxim_heart_rate_and_oxygen_saturation(uint32_t *pun_ir_buffer, int32_t n_ir_buffer_length, uint32_t *pun_red_buffer, int32_t *pn_spo2, int8_t *pch_spo2_valid, int32_t *pn_heart_rate, int8_t *pch_hr_valid) /** * \brief Calculate the heart rate and SpO2 level * \par Details * By detecting peaks of PPG cycle and corresponding AC/DC of red/infra-red signal, the an_ratio for the SPO2 is computed. * Since this algorithm is aiming for Arm M0/M3. formaula for SPO2 did not achieve the accuracy due to register overflow. * Thus, accurate SPO2 is precalculated and save longo uch_spo2_table[] per each an_ratio. * * \param[in] *pun_ir_buffer - IR sensor data buffer * \param[in] n_ir_buffer_length - IR sensor data buffer length * \param[in] *pun_red_buffer - Red sensor data buffer * \param[out] *pn_spo2 - Calculated SpO2 value * \param[out] *pch_spo2_valid - 1 if the calculated SpO2 value is valid * \param[out] *pn_heart_rate - Calculated heart rate value * \param[out] *pch_hr_valid - 1 if the calculated heart rate value is valid * * \retval None */ { uint32_t un_ir_mean ; int32_t k, n_i_ratio_count; int32_t i, n_exact_ir_valley_locs_count, n_middle_idx; int32_t n_th1, n_npks; int32_t an_ir_valley_locs[15] ; int32_t n_peak_interval_sum; int32_t n_y_ac, n_x_ac; int32_t n_spo2_calc; int32_t n_y_dc_max, n_x_dc_max; int32_t n_y_dc_max_idx, n_x_dc_max_idx; int32_t an_ratio[5], n_ratio_average; int32_t n_nume, n_denom ; // calculates DC mean and subtract DC from ir 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 ; // remove DC and invert signal so that we can use peak detector as valley detector for (k = 0 ; k < n_ir_buffer_length ; k++ ) an_x[k] = -1 * (pun_ir_buffer[k] - un_ir_mean) ; // 4 pt Moving Average 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; } // calculate threshold 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; // min allowed if( n_th1 > 60) n_th1 = 60; // max allowed for ( k = 0 ; k < 15; k++) an_ir_valley_locs[k] = 0; // since we flipped signal, we use peak detector as vSalley detector maxim_find_peaks( an_ir_valley_locs, &n_npks, an_x, BUFFER_SIZE, n_th1, 4, 15 );//peak_height, peak_distance, max_num_peaks 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; // unable to calculate because # of peaks are too small *pch_hr_valid = 0; } // load raw value again for SPO2 calculation : 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] ; } // find precise min near an_ir_valley_locs n_exact_ir_valley_locs_count = n_npks; //using exact_ir_valley_locs , find ir-red DC andir-red AC for SPO2 calibration an_ratio //finding AC/DC maximum of raw n_ratio_average = 0; n_i_ratio_count = 0; for(k = 0; k < 5; k++) an_ratio[k] = 0; for (k = 0; k < n_exact_ir_valley_locs_count; k++) { if (an_ir_valley_locs[k] > BUFFER_SIZE ) { *pn_spo2 = -999 ; // do not use SPO2 since valley loc is out of range *pch_spo2_valid = 0; return; } } // find max between two valley locations // and use an_ratio betwen AC compoent of Ir & Red and DC compoent of Ir & Red for SPO2 for (k = 0; k < n_exact_ir_valley_locs_count - 1; k++) { n_y_dc_max = -16777216 ; n_x_dc_max = -16777216; if (an_ir_valley_locs[k + 1] - an_ir_valley_locs[k] > 3) { for (i = an_ir_valley_locs[k]; i < an_ir_valley_locs[k + 1]; i++) { if (an_x[i] > n_x_dc_max) { n_x_dc_max = an_x[i]; n_x_dc_max_idx = i; } if (an_y[i] > n_y_dc_max) { n_y_dc_max = an_y[i]; n_y_dc_max_idx = i; } } 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]); //red 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; // subracting linear DC compoenents from raw n_x_ac = (an_x[an_ir_valley_locs[k + 1]] - an_x[an_ir_valley_locs[k] ] ) * (n_x_dc_max_idx - an_ir_valley_locs[k]); // ir n_x_ac = an_x[an_ir_valley_locs[k]] + n_x_ac / (an_ir_valley_locs[k + 1] - an_ir_valley_locs[k]); n_x_ac = an_x[n_y_dc_max_idx] - n_x_ac; // subracting linear DC compoenents from raw n_nume = ( n_y_ac * n_x_dc_max) >> 7 ; //prepare X100 to preserve floating value n_denom = ( n_x_ac * n_y_dc_max) >> 7; if (n_denom > 0 && n_i_ratio_count < 5 && n_nume != 0) { an_ratio[n_i_ratio_count] = (n_nume * 100) / n_denom ; //formular is ( n_y_ac *n_x_dc_max) / ( n_x_ac *n_y_dc_max) ; n_i_ratio_count++; } } } // choose median value since PPG signal may varies from beat to beat maxim_sort_ascend(an_ratio, n_i_ratio_count); n_middle_idx = n_i_ratio_count / 2; if (n_middle_idx > 1) n_ratio_average = ( an_ratio[n_middle_idx - 1] + an_ratio[n_middle_idx]) / 2; // use median else n_ratio_average = an_ratio[n_middle_idx ]; if( n_ratio_average > 2 && n_ratio_average < 184) { n_spo2_calc = uch_spo2_table[n_ratio_average] ; *pn_spo2 = n_spo2_calc ; *pch_spo2_valid = 1;// float_SPO2 = -45.060*n_ratio_average* n_ratio_average/10000 + 30.354 *n_ratio_average/100 + 94.845 ; // for comparison with table } else { *pn_spo2 = -999 ; // do not use SPO2 since signal an_ratio is out of range *pch_spo2_valid = 0; } } void maxim_find_peaks( int32_t *pn_locs, int32_t *n_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height, int32_t n_min_distance, int32_t n_max_num ) /** * \brief Find peaks * \par Details * Find at most MAX_NUM peaks above MIN_HEIGHT separated by at least MIN_DISTANCE * * \retval None */ { maxim_peaks_above_min_height( pn_locs, n_npks, pn_x, n_size, n_min_height ); maxim_remove_close_peaks( pn_locs, n_npks, pn_x, n_min_distance ); *n_npks = min( *n_npks, n_max_num ); } void maxim_peaks_above_min_height( int32_t *pn_locs, int32_t *n_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height ) /** * \brief Find peaks above n_min_height * \par Details * Find all peaks above MIN_HEIGHT * * \retval None */ { int32_t i = 1, riseFound = 0, holdOff1 = 0, holdOff2 = 0, holdOffThresh = 4; *n_npks = 0; while (i < n_size - 1) { if (holdOff2 == 0) { if (pn_x[i] > n_min_height && pn_x[i] > pn_x[i - 1]) // find left edge of potential peaks { riseFound = 1; } if (riseFound == 1) { if ((pn_x[i] < n_min_height) && (holdOff1 < holdOffThresh)) // if false edge { riseFound = 0; holdOff1 = 0; } else { if (holdOff1 == holdOffThresh) { if ((pn_x[i] < n_min_height) && (pn_x[i - 1] >= n_min_height)) { if ((*n_npks) < 15 ) { pn_locs[(*n_npks)++] = i; // peak is right edge } holdOff1 = 0; riseFound = 0; holdOff2 = 8; } } else { holdOff1 = holdOff1 + 1; } } } } else { holdOff2 = holdOff2 - 1; } i++; } } void maxim_remove_close_peaks(int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_min_distance) /** * \brief Remove peaks * \par Details * Remove peaks separated by less than MIN_DISTANCE * * \retval None */ { int32_t i, j, n_old_npks, n_dist; /* Order peaks from large to small */ maxim_sort_indices_descend( pn_x, pn_locs, *pn_npks ); for ( i = -1; i < *pn_npks; i++ ) { n_old_npks = *pn_npks; *pn_npks = i + 1; for ( j = i + 1; j < n_old_npks; j++ ) { n_dist = pn_locs[j] - ( i == -1 ? -1 : pn_locs[i] ); // lag-zero peak of autocorr is at index -1 if ( n_dist > n_min_distance || n_dist < -n_min_distance ) pn_locs[(*pn_npks)++] = pn_locs[j]; } } // Resort indices int32_to ascending order maxim_sort_ascend( pn_locs, *pn_npks ); } void maxim_sort_ascend(int32_t *pn_x, int32_t n_size) /** * \brief Sort array * \par Details * Sort array in ascending order (insertion sort algorithm) * * \retval None */ { int32_t i, j, n_temp; for (i = 1; i < n_size; i++) { n_temp = pn_x[i]; for (j = i; j > 0 && n_temp < pn_x[j - 1]; j--) pn_x[j] = pn_x[j - 1]; pn_x[j] = n_temp; } } void maxim_sort_indices_descend( int32_t *pn_x, int32_t *pn_indx, int32_t n_size) /** * \brief Sort indices * \par Details * Sort indices according to descending order (insertion sort algorithm) * * \retval None */ { int32_t i, j, n_temp; for (i = 1; i < n_size; i++) { n_temp = pn_indx[i]; for (j = i; j > 0 && pn_x[n_temp] > pn_x[pn_indx[j - 1]]; j--) pn_indx[j] = pn_indx[j - 1]; pn_indx[j] = n_temp; } } 

代码和资料链接:

通过网盘分享的文件:MAX30102心率血氧传感器资料
链接: https://pan.baidu.com/s/1u_J5HX3-fc0obVjtVtk0Vg?pwd=wgti 提取码: wgti 
--来自百度网盘超级会员v7的分享

Could not load content