STM32单片机MAX30102血氧检测模块全网不同的教学

 对于MAX30102这款模块其实网上已经有很多教学,但是很可惜,这些教学都千篇一律,代码例程更像是清朝的代码,个个代码都长得一样,连注释都一样。我想分享一些对于新手而言友好一点的教学,有用的,不一样的教学。因此,我做出了一个违背祖宗的决定!!!废话不多说直接开整。

我们这次的目标很简单,能建立通信, 能读出数据,能识别手指。

我们一个一个步骤的来

1.建立通信

这款模块使用的是 I2C通信协议,对于I2C的时序我相信大家都已经了解,但我这里也给出我的 I2C代码(默默感谢一下浩哥),这里的I2C使用的是软件模拟。

#define RCC_IIC_SCL   RCC_AHB1Periph_GPIOE //端口时钟
#define IIC_SCL_PORT  GPIOE				   //端口号
#define IIC_SCL 	  GPIO_Pin_3		   //引脚

#define RCC_IIC_SDA   RCC_AHB1Periph_GPIOE
#define	IIC_SDA_PORT  GPIOE
#define	IIC_SDA 	  GPIO_Pin_4

//io操作

#define IIC_SCL_H     GPIO_SetBits(IIC_SCL_PORT,IIC_SCL);  //SCL置1
#define	IIC_SCL_L     GPIO_ResetBits(IIC_SCL_PORT,IIC_SCL);//SCL置0

#define IIC_SDA_H     GPIO_SetBits(IIC_SDA_PORT,IIC_SDA);  //SDA置1
#define	IIC_SDA_L     GPIO_ResetBits(IIC_SDA_PORT,IIC_SDA);//SDA置0

#define READ_SDA 	  GPIO_ReadInputDataBit(IIC_SDA_PORT,IIC_SDA)//读取SDA输入引脚电平

void IICx_GPIO_Init(void);
void IIC_SDA_OUT(void);
void IIC_SDA_IN(void);
void IIC_Start(void);
void IIC_Stop(void);
void IIC_ACK(void);
void IIC_NACK(void);
void IIC_SendByte(uint8_t data);
uint8_t IIC_ReadByte(uint8_t ack);
uint8_t IIC_WaitACK(void);
uint16_t BH1750_ReadData(void);




void IICx_GPIO_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	
	//GPIO时钟
	RCC_AHB1PeriphClockCmd(RCC_IIC_SCL, ENABLE);
	RCC_AHB1PeriphClockCmd(RCC_IIC_SDA, ENABLE);
	
	//SCL GPIO初始化
	GPIO_InitStructure.GPIO_Pin=IIC_SCL; 
	GPIO_InitStructure.GPIO_Mode= GPIO_Mode_OUT; 
	GPIO_InitStructure.GPIO_OType=GPIO_OType_OD; //开漏输出
	GPIO_InitStructure.GPIO_Speed=GPIO_High_Speed;
	
	GPIO_Init(IIC_SCL_PORT, &GPIO_InitStructure);
	
	//SDA GPIO初始化
	GPIO_InitStructure.GPIO_Pin=IIC_SDA; 
	GPIO_InitStructure.GPIO_Mode= GPIO_Mode_OUT; 
	GPIO_InitStructure.GPIO_OType=GPIO_OType_OD; //开漏输出
	GPIO_InitStructure.GPIO_Speed=GPIO_High_Speed;
	
	GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure);
	
	IIC_SCL_H;
	IIC_SDA_H;
}

//配置SDA数据线为输出
void IIC_SDA_OUT(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	
	//SDA GPIO初始化
	GPIO_InitStructure.GPIO_Pin=IIC_SDA; 
	GPIO_InitStructure.GPIO_Mode= GPIO_Mode_OUT; 
	GPIO_InitStructure.GPIO_OType=GPIO_OType_OD; //开漏输出
	GPIO_InitStructure.GPIO_Speed=GPIO_High_Speed;
	
	GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure);
}


//配置SDA数据线为输入
void IIC_SDA_IN(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	
	//SDA GPIO初始化
	GPIO_InitStructure.GPIO_Pin=IIC_SDA; 
	GPIO_InitStructure.GPIO_Mode= GPIO_Mode_IN; 
	GPIO_InitStructure.GPIO_Speed=GPIO_High_Speed;
	GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP; //上拉,仅对输入有效
	
	GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure);
}

//IIC时序信号

//IIC开始信号
void IIC_Start(void)
{
	IIC_SDA_OUT();
	
	IIC_SCL_H;
	IIC_SDA_H;
	Delay_us(4);//让高电平保持稳定
	
	IIC_SDA_L;  // 拉低SDA
	Delay_us(4);
	IIC_SCL_L;	//钳住IIC总线,准备发送或者接收数据	
}

//IIC停止信号
void IIC_Stop(void)
{
	IIC_SDA_OUT();
	IIC_SCL_H;
	IIC_SDA_L;  //SAD低
	Delay_us(4);//等待时序稳定
	IIC_SDA_H;  //SDA高	
}

//IIC应答
void IIC_ACK(void)
{
	IIC_SDA_OUT();
	IIC_SCL_L;  //在SCL低电平的时候,SDA可以进行数据切换(1和0切换)
	IIC_SDA_L;  //SDA低电平表示应答型号
	Delay_us(1);//让电平稳定
	
	IIC_SCL_H;  //拉高SCL,表示此时SDA的数据有效
	Delay_us(1);
	IIC_SCL_L;  //拉低SCL,表示SCL一个周期结束
}

//IIC非应答
void IIC_NACK(void)
{
	IIC_SDA_OUT();
	IIC_SCL_L;  //在SCL低电平的时候,SDA可以进行数据切换(1和0切换)
	IIC_SDA_H;  //SDA高电平表示非应答型号
	Delay_us(1);//让电平稳定
	
	IIC_SCL_H;  //拉高SCL,表示此时SDA的数据有效
	Delay_us(1);
	IIC_SCL_L;  //拉低SCL,表示SCL一个周期结束
}

//IIC发送一个字节
void IIC_SendByte(uint8_t data)
{
	IIC_SDA_OUT();
	IIC_SCL_L; //在SCL低电平的时候,SDA可以进行数据切换(1和0切换)
	
	uint8_t i=0;
	for(i=0;i<8;i++)
	{
		if((data&0x80)>0)   //0x80  1000 0000  
		{
			IIC_SDA_H;
		}
		else
		{
			IIC_SDA_L;
		}
		IIC_SCL_H;  //拉高SCL ,数据有效
		Delay_us(1); //延时,将数据发送出去
		
		IIC_SCL_L;
		Delay_us(1);
		data<<=1;
	}
}


//IIC读取一个字节
uint8_t IIC_ReadByte(uint8_t ack) //ack 1 应答 0 非应答
{
	IIC_SDA_IN();
	
	uint8_t i=0;
	uint8_t receive=0;
	
	for(i=0;i<8;i++)
	{
		IIC_SCL_L;
		Delay_us(1);
		IIC_SCL_H;
		
		receive<<=1; 
		if(READ_SDA)
		{
			receive++; 
		}
		Delay_us(1);
		
	}
	if(ack)
		IIC_ACK();
	else
		IIC_NACK();
	
	return receive;
	
}


//IIC等待应答 ,返回0表示应答,返回1表示非应答
uint8_t IIC_WaitACK(void)
{
	IIC_SDA_IN();
	
	uint8_t temp=0;
	IIC_SDA_H;
	Delay_us(1);
	IIC_SCL_H;
	Delay_us(1);
	while(READ_SDA)
	{
		temp++;
		if(temp>250)
		{
			IIC_Stop();
			return 1;
		}
	}
	IIC_SCL_L;
	return 0;
	
}

我相信这段代码大家拿去只需要改一改引脚,直接拿去用,俩个字 无敌!

好了,以下进入正题。

问:在MAX30102模块的时序中,如何向一个寄存器里面写入命令呢?

        该如何封装代码呢?

答:和普通I2C向寄存器写值一样。

        需要知道:1.MAX30102的设备地址: 

                                #define I2C_WRITE_ADDR 0xAE
                                #define I2C_READ_ADDR 0xAF

咦?怎么是2个地址? 7位地址位+1位读写位 ,写的时候用上面的,读的时候用下面的

具体封装:


//Addr:寄存器地址  data:写入的数据
bool MAX30102_Write_REG(uint8_t addr, uint8_t data)
{
	IIC_Start();//起始信号

	IIC_SendByte(I2C_WRITE_ADDR);  //从机地址,并且写数据

	IIC_WaitACK() ;                //等待应答

	IIC_SendByte(addr);            //发送寄存器地址

    IIC_WaitACK() ;                //等待应答

	IIC_SendByte(data);            //写入数据

	IIC_WaitACK();                 //等待应答

	IIC_Stop();                    //停止
	
	return true;

}

问: 在MAX30102模块的时序中,如何从一个寄存器里面读出数据呢?

        在MAX30102模块的时序中,如何从一个寄存器里面读出数据呢?

        在MAX30102模块的时序中,如何从一个寄存器里面读出数据呢?

 为什么要重复三遍这个问题? 因为这个模块的读寄存器数据的时序有需要注意的地方!!!

        我觉得说的再多,不如一份清晰明了的代码实用,直接看我的封装!看我的注释

//addr:需要读的寄存器地址    返回值:从寄存器中读到的数据
uint8_t MAX30102_Read_REG(uint8_t addr)
{
	uint8_t temp = 0;                    //用来存放读到的数据,最后返回这个数据
	
	IIC_Start();                          //开始信号

	IIC_SendByte(I2C_WRITE_ADDR);         // 注意!!这里还是用【写】的设备地址命令!!!

	IIC_WaitACK();                        //等待应答

	IIC_SendByte(addr);                   //发送要读的寄存器地址

	IIC_WaitACK();                        //等待应答
	
	IIC_Start();//!!!!!!!!!!!!!一定一定要重新开始一遍!!!

	IIC_SendByte(I2C_READ_ADDR);          //这里再用【读】的命令,第一次用写,第二次用读

	IIC_WaitACK();                        //等待应答
	
	temp = IIC_ReadByte(0);               //读取一个字节 0代表不继续读了,继续读,填1

	IIC_Stop();                            //停止信号
	
	return temp;

}

仔细看中间一点一点要重新【开始信号】!!!!!!!!!!!

这个是对寄存器的宏定义

#define I2C_WRITE_ADDR 0xAE
#define I2C_READ_ADDR 0xAF

//register addresses
#define REG_INTR_STATUS_1 0x00
#define REG_INTR_STATUS_2 0x01
#define REG_INTR_ENABLE_1 0x02
#define REG_INTR_ENABLE_2 0x03
#define REG_FIFO_WR_PTR 0x04
#define REG_OVF_COUNTER 0x05
#define REG_FIFO_RD_PTR 0x06
#define REG_FIFO_DATA 0x07
#define REG_FIFO_CONFIG 0x08
#define REG_MODE_CONFIG 0x09
#define REG_SPO2_CONFIG 0x0A
#define REG_LED1_PA 0x0C
#define REG_LED2_PA 0x0D
#define REG_PILOT_PA 0x10
#define REG_MULTI_LED_CTRL1 0x11
#define REG_MULTI_LED_CTRL2 0x12
#define REG_TEMP_INTR 0x1F
#define REG_TEMP_FRAC 0x20
#define REG_TEMP_CONFIG 0x21
#define REG_PROX_INT_THRESH 0x30
#define REG_REV_ID 0xFE
#define REG_PART_ID 0xFF

问:需要配置哪些寄存器呢?

网上关于这个的资料倒是很多,无非哪个bit位代表着什么含义,看我配置了哪些,对着数据手册上翻就明白了,当然,我注释也会写

bool MAX30102_init(void)
{
	  MAX30102_Write_REG(REG_INTR_ENABLE_1,0xc0); // 中断使能
	  MAX30102_Write_REG(REG_INTR_ENABLE_2,0x00);
	  MAX30102_Write_REG(REG_FIFO_WR_PTR,0x00) ; //样本缓冲区 地址都写0
	  MAX30102_Write_REG(REG_OVF_COUNTER,0x00);  //OVF_COUNTER[4:0] 手册上说直接给0
	  MAX30102_Write_REG(REG_FIFO_RD_PTR,0x00); //FIFO_RD_PTR[4:0] 手册上说直接给0
	  MAX30102_Write_REG(REG_FIFO_CONFIG,0x5f);  //每相邻的4个样本取一个平均值,选择集满15个数据就置起 缓冲区满了的标志位
	  MAX30102_Write_REG(REG_MODE_CONFIG,0x03);   //选择血氧饱和度 红灯和红外
	  MAX30102_Write_REG(REG_SPO2_CONFIG,0x27);  // 量程 = 4096nA, 速率(100 Hz), 精度18bit (400uS)
	  
	  MAX30102_Write_REG(REG_LED1_PA,0x24);   //Choose value for ~ 7mA for LED1
	  MAX30102_Write_REG(REG_LED2_PA,0x24);   // Choose value for ~ 7mA for LED2
	  MAX30102_Write_REG(REG_PILOT_PA,0x7f);   // Choose value for ~ 25mA for Pilot LED

} 

有一点需要注意 细看    

MAX30102_Write_REG(REG_FIFO_CONFIG,0x5f);  这个寄存器

有些人红灯不亮 ,看一下这个 寄存器配置对了没有

 MAX30102_Write_REG(REG_MODE_CONFIG,0x03);   //选择血氧饱和度 红灯和红外

手册上就是这样推荐配置的,比如 3,4,5条,手册说直接给0,其实就是对写指针,读指针,溢出个数啥的 进行清0或者初始化为0,指向地址首位,大概就是这样。

模块复位。这个不多解释了,都知道这个啥意思

//复位函数
bool MAX30102_RESET(void)
{
	MAX30102_Write_REG(REG_MODE_CONFIG,0x40);
}

 到这 如果你读取寄存器 0XFF,能读到 0X15,代表通信成功的!

还有个中断引脚,啥意思呢,就是数据准备好了,发个信号给你,你可以去读了,就是这个意思,新手可能会问 啊啊啊啊啊这个重要吗,是不是不弄就不能读数据了 我看别人都配置了,我只能说,见仁见智,不要害怕这些,学会不用中断的方式,那么对于其他,手到擒来。这里就先不用中断,很多方法都行,什么轮询啊,其实都一样。你要不要中断,你该读数据你就读,无非就是判断 到底有没有已经准备好了的数据我可以去读呢?到底要怎么读呢?里面的逻辑是什么样子的呢?

我们先来看官方手册上的一张图 

解释一下这张图

在SpO2模式下:现在设置的就是spo2模式

每个样本由两个数据三元组组成,每个三元组占据3个字节

(第17位后面就没有数据了,也就是每个三元组只有18个bit有数据,后面为无效字节,但是也占空间啊)
 一个样本[6字节] = 【red :字节1+字节2+字节3】+ 【ir:字节1+字节2+字节3】

 因此每个样本总共占据6个字节,需要6个字节大小存储

数据是以样本的形式 存入一个缓冲区 ,这个缓存区最多可以容纳32个样本数据

既然最多可以容纳32个样本,可是我有很多数据这怎么够存的呢?

答案就是 【读】,读完一个就会少一个样本,就有位置存入新的样本数据,如果样本满了,而没有去读,根据我们寄存器的配置,数据会从头覆盖。也可以设置为不覆盖,一直等空位置,也就是有数据被读走,有新位置存,我就存进去。那数据不更新怎么理解呢?数据满了溢出了,那就溢出吧,新来的数据不要了,然后会有一个寄存器负责记录你溢出了多少个数据。

如果我要读300个数据,虽然缓存区只能存最多32个(这个好像自己也可以设置),但是我可以多读几次啊,这32个我先读走,后面又来32个满了,我继续读,那么现在我就有64 个数据了,那我继续读,把读到的数据存起来,存满300个即可

【读数据】一次读一个样本的数据,也就是6个字节,前3个字节是红光数据,后3个是红外数据

能不能一次读32个样本的数据???可以但没必要,因为这会涉及到直接写地址,自己偏移,如果是每次读一个样本的数据,那么系统会自己偏移,根据手册,强烈不建议我们对地址进行写操作。不懂的记住这个:一次读一个样本的数据,读完一个样本,模块内部会自己指向下一个样本

编写    读取一个样本数据的代码

//Data:用来存储数据的数组,Data[2]
void MAX30102_Read_FIFO(uint32_t Data[])
{

	char Buf[6];
	uint32_t RedData, IrData;

	IIC_Start();·                    //开始
	IIC_SendByte(I2C_WRITE_ADDR); 	//从机地址,并且写数据
	IIC_WaitACK();                   
	IIC_SendByte(REG_FIFO_DATA);    //  REG_FIFO_DATA:数据寄存器,数据就存在这里
	IIC_WaitACK();
	
	IIC_Start();                    //记住这里也是一样,要重新开始!!!!
	IIC_SendByte(I2C_READ_ADDR); 
	IIC_WaitACK();
	
	Buf[0] = IIC_ReadByte(1);       //读取一个字节,1代表继续读
	Buf[1] = IIC_ReadByte(1);
	Buf[2] = IIC_ReadByte(1);

	Buf[3] = IIC_ReadByte(1);
	Buf[4] = IIC_ReadByte(1);
	Buf[5] = IIC_ReadByte(0);        //0代表不读了,一共读了6个字节 也就是一个样本数据
	
	IIC_Stop();                    
	
	Data[0] = ( (uint32_t)Buf[0]<<16 | (uint32_t)Buf[1]<<8 | (uint32_t)Buf[2] ) & 0X3FFFF;                             //数据整合一下,应该都懂的
	
	Data[1] = ( (uint32_t)Buf[3]<<16 | (uint32_t)Buf[4]<<8 | (uint32_t)Buf[5] ) & 0x3FFFF;
	
}

 

现在有了 初始化函数 已经可以和模块建立通信,也有了读取数据的函数,可以读取想要的数据,问题来了:我该什么时候读呢?

答:有数据的时候就读

问:我怎么知道有没有数据呢?

答:看缓存区里面有多少个数据,不为0就代表有数据

问:怎么知道缓存区里面有多少数据呢?

不用再答了,直接上代码:

//计算多少个样本数据
uint8_t MAX30102_FIFO_DataSize(void)
{
	uint8_t Size = 0;
	uint8_t Addr_Last, Addr_First;
	
	
	Addr_Last = MAX30102_Read_REG(REG_FIFO_WR_PTR);//最后数据指针指向的地址
	Addr_First = MAX30102_Read_REG(REG_FIFO_RD_PTR);//一开始数据指针指向的地址
	
	
	if(Addr_Last == Addr_First)
	{
		return 0;
	
	}
	else 
	{
		Size =  Addr_Last - Addr_First;
		if(Size < 0)
		{
			Size += 32;
		}
		else
		{
			return Size;
		}
	}
	
}

把【指针指向的地址】这东西 想像成数组下标 ,一开始数据下标是 【0】没有数据,现在数据下标是【16】,数据下标一样就代表没有数据可以读,不一样就有数据可以读

问:这个 +32 是啥东东?  

答:FIFO是啥东东呢?先来先服务,先进先出的队列 啊, 头尾相连的队列,你跑步,发现第一名在你屁股后面,是不是说明人家已经套你一圈了, 而缓存区最大可用容纳32个数据,所以要 +32,人家第一名比你多跑一圈的道理。

我们现在实现 读100个数据存起来,根据这些数据 判断手指有没有放在上面

uint32_t Red_Ir_Data[200][2];  //数据存进这里
uint8_t hand_flag = 0;         //判断是不是手放在上面
uint8_t quitData_flag = 0;

void hand(void)
{
	uint16_t index = 0;    //存入数组里面的数组下标
	uint32_t Data[2];      //存放一个整合的数据
	int16_t NumBytes;      //缓存区里面有多少个样本数据

	while(index < 100)
	{
		NumBytes = MAX30102_FIFO_DataSize();  //计算缓存区里面有多少个样本数据


		
		while(NumBytes > 0  && index < 100)  //还有样本数据,且存起来的数小于100个
		{
			MAX30102_Read_FIFO(Data);         //取一个样本的数据
			
			//存储起来
			Red_Ir_Data[index][0] = Data[0];    //存起来
			Red_Ir_Data[index][1] = Data[1];    //存起来
			
			index++;                            //存一个,个数就加一个
			NumBytes--;		                    //读一个样本数据就少一个
		}
		delay_ms(10);                           //读取的间隔不要太快了
	}

		if(index >= 100)                        //存够100个了
		{
			u8 i;
			uint32_t Red = 0, Ir = 0;
			for(i=0; i<100; i++)
			{
				Red += Red_Ir_Data[i][0];
				Ir  += Red_Ir_Data[i][1];		
			}
			Red /= 100;
			Ir  /= 100;                     //这里就是把数据加起来 求平均值
			
			if(Red < 50000 || Ir <50000 )  //如果平均值 <50000就说明 没有手在上面
			{
				hand_flag = 0;		
			}
			else
			{
				hand_flag = 1;         //有手在上面 别问 50000这个数是怎么来的 ,我没法回答你
			}
			if( hand_flag == 0)
			{
				printf("NO\r\n");        //打印一下,有手还是没手
			}
			else
			{
				printf("YES\r\n");
			}
			
		}

}

来看看main函数中怎么调用的

main.c

int main(void)

	
	MAX30102_INT_GPIO_Config();   //模拟I2c 引脚初始化
	MAX30102_RESET();             //复位一下
	MAX30102_init();              //初始化
			

	while(1)
	{
		
		delay_ms(30);
		
		hand();                   //判断有没有手的函数
	
				
	}
	
}

手盖在上面,又拿走,看打印效果

后续思路:一旦检测到手在上面,标志位为1,那我们就开始 真正读取数据,然后对数据进行分析,得出血氧心率什么的,不是手,当然就不用做出分析,还是很简单的吧!!!!

  • 24
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值