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血氧心率模块讲解一:测量原理,硬件介绍及寄存器详细解析-CSDN博客

这是第二篇讲解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血氧心率模块讲解一:测量原理,硬件介绍及寄存器详细解析-CSDN博客

/**
 * @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血氧心率模块讲解一:测量原理,硬件介绍及寄存器详细解析-CSDN博客

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的分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值