对于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,那我们就开始 真正读取数据,然后对数据进行分析,得出血氧心率什么的,不是手,当然就不用做出分析,还是很简单的吧!!!!