目录
5.重置MAX30102传感器,读取MAX30102的设备ID,清空MAX30102 FIFO缓冲区,启用或禁用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库(文中末尾会放网盘链接,包含完整代码和项目)
包含
- 配置SDA引脚为输出方向
- 配置SDA引脚为输入方向
- 初始化I²C总线对应的GPIO引脚
- 生成I²C总线起始信号
- 生成I²C总线停止信号
- 主机产生一个应答信号(ACK)
- 主机产生一个非应答信号(NACK)
- 等待从机应答信号
- 向I²C总线发送一个字节数据
- 从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传感器
初始化中会
- 配置中断使能寄存器
- 配置FIFO寄存器
- 模式配置
- SpO2配置
- 配置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, ®_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)
心率测量基于以下原理:
- 心脏跳动导致动脉血管扩张和收缩
- 血管容积的周期性变化影响光的反射和吸收
- 光电二极管捕获这种周期性的光强变化,产生PPG信号
- 通过数字滤波去除噪声和运动伪影
计算处理后的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.心率计算原理:
-
信号预处理:
- 计算DC均值并从原始信号中移除
- 反转信号以便使用峰值检测器作为谷值检测器
- 应用4点移动平均滤波平滑信号
-
峰值检测:
- 设定阈值(通常在30-60之间)
- 检测高于阈值的信号峰值
- 移除间距太近的峰值
-
心率计算:
- 计算相邻峰值间的时间间隔
- 应用公式:
心率(BPM) = (采样频率 * 60) / 峰值间隔平均值
- 例如:若采样率为50Hz,峰值间隔为50个样本,则心率为60BPM
4.血氧饱和度(SpO2)计算
血氧计算基于不同波长光在含氧和不含氧血红蛋白中吸收率的差异:
-
比率计算:
- 对于每对谷值之间:
- 找出红光和红外信号的DC最大值
- 计算AC分量(交流部分)
- 计算比率:
R = (RED_AC * IR_DC) / (IR_AC * RED_DC)
- 对于每对谷值之间:
-
SpO2值查表:
- 将计算出的比率R排序并取中值
- 通过查找表转换为SpO2值
- 实际公式近似为:
SpO2 ≈ -45.060 * R² + 30.354 * R + 94.845
5.数据过滤与平均
为提高测量可靠性,代码实现了多种数据处理机制:
-
有效性验证:
- 心率必须在60-150范围内才被视为有效
- 血氧必须大于80%才被视为有效
-
连续验证:
- 需要连续5个有效样本才确认为有效测量
- 与历史数据比较以排除异常波动
-
多级平均:
- 使用16元素环形缓冲区保存历史数据
- 根据有效数据量采用不同平均策略:
- 2-3个样本:取最近2个平均
- 4-7个样本:取最近4个平均
- 8-15个样本:取最近8个平均
- 16个样本:全部16个平均
-
超时处理:
- 连续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的分享