通过编写MAX30102驱动来了解IIC协议和寄存器操作
内容
IIC详解
一、IIC 简介
I2C(Inter-Integrated Circuit) 是内部整合电路的称呼, 是一种串行通讯总线, 使用多主从架构, 由飞利浦公司在1980年为了让主板、 嵌入式系统或手机用以连接低速周边装置而发展。 I2C的正确读法为"I-squared-C" , 而"I-two-C"则是另一种错误但被广泛使用的读法, 在中国则多以"I方C"称之。 I2C 总线支持任何IC 生产过程(NMOS CMOS、 双极性) 。 两线――串行数据(SDA) 和串行时钟 (SCL) 线在连接到总线的器件间传递信息。 每个器件都有一个唯一的地址识别(无论是微控制器——MCU、 LCD 驱动器、 存储器或键盘接口) , 而且都可以作为一个发送器或接收器(由器件的功能决定) 。
在 CPU 与被控 IC 之间、 IC 与 IC 之间进行双向传送, 高速 IIC 总线一般可达 400kbps 以上。
I2C 总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答
1、开始信号: SCL 为高电平时, SDA 由高电平向低电平跳变,开始传送数据。
2、结束信号: SCL 为高电平时, SDA 由低电平向高电平跳变,结束传送数据。
3、应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。 CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号, CPU 接收到应答信号后,根据实际情况做出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。
这些信号中,起始信号是必需的,结束信号和应答信号, 都可以不要。
IIC 总线时序图
IIC的特征
1、 只要求两条总线线路: 一条串行数据线SDA(因此是半双工的), 一条串行时钟线SCL;
2、 每个连接到总线的器件都可以通过唯一的地址和一直存在的简单的主机/从机关系软件设定地址, 主机可以作为主机发送器或主机接收器;
3、 它是多主机总线, 如果两个或更多主机同时初始化, 数据传输可以通过冲突检测和仲裁防止数据被破坏;
4、 串行的8 位双向数据传输位速率在标准模式下可达100kbit/s, 快速模式下可达400kbit/s, 高速模式下可达3.4Mbit/s;
5、 连接到相同总线的IC 数量只受到总线的最大电容400pF 限制
IIC的从地址
IIC从地址有3种类型:分别是7位,8位和10位。产生这么多类型的原因是厂商采用的不同的地址约定。
7位寻址
在7位寻址过程中,从机地址在启动信号后的第一个字节开始传输,该字节的前7位为从机地址,第8位为读写位,其中0表示写,1表示读,如下图所示。
I2C总线规范规定,标准模式I2C,从机地址为7位长,其次是读/写位。
第一个字节的头7 位组成了从机地址, 最低位(LSB) 是第8 位, 它决定了传输的方向。
第一个字节的第8位是“0” , 表示主机会写信息到被选中的从机;
“1” 表示主机会向从机读信息, 当发送了一个地址后, 系统中的每个器件都在起始条件后将头7 位与它自己的地址比较, 如果一样, 器件会判定它被主机寻址, 至于是从机接收器还是从机发送器, 都由R/W 位决定。
任何I2C设备都必须遵循这个标准,USB2XXX传输的从机地址即为这7bit地址,不包含读写位,读写位会根据不同的函数自动添加进去。
8位地址
一些厂商在提供从机地址的时候说的是包含了读写位的8bit地址,比如他说写地址为0x92,读地址为0x93,如下图所示。
如果是8位寻址的情况,需要将这个地址的前7bit提取出来,然后传入USB2XXX的接口函数即可,比如为0x49。
判断厂商提供的地址是7bit模式地址还是8bit地址模式的地址的方式:7bit地址模式下,地址的取值范围在0x07到0x78之间,若超过了这个范围,那么这个地址可能就是8bit地址。
10位寻址
I2C总线的10bit寻址和7bit寻址是兼容的,这样就可以在同一个总线上同时使用7bit地址和10bit地址模式的设备,在进行10bit地址传输时,第一字节是一个特殊的保留地址来指示当前传输的是10bit地址。
在使用USB2XXX传输10bit地址模式的时候,只需要在初始化的时候配置为10bit地址模式(由第一个字节设置),然后再调用读写数据函数的时候传入正确的10bit地址即可。
四、保留地址
I2C规范保留了两组和8个地址,1111XXX和0000XXX。这些地址用于特殊用途。下表已被取自 I2C规范(2000年)。
五、IIC的连接
1、IIC可以接多个主设备,多个从设备(外围 设备)。如下图,存在多个主机、多个从机。
2、当多主机会产生总线裁决问题。当多个主机同时想占用总线时,企图启动总线传输数据,就叫做总线竞争。I2C通过总线仲裁,以决定哪台主机控制总线
3、上拉电阻一般在4.7k~10k之间,默认拉高。
六、IIC总线最多可以挂多少个设备
由IIC地址决定,8位地址,减去1位广播地址,是7位地址,2^7=128,但是地址0x00不用,那就是127个地址, 所以理论上可以挂127个从器件。
但是IIC协议没有规定总线上device最大数目,但是规定了总线电容不能超过400pF。管脚都是有输入电容的,PCB上也会有寄生电容,所以会有一个限制。实际设计中经验值大概是不超过8个器件。
规定电容大小的原因:
IIC的OD(漏极开路)要求外部有电阻上拉,电阻和总线电容产生了一个RC延时效应,电容越大信号的边沿就越缓,有可能带来信号质量风险。
传输速度越快,信号的窗口就越小,上升沿下降沿时间要求更短更陡峭,所以RC乘积必须更小。
七、IIC实现的方式
IIC实现的方式主要有两种,硬件IIC和软件模拟IIC。
软件IIC是程序员使用程序控制SCL,SDA线输出高低电平,模拟i2c协议的时序。一般较硬件IIC稳定,虽然程序较为繁琐,但不难。
硬件IIC程序员只要调用IIC的控制函数即可,不用直接的去控制SCL,SDA高低电平的输出。但是有些单片机的硬件IIC不太稳定,调试问题较多。
硬件IIC、软件模拟IIC的区别
IIC协议编程实现
一、IIC连接实物示意图
二、IIC协议程序编写的要点
1、空闲状态
2、开始信号
3、停止信号
4、应答信号
5、数据的有效位
6、数据传输
三、IIC驱动编写
1、硬件准备
此处使用FPGA实验室自制野生CH32F103开发板,使用的IO口为C11、C12。由于使用的IO口并不是自带硬件IIC口,所以在此我们使用软件模拟IIC传输。(没有硬件的同学也可以继续看下去,协议的实现与硬件没有太大关系)
2、程序编写
由上文得知IIC协议程序编写的要点:下面我们来依次实现
第一步、首先我们先来初始化一下IO口(只是理解IIC协议原理的同学,可直接跳过)
1 void IIC_Init(void)
2 {
3 GPIO_InitTypeDef GPIO_Initure;
4 RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC, ENABLE ); //使能5 GPIOC时钟
6 //PC11,12初始化设置
7 GPIO_InitStructure.Pin= GPIO_Pin_12| GPIO_Pin_11;
8 GPIO_InitStructure.Mode= GPIO_Mode_Out_PP; //推挽输出
9 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //高速
10 GPIO_Init(GPIOC, &GPIO_InitStructure);
11 IIC_SDA=1;
12 IIC_SCL=1;
13 }
此处初始化需注意要两个IO口,因为IIC协议设定SDA=1,SCL=1为空闲状态。此处使用的是标准库库,对IO口的结构体定义如下:
1 typedef struct
2 {
3 uint16_t GPIO_Pin;
4 GPIOSpeed_TypeDef GPIO_Speed;
5 GPIOMode_TypeDef GPIO_Mode;
6 } GPIO_InitTypeDef;
第二步、对SDA、SCL进行宏定义(只是理解IIC协议原理的同学,可直接跳过)
1 //IO操作
2 #define IIC_SCL PCout(12) //SCL
3 #define IIC_SDA PCout(11) //SDA
4 #define READ_SDA PCin(11) //输入SDA
第三步、对各个要点进行实现
1、空闲状态
无数据发送接收时
1 void IIC_Leisure(void)
2 {
3 IIC_SDA=1;
4 IIC_SCL=1;
5 }
2、开始信号
根据时序图,可知,开始信号:SCL为高期间, SDA由高到低的跳变;
特别注意:启动信号是一种电平跳变时序信号,下面是程序编写:
1 void IIC_Start(void)
2 {
3 SDA_OUT(); /*设置C12为输出模式*/
4 IIC_SDA=1; /*拉高保持空闲状态*/
5 IIC_SCL=1; /*拉高保持空闲状态*/
6 delay_us(4); /*延时保证电平稳定*/
7 IIC_SDA=0; /*当SCL为高时,SDA由高到低的跳变*/
8 delay_us(4); /*延时保证电平稳定*/
9 IIC_SCL=0; /*钳住I2C总线,准备发送或接收数据*/
10 }
3、停止信号
根据时序图,可知,开始信号:SCL为高期间, SDA由低到高的跳变;
特别注意:停止信号是一种电平跳变时序信号,同时此处有个小细节,如时序图,SDA只有在SCL变高时才能变换。下面是程序编写:
1 void IIC_Stop(void)
2 {
3 SDA_OUT(); /*设置C12为输出模式*/
4 IIC_SCL=0;
5 IIC_SDA=0;
6 delay_us(4);
7 IIC_SCL=1;
8 IIC_SDA=1;/*当SCL为高时,SDA由高到低的跳变*/ /*如出现电平不稳,9 也可在本句上方+一个延时*/
10 delay_us(4);
11 }
4、应答信号
根据我们此项目来说,我们是使用单片机读取MAX30102里的数据。故此处发送器为MAX30102,接收器为单片机。
发送器(MAX30102)每发送一个字节, 就在时钟脉冲第9个期间释放数据线,由接收器(单片机)反馈一个应答信号。
当应答信号为低电平时, 表示接收器已经成功地接收了该字节,规定为应答位(ACK)
当应答信号为高电平时, 表示接收器接收该字节失败。规定为非应答位(NACK)
(1) 产生应答信号
1 void IIC_Ack(void)
2 {
3 IIC_SCL=0;
4 SDA_OUT();
5 IIC_SDA=0;
6 delay_us(2);
7 IIC_SCL=1;
8 delay_us(2);
9 IIC_SCL=0;
10 }
(2)产生非应答信号
1 void IIC_NAck(void)
2 {
3 IIC_SCL=0;
4 SDA_OUT();
5 IIC_SDA=1;
6 delay_us(2);
7 IIC_SCL=1;
8 delay_us(2);
9 IIC_SCL=0;
10 }
(3)检测应答信号
1 u8 IIC_Wait_Ack(void)
2 {
3 u8 ucErrTime=0;
4 SDA_IN(); //SDA设置为输入
5 IIC_SDA=1;delay_us(1);
6 IIC_SCL=1;delay_us(1);
7 while(READ_SDA)
8 {
9 ucErrTime++;
10 if(ucErrTime>250)
11 {
12 IIC_Stop();
13 return 1;/*数据传送失败。检测为非应答信号*/
14 }
15 }
16 IIC_SCL=0;//时钟输出0
17 return 0; /*数据传送失败。检测为应答信号*/
18 }
5、数据传输、数据的有效性
(1)数据的有效性
I2C总线进行数据传送时, 时钟信号为高电平期间, 数据线上的数据必须保持稳定, 只有在时钟线上的信号为低电平期间, 数据线上的高电平或低电平状态才允许变化。
即: 数据在SCL的上升沿到来之前就需准备好。 并在在下降沿到来之前必须稳定。(如图示,SCL周期小于SDA周期,且被包含在内)
(2)数据传输
数据位的传输采用边沿触发。
在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据 。
写数据
1 //IIC发送一个字节//返回从机有无应答//1,有应答//0,无应答
2 void IIC_Send_Byte(u8 txd)
3 {
4 u8 t;
5 SDA_OUT();
6 IIC_SCL=0;/*拉低时钟开始数据传输*/
7 for(t=0;t<8;t++)
8 {
9 IIC_SDA=(txd&0x80)>>7;/*需要发送的数据经过和0x80的与运算之10 后,右移7位*/
11 txd<<=1;/*左移7位*/
12 delay_us(2); /*对延时是必须的*/
13 IIC_SCL=1;
14 delay_us(2); /*对延时是必须的*/
15 IIC_SCL=0;
16 delay_us(2); /*对延时是必须的*/
17 }
18 }
读数据
1 //读1个字节,ack=1时,发送ACK,ack=0,发送nACK
2 u8 IIC_Read_Byte(unsigned char ack)
3 {
4 unsigned char i,receive=0;
5 SDA_IN();/*SDA设置为输入*/
6 for(i=0;i<8;i++ )
7 {
8 IIC_SCL=0;
9 delay_us(2);
10 IIC_SCL=1;
11 receive<<=1;
12 if(READ_SDA)receive++;
13 delay_us(1);
14 }
15 if (!ack)
16 IIC_NAck();/*发送NACK*/
17 else
18 IIC_Ack(); /*发送ACK*/
19 return receive;
20 }
IIC协议实战项目
前面讲了IIC协议的介绍和IIC协议的编程实现,接下来我们来做一个关于IIC的小项目。
一、项目的实现功能:
使用STM32单片机用IIC协议对MAX30102进行数据的循环读取。
二、本章使用模块简介:
1、MAX30102模块简介
本章实验使用的是MAX30102传感器模块。实物图片如下:
MAX30102是一个集成的脉搏血氧仪和心率监测仪生物传感器的模块(芯片)。它集成了一个660nm红光LED、880nm红外光LED、光电检测器、光器件,以及带环境光抑制的低噪声电子电路。可通过软件关断模块,待机电流为零,实现电源始终维持供电状态,可运用于低功耗产品中。
芯片内部有3.3V-5.0V的LED电源和1.8V的逻辑电源,所以模块带有两路稳压电路,将5V电源分别转化为3.3V和1.8V;由于LED驱动电源的供电范围为3.3V-5.0V,3.3V稳压电路可省去。
原理图如下:
MAX30102用的脉搏测量方法为光电容积法(PPG)
PPG介绍
血氧饱和度(英语:Oxygen saturation),或称血氧浓度,是指血中氧饱和血红蛋白相对于总血红蛋白(不饱和+饱和)的比例。人体需要并调节血液中氧气的非常精确和特定的平衡。 人体的正常动脉血氧饱和度为95-100%。 如果该水平低于90%,则被认为是低氧血症。
血氧的测量主要分为透射式和反射式。目前的主流是透射式。但是两者原理差不多,都是使用发光二极管(红光RED,红外IR,绿光GREEN和蓝光BLUE等)照射被测部位,然后使用一个光电二极管接收透射/反射的光线,将光信号转换为电信号。然后通过高精度的ADC测量反射回的电流大小,评估血液中的含氧量。
实时心率值可以反映一个人当时的心脏活动能力,进而从侧面衡量人体的健康状态。医院中测心率多采用心电图的方式,这在日常活动以及运动中是不便测量的,PRG( photoplethysmographic,光电描记法)脉搏波信号采用ILED光源和探测器为基础,测量经过人体血管和组织反射、吸收后的衰减光,描记出血管的搏动状态并测量脉搏波。由于PG信号获得简单,测量装置易于佩戴等特点逐渐成为非医院条件下测量血氧、脉搏及心率的主要方法。
光学心率传感器,如果带过上述那些智能手表或者智能手环的朋友来说也不算稀奇的事情。就拿AppleWatch来说,测量心率时底部的表盘会发出绿色的灯光,并且测量的时候手腕最好保持不动否侧会影响测量结果。
接下来将详细介绍光学心率测量的原理。如下两张图是光学心率传感器。图a是LED没有发光的时候中间是一个光敏二极管,图b是传感器的LED发光的时候。
那么为什么通过LED灯发光就能测量心率呢?
当LED光射向皮肤,透过皮肤组织反射回的光被光敏传感器接受并转换成电信号再经过AD转换成数字信号,简化过程:光–> 电 --> 数字信号
为什么大多数传感器都是采用的绿光呢?
我们先看看光谱的特点,从紫外线到红外线的波长是越来越长的。
之所以选择绿光作为光源是考虑到一下几个特点:
- 皮肤的黑色素会吸收大量波长较短的波
- 皮肤上的水份也会吸收大量的UV和IR部分的光
- 进入皮肤组织的绿光(500nm)-- 黄光(600nm)大部分会被红细胞吸收
- 红光和接近IR的光相比其他波长的光更容易穿过皮肤组织
- 血液要比其他组织吸收更多的光
- 相比红光,绿(绿-黄)光能被氧合血红蛋白和脱氧血红蛋白吸收
总体来说,绿光-- 红光能作为测量光源。早起多数采用红光为光源,随着进一步的研究和对比,绿光作为光源得到的信号更好,信噪比也比其他光源好些,所以现在大部分穿戴设备采用绿光为光源。但是考虑到皮肤情况的不用(肤色、汗水),高端产品会根据情况自动使用换绿光、红光和IR多种光源。
虽然知道了上面的几个特点,但是还不足以弄清楚为什么通过光照就能测出心率、血氧等参数呢?
下图就解释了核心原理
当光照透过皮肤组织然后再反射到光敏传感器时光照有一定的衰减的。像肌肉、骨骼、静脉和其他连接组织等等对光的吸收是基本不变的(前提是测量部位没有大幅度的运动),但是血液不同,由于动脉里有血液的流动,那么对光的吸收自然也有所变化。当我们把光转换成电信号时,正是由于动脉对光的吸收有变化而其他组织对光的吸收基本不变,得到的信号就可以分为直流DC信号和交流AC信号。提取其中的AC信号,就能反应出血液流动的特点。我们把这种技术叫做光电容积脉搏波描记法PPG。
下图是PPG信号和ECG信号的对比
实际测量手指的PPG信号如下:
所以,只要测得到的PPG信号比较理想算出心率也不算什么难事。但是事实总是残酷的,由于测量部位的移动、自然光、日光灯等等其他的干扰,最终测到的信号可能是下面的这种,所以要通过很多方法进行滤波处理
对于PPG信号的处理,目前我知道的有两种方法。一种是时域分析,即算出一定时间内PPG信号的波峰个数,另一种是通过对PPG信号进行FFT变换得到频域的特点。
时域方法:
通过对原始的{PPG信号进行滤波处理,得到一定时间内的波峰个数,然后既可算出心率值
假设连续采样5秒的时间,在5s内的波峰个数为N,那么心率就是N*12 (这个相信大家都懂,就跟把脉一样~)
频域分析:
上面分析过,我们把血液流动对光吸收转变成了AC信号,如果对于进行FFT变换,那么就能看到频域的特点。如下图就是对PPG信号的FFT转变
上图中的频域图,0Hz的信号很强,这部分是骨骼、肌肉等组织的DC信号,在1Hz附近有个相对比较突出的信号就是血液流动转变的AC信号。假设测得到的频率f = 1.2Hz
那么心率HeartRate HR = f x60 = 1.2 x 60 = 72
最后再简单提一下血氧的测量,相比心率血氧测量难度较大而且精度不算太高。测量血氧的原理图下图所示
由于血液中含有的氧合血红蛋白HbO2和血红蛋白Hb存在一定的比例,简单说也就是含氧量吧。上面的图表示了氧合血红蛋白HbO2和血红蛋白Hb对波长6001000nm的光吸收特性,从图中可以看出上600800nm间Hb的吸收系数更高,8001000之间HbO2的吸收系数更高。所以可以利用红光(600800nm)和接近IR(800~1000nm)的光分别检测HbO2和Hb的PPG信号,然后通过程序处理算出相应的比值,这样就得到了血氧值。
但是由于光源不同,直接利用红光和接近IR的光进行信号对比是不可靠的,因为红光和IR透过皮肤组织也会产生不同的吸收。下图是红光和IR透过皮肤的原始信号示意图
上面分析说过,DC部分是光透过皮肤组织转换成的直流信号,AC是血液流动产生转换成的交流信号。由于皮肤组织对红光和IR的吸收程度不同,DC部分自然也就不一样。为了能共“公平对待”两种光源的PPG信号,所以需要对原始信号处理一下。下图示意了处理后的信号(DC部分相等)
通过一定的比例计算,公平对待Red和IR的PPG信号。这样计算出来的Hb和HbO2比例才可靠。
三.MAX30102编程
编程主要实现的是3个部分,如下:
1 MAX30102 IIC接口驱动 maxim_max30102_write_reg
2 MAX30102初始化函数 maxim_max30102_init
3 MAX30102 FIFO读取 maxim_max30102_read_fifo
1、MAX30102 IIC接口驱动
对于MAX30102的驱动程序,将其拆分出来,可分为标准IIC程序和MAX30102寄存器的读写操作,实现这两部分的编程,便完成MAX30102的驱动; IIC程序上文已给出,这里就不例举出来。
和一般的IIC通信不同,MAX30102的读写时序如下所示:
写入数据:
在IIC程序定义了
#define I2C_WR 0 /* 写控制bit /
#define I2C_RD 1 / 读控制bit */
1 #define max30102_WR_address 0xAE
2 bool maxim_max30102_write_reg(uint8_t uch_addr, uint8_t uch_data)
3 {
4 IIC_Start();/* 第1步:发起I2C总线启动信号 */
5 /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
6 IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */
7 if (IIC_Wait_Ack() != 0) /* 第3步:发送ACK */
8 {
9 goto cmd_fail; /* EEPROM器件无应答 */
10 }
11 IIC_Send_Byte(uch_addr); /* 第4步:发送字节地址 */
12 if (IIC_Wait_Ack() != 0)
13 {
14 goto cmd_fail; /* EEPROM器件无应答 */
15 }
16 IIC_Send_Byte(uch_data); /* 第5步:开始写入数据 */
17 if (IIC_Wait_Ack() != 0) /* 第6步:发送ACK */
18 {
19 goto cmd_fail; /* EEPROM器件无应答 */
20 }
21 IIC_Stop();/* 发送I2C总线停止信号 */
22 return true; /* 执行成功 */
23 cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
24 IIC_Stop();/* 发送I2C总线停止信号 */
25 return false;
26 }
读取数据:
1 bool maxim_max30102_read_reg(uint8_t uch_addr, uint8_t *puch_data)
2 {
3 IIC_Start();/* 第1步:发起I2C总线启动信号 */
4 /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
5 IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */
6 if (IIC_Wait_Ack() != 0) /* 第3步:发送ACK */
7 {
8 goto cmd_fail; /* EEPROM器件无应答 */
9 }
10 IIC_Send_Byte((uint8_t)uch_addr); /* 第4步:发送字节地址, */
11 if (IIC_Wait_Ack() != 0)
12 {
13 goto cmd_fail; /* EEPROM器件无应答 */
14 }
15 IIC_Start(); /* 第6步:重新启动I2C总线。下面开始读取数据 */
16 /* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
17 IIC_Send_Byte(max30102_WR_address | I2C_RD); /* 此处是读指令 */
18 if (IIC_Wait_Ack() != 0) /* 第8步:发送ACK */
19 {
20 goto cmd_fail; /* EEPROM器件无应答 */
21 }
22 /* 第9步:读取数据 */
23 {
24 *puch_data = IIC_Read_Byte(); /* 读1个字节 */
25 IIC_NAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
26 }
27 IIC_Stop();/* 发送I2C总线停止信号 */
28 return true; /* 执行成功 返回data值 */
29 cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
30 IIC_Stop();/* 发送I2C总线停止信号 */
31 return false;
32 }
2、MAX30102初始化函数和MAX30102 FIFO读取
MAX30102初始化需要查看寄存器进行配置,下文把MAX30102所有用到的的寄存器都列举出来了,先了解MAX30102的寄存器,再进行配置。FIFO数据的读取,笔者将采集到的值通过查表法将血氧值拟合出来。
FIFO数据的读取:
1 bool maxim_max30102_read_fifo(uint32_t *pun_red_led, uint32_t *pun_ir_led)
2 {
3 uint32_t un_temp;
4 uint8_t uch_temp;
5 *pun_ir_led = 0;
6 *pun_red_led = 0;
7 maxim_max30102_read_reg(REG_INTR_STATUS_1, &uch_temp);
8 maxim_max30102_read_reg(REG_INTR_STATUS_2, &uch_temp);
9 IIC_Start();/* 第1步:发起I2C总线启动信号 */
10 /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
11 IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */
12 if (IIC_Wait_Ack() != 0) /* 第3步:发送ACK */
13 {
14 goto cmd_fail; /* EEPROM器件无应答 */
15 }
16 IIC_Send_Byte((uint8_t)REG_FIFO_DATA); /* 第4步:发送字节地址, */
17 if (IIC_Wait_Ack() != 0)
18 {
19 goto cmd_fail; /* EEPROM器件无应答 */
20 }
21 IIC_Start();/* 第6步:重新启动I2C总线。下面开始读取数据 */
22 /* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
23 IIC_Send_Byte(max30102_WR_address | I2C_RD); /* 此处是读指令 */
24 if (IIC_Wait_Ack() != 0) /* 第8步:发送ACK */
25 {
26 goto cmd_fail; /* EEPROM器件无应答 */
27 }
28 un_temp = IIC_Read_Byte();
29 IIC_Ack();
30 un_temp <<= 16;
31 *pun_red_led += un_temp;
32 un_temp = IIC_Read_Byte();
33 IIC_Ack();
34 un_temp <<= 8;
35 *pun_red_led += un_temp;
36 un_temp = IIC_Read_Byte();
37 IIC_Ack();
38 *pun_red_led += un_temp;
39 un_temp = IIC_Read_Byte();
40 IIC_Ack();
41 un_temp <<= 16;
42 *pun_ir_led += un_temp;
43 un_temp = IIC_Read_Byte();
44 IIC_Ack();
45 un_temp <<= 8;
46 *pun_ir_led += un_temp;
47 un_temp = IIC_Read_Byte();
48 IIC_Ack();
49 *pun_ir_led += un_temp;
50 *pun_red_led &= 0x03FFFF; //Mask MSB [23:18]
51 *pun_ir_led &= 0x03FFFF; //Mask MSB [23:18]
52 IIC_Stop();/* 发送I2C总线停止信号 */
53 return true;
54 cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
55 IIC_Stop();/* 发送I2C总线停止信号 */
56 return false;
57 }
上图是一个典型的PPG波形,即光电二极管接收到原始的光信号波形。波形(由图中间的白色横虚线)可分为两部分:DC signal 和 AC signal,即直流信号和交流信号。
其中直流信号由下到上可分为以下三部分的反射:组织(issue),静脉血(Venous blood)和不跳动的动脉血(Non pulsatile arterial blood)。当然对于不同年龄、性别和肤色等人不同,对应的DC signal 值也会不同。所以后面的血氧计算都是使用相对值。
而 AC signal 交流信号就比较单一:由跳动的动脉血反射得到。其中波峰对应心脏的收缩(Systole),波谷对应心脏舒张(Diastole)。
1 心率计算(HR)
通过计算AC signal 两个波峰的时间(图中两条竖着的黑色虚线),我们就能计算出心率。这里不再赘述。
2 灌注指数(PI)
临床上,交流分量与直流分量的幅值之比反映了人体的血流灌注能力,称为血流灌注指数(Perfusion Index,PI),其表达式为:
3 血氧计算(SpO2)
血压计算公式也比较简单,这里MAX30102是一路红光,一路红外。只分别算出红光的交流除以红光的直流即: ACred/DCred,和红外的交流除以红外的直流分量即:ACired/DCired。然后两者再相除得到R。
得到 R 然后查表即可得到血氧值,也可以通过下面美信拟合的公式计算:
SpO2 = -45.060RR + 30.354 *R + 94.845
3、MAX30102寄存器
1、了解寄存器
(1) Interrupt Status (0x00–0x01)【中断状态】
每当中断被触发时,MAX30102就会将主动低电平中断引脚拉到低电平状态,直到中断被清除。
A_FULL: FIFO快满标志
在SpO2和HR模式下,当FIFO写指针剩余一定数量的空闲空间时,该中断会被触发。触发数可以由FIFO_A_FULL[3:0]寄存器设置。中断通过读取Interrupt Status 1寄存器(0x00)来清除。
PPG_RDY: 新的FIFO数据就绪
在SpO2和HR模式下,当数据FIFO中出现新的样本时,该中断会被触发。该中断通过读取Interrupt Status 1寄存器(0x00)或读取FIFO_DATA寄存器来清除。
ALC_OVF: 环境光消除溢出
当SpO2/HR光电二极管的环境光消除功能达到其最大极限,因此环境光影响ADC的输出时,该中断就会触发。该中断通过读取Interrupt Status 1寄存器(0x00)而被清除。
PWR_RDY: 电源就绪标志
在上电时或停电后,当电源电压VDD从低于欠压锁定(UVLO)电压过渡到高于UVLO电压时,会触发一个电源就绪中断,以示模块已上电并准备好收集数据。
DIE_TEMP_RDY: 内部温度准备标志
当内部芯片温度转换完成后,该中断被触发,以便处理器可以读取温度数据寄存器。通过读取Interrupt Status 2寄存器(0x01) 或TFRAC寄存器(0x20)来清除该中断。只要读取中断状态寄存器,或读取触发中断的寄存器,中断就会被清除。例如,如果SpO2传感器由于完成转换而触发了一个中断,读取FIFO数据寄存器或中断寄存器就会清除中断引脚(恢复到正常的高电平状态)。这也会将中断状态寄存器中的所有位清除为零
(2) Interrupt Enable (0x02-0x03)【中断使能】
除电源准备外,每个硬件中断源都可以在MAX30102 IC内部的软件寄存器中禁用。电源准备中断不能被禁用,因为模块的数字状态在停电条件(低电源电压)下被重置,而默认情况下所有中断都被禁用。另外,系统必须知道发生了停电情况,模块内的数据因此而被重置,这一点很重要。在正常操作中,未使用的位应始终被设置为零。
(3) FIFO (0x04–0x07)
FIFO Write Pointer【FIFO写指针】
FIFO写入指针指向MAX30102写入下一个样本的位置。这个指针每推送一个样本到 FIFO 就会前进。当 MODE[2:0]为010、011 或 111 时,它也可以通过 I2C 接口改变。
Over Flow Counter【FIFO溢出计数器】
当FIFO满的时候,样本不会被推到FIFO上,样本会丢失。OVF_COUNTER计算丢失的样本数。它在0x1F时达到饱和。当一个完整的样本从FIFO中被 “弹出”(即移除旧的FIFO数据并将样本下移)时(当读指针前进时),OVF_COUNTER 被重置为零。
FIFO Read Pointer【FIFO读取指针】
FIFO读取指针指向处理器通过I2C接口从FIFO获取下一个样本的位置。每次从FIFO中取出一个样本时,这个指针就会前进。如果出现数据通信错误,处理器也可以在读取样本后写到这个指针,以便从FIFO中重新读取样本。
FIFO Data Register【FIFO数据寄存器】
循环FIFO的深度是32,可以容纳32个数据样本。采样大小取决于配置为活动的LED通道(又称通道)的数量。由于每个通道信号被存储为3字节的数据信号,FIFO宽度可以是3字节或6字节大小。
I2C寄存器图中的FIFO_DATA寄存器指向要从FIFO读取的下一个样本。FIFO_RD_PTR 指向这个样本。读取FIFO_DATA 寄存器,不会自动增加I2C寄存器的地址。突发读取该寄存器,重复读取相同的地址。每个样本是每个通道的3个字节的数据(例如,红色3个字节,红外3个字节,等等)。
FIFO寄存器(0x04-0x07)都可以被写入和读取,但在实际操作中,只有FIFO_RD_PTR寄存器应该被写入。其他的则由MAX30102自动递增或填充数据。当开始一个新的SpO2或心率转换时,建议首先将FIFO_WR_PTR、OVF_COUNTER 和FIFO_RD_PTR寄存器清零(0x00),以确保FIFO为空并处于已知状态。当在一个连读I2C事件中读取MAX30102寄存器时,寄存器地址指针通常会递增,以便发送的下一个字节的数据来自下一个寄存器,等等。这方面的例外是FIFO数据寄存器,寄存器0x07。当读取这个寄存器时,地址指针不会增加,但是FIFO_RD_PTR会增加。所以发送的下一个字节的数据代表FIFO中的下一个字节的数据。
从FIFO中读取
通常情况下,从I2C接口读取寄存器会自动增加寄存器地址指针,因此所有的寄存器都可以在突发读取中读取,而无需I2C启动事件。在MAX30102中,除了FIFO_DATA寄存器(寄存器0x07)外,所有寄存器都是如此。
读取FIFO_DATA寄存器不会自动增加寄存器的地址。突发读取该寄存器从同一地址反复读取数据。每个样本包括多个字节的数据,所以应该从这个寄存器中读取多个字节(在同一个事务中)以获得一个完整的样本。
另一个例外是0xFF。在0xFF寄存器之后读取更多的字节并不能使地址指针前进到0x00,而且读取的数据也没有意义。
FIFO数据结构
数据FIFO包括一个32个样本的存储库,可以存储IR和Red ADC数据。由于每个样本由两个通道的数据组成,每个样本有6个字节的数据,因此FIFO中可以存储总共192个字节的数据。
FIFO数据是左对齐的,如表1所示;换句话说,无论ADC分辨率如何设置,MSB位总是在第17位数据位置。FIFO数据结构的直观介绍见表2。
FIFO数据每通道包含3个字节
FIFO数据是左对齐的,这意味着无论ADC分辨率设置如何,MSB总是在同一位置。FIFO DATA[18] - [23]不被使用。表2显示了每个三字节的结构(包含每个通道的18位ADC数据输出)。
SpO2模式下的每个数据样本包括两个数据三字节(每个3字节),读取一个样本需要对每个字节进行I2C读取命令。因此,在SpO2模式下读取一个样本,需要6个I2C字节的读取。读取每个样本的第一个字节后,FIFO读取指针会自动递增。
写/读指针
写/读指针被时用来控制FIFO中的数据流。每当一个新的样本被添加到FIFO中时,写指针就会递增。每当从FIFO中读取一个样本,读指针就会增加。要从FIFO中重读一个样本,将其值减一并再次读取数据寄存器。在进入SpO2模式或HR模式时,FIFO的写/读指针应被清除(回到0x00),这样FIFO中就没有旧数据了。如果VDD断电或VDD降到UVLO电压以下,指针会自动清除。
(4) FIFO Configuration (0x08)【FIFO配置】
位 7:5: 采样平均 (SMP_AVE)
为了减少数据吞吐量,相邻的样本(在每个单独的通道中)可以通过设置这个寄存器在芯片上进行平均和抽取。
位4: FIFO满时滚动 (FIFO_ROLLOVER_EN)
这个位控制FIFO完全被数据填满时的行为。如果FIFO_ROLLOVER_EN被设置(1),FIFO地址将翻转为零,FIFO继续填充新数据。如果该位没有被设置(0),那么FIFO不会被更新,直到FIFO_DATA被读取或者WRITE/READ指针位置被改变。
位3:0:FIFO几乎满值(FIFO_A_FULL)
这个寄存器设置了中断发出时FIFO中剩余的数据样本(3字节/样本)的数量。例如,如果这个字段被设置为0x0,那么当FIFO中剩余的数据样本为0时,就会发出中断(所有32个FIFO字都有未读数据)。此外,如果这个字段被设置为0xF,那么当FIFO中还剩下15个数据样本(17个FIFO数据样本有未读数据)时,就会发出中断。
(5) Mode Configuration (0x09)【模式配置】
位 7:关机控制(SHDN)
通过将该位设置为1,可以使该部件进入省电模式。在省电模式下,所有的寄存器都保留它们的值,写/读操作都正常进行。在这种模式下,所有的中断都被清除为零。
第6位:复位控制(RESET)
当RESET位被设置为1时,所有的配置、阈值和数据寄存器都通过上电复位被重置为上电状态。复位序列完成后,RESET位会自动清空为零。
注意:设置RESET位不会触发PWR_RDY中断事件。
位2:0:模式控制
这些位设定了MAX30102的工作状态。改变模式不会改变任何其他设置,也不会擦除数据寄存器内先前存储的任何数据。
(6) SpO2 Configuration (0x0A)【SpO2配置】
位 6:5: SpO2 ADC 范围控制
该寄存器设置SpO2传感器ADC的满刻度范围,如表5所示。
位 4:2: SpO2 采样率控制
这些位定义了有效的采样率,一个采样由一个红外脉冲/转换和一个红色脉冲/转换组成。采样率和脉冲宽度是相关的,因为采样率为脉冲宽度时间设定了一个上限。如果用户选择的采样率对于所选的LED_PW设置来说太高,那么可能的最高采样率将被编入寄存器。
脉冲宽度与采样率的关系见表11和表12。
位1:0:LED脉冲宽度控制和ADC分辨率
这些位设置了LED脉冲宽度(红外和红光具有相同的脉冲宽度),因此,间接地设置了ADC在每个采样中的积分时间。ADC的分辨率与积分时间直接相关。
(7) LED Pulse Amplitude (0x0C–0x0D)【LED脉冲振幅】
这些位设置每个LED的电流水平,如表8所示。
(8) Multi-LED Mode Control Registers (0x11–0x12)【多LED模式控制寄存器】
在多LED模式下,每个样本被分成最多四个时隙,即SLOT1至SLOT4。这些控制寄存器决定在每个时隙中哪个LED是活动的,从而使配置非常灵活。
每个槽产生一个3字节的输出到FIFO。一个样本包括所有活动槽,例如,如果SLOT1和SLOT2不为零,那么一个样本就是2 x 3 = 6字节。插槽应按顺序启用(即,如果启用了SLOT2,就不应该禁用SLOT1)。
(9) Temperature Data (0x1F–0x21)【温度数据】
温度整数
板载温度ADC输出被分成两个寄存器,一个用于存储整数温度,一个用于存储分数。在读取温度数据时,这两个都应该被读取,下面的公式显示了如何将这两个寄存器加在一起。
tmeasured = tinteger + tfraction
该寄存器以2的补码格式存储整数温度数据,其中每一位对应1℃。
温度部分
该寄存器以0.0625℃的增量存储分数温度数据。如果这个分数温度与一个负的整数配对,它仍然作为一个正的分数值添加
(例如,-128℃+0.5℃=-127.5℃)。
温度启用 (TEMP_EN)
这是一个自清位,当被设置时,从温度传感器开始一个单一的温度读数。当该位被设置为1时,该位在温度读数结束后自动清零。
2、宏定义寄存器
#define REG_INTR_STATUS_1 0x00 //中断状态1寄存器 0x00
#define REG_INTR_STATUS_2 0x01 //中断状态2寄存器 0x01
#define REG_INTR_ENABLE_1 0x02 //中断使能1寄存器 0x02
#define REG_INTR_ENABLE_2 0x03 //中断使能1寄存器 0x03
#define REG_FIFO_WR_PTR 0x04 //FIFO写指针寄存器 0x04
#define REG_OVF_COUNTER 0x05 //FIFO溢出计数器寄存器 0x05
#define REG_FIFO_RD_PTR 0x06 //FIFO读取指针寄存器 0x06
#define REG_FIFO_DATA 0x07 //FIFO数据寄存器 0x07
#define REG_FIFO_CONFIG 0x08 //FIFO配置寄存器 0x08
#define REG_MODE_CONFIG 0x09 //模式配置寄存器 0x09
#define REG_SPO2_CONFIG 0x0A //SpO2配置寄存器 0x0A
#define REG_LED1_PA 0x0C //LED1脉冲振幅寄存器 0x0C
#define REG_LED2_PA 0x0D //LED2脉冲振幅寄存器 0x0D
#define REG_PILOT_PA 0x10 //LED为25mA寄存器 0x10
#define REG_MULTI_LED_CTRL1 0x11 //多LED模式控制寄存器 0x11-0x12
#define REG_MULTI_LED_CTRL2 0x12
#define REG_TEMP_INTR 0x1F //温度数据寄存器 0x1F–0x21
#define REG_TEMP_FRAC 0x20
#define REG_TEMP_CONFIG 0x21
#define REG_PROX_INT_THRESH 0x30 //保留寄存器 0x30
#define REG_REV_ID 0xFE //订正ID寄存器 0xFF
#define REG_PART_ID 0xFF //设备ID寄存器 0xFF
MAX30102初始化:
1 bool maxim_max30102_init(void)
2 {
3 GPIO_InitTypeDef GPIO_InitStructure;
4 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能PORTA,PORTC时钟
5 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//PA3
6 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //PA3设置成浮空输入
7 GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA3
8 if(!maxim_max30102_write_reg(REG_INTR_ENABLE_1, 0xc0)) // INTR setting
9 return false;
10 if(!maxim_max30102_write_reg(REG_INTR_ENABLE_2, 0x00))
11 return false;
12 if(!maxim_max30102_write_reg(REG_FIFO_WR_PTR, 0x00)) //FIFO_WR_PTR[4:0]
13 return false;
14 if(!maxim_max30102_write_reg(REG_OVF_COUNTER, 0x00)) //OVF_COUNTER[4:0]
15 return false;
16 if(!maxim_max30102_write_reg(REG_FIFO_RD_PTR, 0x00)) //FIFO_RD_PTR[4:0]
17 return false;
18 //sample avg = 8, fifo 19 rollover=false, fifo almost full = 17
19 if(!maxim_max30102_write_reg(REG_FIFO_CONFIG, 0x6f))
20 return false;
21 //0x02 for Red only, 0x03 for SpO2 mode 0x07 multimode LED
22 if(!maxim_max30102_write_reg(REG_MODE_CONFIG, 0x03))
23 return false;
24 // SPO2_ADC range = 4096nA, SPO2 sample rate (400 Hz), LED pulseWidth (411uS)
25 if(!maxim_max30102_write_reg(REG_SPO2_CONFIG, 0x2F))
26 return false;
27 //Choose value for ~ 4.5mA for LED1
28 if(!maxim_max30102_write_reg(REG_LED1_PA, 0x17))
29 return false;
30 // Choose value for ~ 4.5mA for LED2
31 if(!maxim_max30102_write_reg(REG_LED2_PA, 0x17))
32 return false;
33 // Choose value for ~ 25mA for Pilot LED
34 if(!maxim_max30102_write_reg(REG_PILOT_PA, 0x7f))
35 return false;
36 return true;
37 }