基于STM32的心率血氧检测仪
一、硬件连接
1.1器材准备
(1)开发板:STM32F103系列
(2)显示屏:0.96寸OLED
(3)串口监视:USB-TTL
1.2线路连接
STM32F103 | 0.96寸 OLED | USB-TTL |
---|---|---|
VCC<->3.3V | VCC<->3.3V | 5V<->5V |
GND<->GND | GND<->GND | GND<->GND |
SCL<->PB7 | SCL<->PA5 | RXD<->PA9 |
SDA<->PB8 | SDA<->PA6 | TXD<->PA10 |
INT<->PB9 | RST<->PA3 | |
DC<->PA4 | ||
CS<->PA2 |
二、实验原理
2.1 0.96寸OLED显示原理
引脚含义:
端口名 | 功能 |
---|---|
GND | 接地端口 |
VCC | 接3.3V电源端口 |
D0 | CLK时钟信号(等同于SCL) |
D1 | 数据端口(等同于SDA) |
RES | 复位端口(等同于RST) |
DC | 数据/命令选择引脚(等同于D/C) |
CS | 片选引脚(低电平有效,也就是所需要接低电平,我实际试验过不接该引脚也是可以正常使用的) |
![]() |
我们利用液晶屏幕可显示数据,字符以及各种图片信息等
#include "stm32f10x.h"
#include "oled.h"
//清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
void OLED_Clear(void)
{
u8 i,n;
for(i=0;i<8;i++)for(n=0;n<128;n++)OLED_GRAM[n][i]=0X00;
OLED_Refresh_Gram();//更新显示
}
//画点
//x:0~127
//y:0~63
//t:1 填充 0,清空
void OLED_DrawPoint(u8 x,u8 y,u8 t)
{
u8 pos,bx,temp=0;
if(x>127||y>63)return;//超出范围了.
pos=7-y/8;
bx=y%8;
temp=1<<(7-bx);
if(t)OLED_GRAM[x][pos]|=temp;
else OLED_GRAM[x][pos]&=~temp;
}
2.2心率血氧传感器原理
2.2.1光电容积脉搏波描记法PPG
光学心率传感器,如果带过上述那些智能手表或者智能手环的朋友来说也不算稀奇的事情。就拿AppleWatch来说,测量心率时底部的表盘会发出绿色的灯光,并且测量的时候手腕最好保持不动否侧会影响测量结果。接下来将详细介绍光学心率测量的原理。
2.2.2那么为什么通过LED灯发光就能测量心率呢?
当LED光射向皮肤,透过皮肤组织反射回的光被光敏传感器接受并转换成电信号再经过AD转换成数字信号,简化过程:光–> 电 --> 数字信号
2.2.3为什么大多数传感器都是采用的绿光呢?
我们先看看光谱的特点,从紫外线到红外线的波长是越来越长的。
之所以选择绿光作为光源是考虑到一下几个特点:
-
皮肤的黑色素会吸收大量波长较短的波
-
皮肤上的水份也会吸收大量的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部分相等)
三、实验程序解析
3.1主函数部分
#include "led.h"
#include "delay.h"
#include "sys.h"
#include "usart.h"
#include "max30102.h"
#include "myiic.h"
#include "algorithm.h"
#include "oled.h"
uint32_t aun_ir_buffer[500]; //显示传感器数据
int32_t n_ir_buffer_length; //数据长度
uint32_t aun_red_buffer[500]; //LED数据
int32_t n_sp02; //SPO2值
int8_t ch_spo2_valid; //指示器:显示 SP02 计算是否有效
int32_t n_heart_rate; //心率值
int8_t ch_hr_valid; //指示器:用于显示心率计算是否有效
uint8_t uch_dummy;
#define MAX_BRIGHTNESS 255 //亮度最大值
void dis_DrawCurve(u32* data,u8 x);
int main(void)
{
//用于计算反映心跳的板载 LED 亮度的变量
uint32_t un_min, un_max, un_prev_data;
int i;
int32_t n_brightness;
float f_temp;
u8 temp_num=0;
u8 temp[6];
u8 str[100];
u8 dis_hr=0,dis_spo2=0;
NVIC_Configuration();
delay_init(); //延时函数初始化
uart_init(115200); //串口初始化为115200
LED_Init();
//OLED
OLED_Init();
OLED_ShowString(0,0," initializing ",16);
OLED_Refresh_Gram();//更新显示到OLED
max30102_init();
printf("\r\n MAX30102 init \r\n");
un_min=0x3FFFF;
un_max=0;
n_ir_buffer_length=500; //缓冲液长度为 100 可存储以 100sps 运行的 5 秒样本
//读取前500个样本,并确定信号范围
for(i=0;i<n_ir_buffer_length;i++)
{
while(MAX30102_INT==1); //等到中断引脚置位
max30102_FIFO_ReadBytes(REG_FIFO_DATA,temp);
aun_red_buffer[i] = (long)((long)((long)temp[0]&0x03)<<16) | (long)temp[1]<<8 | (long)temp[2]; // 合并值以获取实际数字
aun_ir_buffer[i] = (long)((long)((long)temp[3] & 0x03)<<16) |(long)temp[4]<<8 | (long)temp[5]; // 合并值以获取实际数字
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];
//计算前 500 个样本(样本的前 5 秒)后的心率和 SpO2
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
while(1)
{
i=0;
un_min=0x3FFFF;
un_max=0;
//将前 100 组样本转储到内存中,并将最后 400 组样本移到顶部
for(i=100;i<500;i++)
{
aun_red_buffer[i-100]=aun_red_buffer[i];
aun_ir_buffer[i-100]=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];
}
//在计算心率之前,取100组样本。
for(i=400;i<500;i++)
{
un_prev_data=aun_red_buffer[i-1];
while(MAX30102_INT==1);
max30102_FIFO_ReadBytes(REG_FIFO_DATA,temp);
aun_red_buffer[i] = (long)((long)((long)temp[0]&0x03)<<16) | (long)temp[1]<<8 | (long)temp[2]; // 合并值以获取实际数字
aun_ir_buffer[i] = (long)((long)((long)temp[3] & 0x03)<<16) |(long)temp[4]<<8 | (long)temp[5]; // 合并值以获取实际数字
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;
n_brightness-=(int)f_temp;
if(n_brightness<0)
n_brightness=0;
}
else
{
f_temp=un_prev_data-aun_red_buffer[i];
f_temp/=(un_max-un_min);
f_temp*=MAX_BRIGHTNESS;
n_brightness+=(int)f_temp;
if(n_brightness>MAX_BRIGHTNESS)
n_brightness=MAX_BRIGHTNESS;
}
//通过UART将样品和计算结果发送到终端程序
if(ch_hr_valid == 1 && n_heart_rate<120)//**/ ch_hr_valid == 1 && ch_spo2_valid ==1 && n_heart_rate<120 && n_sp02<101
{
dis_hr = n_heart_rate;
dis_spo2 = n_sp02;
}
else
{
dis_hr = 0;
dis_spo2 = 0;
}
printf("HR=%i, ", n_heart_rate);
printf("HRvalid=%i, ", ch_hr_valid);
printf("SpO2=%i, ", n_sp02);
printf("SPO2Valid=%i\r\n", ch_spo2_valid);
}
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
//显示刷新
LED0=0;
if(dis_hr == 0 && dis_spo2 == 0) //**dis_hr == 0 && dis_spo2 == 0
{
sprintf((char *)str,"HR:--- SpO2:--- ");//**HR:--- SpO2:---
}
else{
sprintf((char *)str,"HR:%3d SpO2:%3d ",dis_hr,dis_spo2);//**HR:%3d SpO2:%3d
}
OLED_ShowString(0,0,str,16);
OLED_Fill(0,23,127,63,0);
//红光在上,红外在下
dis_DrawCurve(aun_red_buffer,20);
dis_DrawCurve(aun_ir_buffer,0);
OLED_Refresh_Gram();//更新显示到OLED
}
}
3.2 OLED部分
#include "oled.h"
#include "stdlib.h"
#include "oledfont.h"
#include "delay.h"
//
//OLED的显存
//存放格式如下.
//[0]0 1 2 3 ... 127
//[1]0 1 2 3 ... 127
//[2]0 1 2 3 ... 127
//[3]0 1 2 3 ... 127
//[4]0 1 2 3 ... 127
//[5]0 1 2 3 ... 127
//[6]0 1 2 3 ... 127
//[7]0 1 2 3 ... 127
u8 OLED_GRAM[128][8];
//更新显存到LCD
void OLED_Refresh_Gram(void)
{
u8 i,n;
for(i=0;i<8;i++)
{
OLED_WR_Byte (0xb0+i,OLED_CMD); //设置页地址(0~7)
OLED_WR_Byte (0x00,OLED_CMD); //设置显示位置—列低地址
OLED_WR_Byte (0x10,OLED_CMD); //设置显示位置—列高地址
for(n=0;n<128;n++)OLED_WR_Byte(OLED_GRAM[n][i],OLED_DATA);
}
}
//向SSD1306写入一个字节。
//dat:要写入的数据/命令
//cmd:数据/命令标志 0,表示命令;1,表示数据;
void OLED_WR_Byte(u8 dat,u8 cmd)
{
u8 i;
OLED_RS=cmd; //写命令
OLED_CS=0;
for(i=0;i<8;i++)
{
OLED_SCLK=0;
if(dat&0x80)OLED_SDIN=1;
else OLED_SDIN=0;
OLED_SCLK=1;
dat<<=1;
}
OLED_CS=1;
OLED_RS=1;
}
//开启OLED显示
void OLED_Display_On(void)
{
OLED_WR_Byte(0X8D,OLED_CMD); //SET DCDC命令
OLED_WR_Byte(0X14,OLED_CMD); //DCDC ON
OLED_WR_Byte(0XAF,OLED_CMD); //DISPLAY ON
}
//关闭OLED显示
void OLED_Display_Off(void)
{
OLED_WR_Byte(0X8D,OLED_CMD); //SET DCDC命令
OLED_WR_Byte(0X10,OLED_CMD); //DCDC OFF
OLED_WR_Byte(0XAE,OLED_CMD); //DISPLAY OFF
}
//清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
void OLED_Clear(void)
{
u8 i,n;
for(i=0;i<8;i++)for(n=0;n<128;n++)OLED_GRAM[n][i]=0X00;
OLED_Refresh_Gram();//更新显示
}
//画点
//x:0~127
//y:0~63
//t:1 填充 0,清空
void OLED_DrawPoint(u8 x,u8 y,u8 t)
{
u8 pos,bx,temp=0;
if(x>127||y>63)return;//超出范围了.
pos=7-y/8;
bx=y%8;
temp=1<<(7-bx);
if(t)OLED_GRAM[x][pos]|=temp;
else OLED_GRAM[x][pos]&=~temp;
}
//x1,y1,x2,y2 填充区域的对角坐标
//确保x1<=x2;y1<=y2 0<=x1<=127 0<=y1<=63
//dot:0,清空;1,填充
void OLED_Fill(u8 x1,u8 y1,u8 x2,u8 y2,u8 dot)
{
u8 x,y;
for(x=x1;x<=x2;x++)
{
for(y=y1;y<=y2;y++)OLED_DrawPoint(x,y,dot);
}
OLED_Refresh_Gram();//更新显示
}
//在指定位置显示一个字符,包括部分字符
//x:0~127
//y:0~63
//mode:0,反白显示;1,正常显示
//size:选择字体 16/12
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size,u8 mode)
{
u8 temp,t,t1;
u8 y0=y;
u8 csize=(size/8+((size%8)?1:0))*(size/2); //得到字体一个字符对应点阵集所占的字节数
chr=chr-' ';//得到偏移后的值
for(t=0;t<csize;t++)
{
if(size==12)temp=asc2_1206[chr][t]; //调用1206字体
else if(size==16)temp=asc2_1608[chr][t]; //调用1608字体
else if(size==24)temp=asc2_2412[chr][t]; //调用2412字体
else return; //没有的字库
for(t1=0;t1<8;t1++)
{
if(temp&0x80)OLED_DrawPoint(x,y,mode);
else OLED_DrawPoint(x,y,!mode);
temp<<=1;
y++;
if((y-y0)==size)
{
y=y0;
x++;
break;
}
}
}
}
//m^n函数
u32 mypow(u8 m,u8 n)
{
u32 result=1;
while(n--)result*=m;
return result;
}
//显示2个数字
//x,y :起点坐标
//len :数字的位数
//size:字体大小
//mode:模式 0,填充模式;1,叠加模式
//num:数值(0~4294967295);
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size)
{
u8 t,temp;
u8 enshow=0;
for(t=0;t<len;t++)
{
temp=(num/mypow(10,len-t-1))%10;
if(enshow==0&&t<(len-1))
{
if(temp==0)
{
OLED_ShowChar(x+(size/2)*t,y,' ',size,1);
continue;
}else enshow=1;
}
OLED_ShowChar(x+(size/2)*t,y,temp+'0',size,1);
}
}
//显示字符串
//x,y:起点坐标
//size:字体大小
//*p:字符串起始地址
void OLED_ShowString(u8 x,u8 y,const u8 *p,u8 size)
{
while((*p<='~')&&(*p>=' '))//判断是不是非法字符!
{
if(x>(128-(size/2))){x=0;y+=size;}
if(y>(64-size)){y=x=0;OLED_Clear();}
OLED_ShowChar(x,y,*p,size,1);
x+=size/2;
p++;
}
}
//初始化SSD1306
void OLED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
OLED_RST = 0;
delay_ms(100);
OLED_RST = 1;
OLED_WR_Byte(0xAE,OLED_CMD); //关闭显示
OLED_WR_Byte(0xD5,OLED_CMD); //设置时钟分频因子,震荡频率
OLED_WR_Byte(80,OLED_CMD); //[3:0],分频因子;[7:4],震荡频率
OLED_WR_Byte(0xA8,OLED_CMD); //设置驱动路数
OLED_WR_Byte(0X3F,OLED_CMD); //默认0X3F(1/64)
OLED_WR_Byte(0xD3,OLED_CMD); //设置显示偏移
OLED_WR_Byte(0X00,OLED_CMD); //默认为0
OLED_WR_Byte(0x40,OLED_CMD); //设置显示开始行 [5:0],行数.
OLED_WR_Byte(0x8D,OLED_CMD); //电荷泵设置
OLED_WR_Byte(0x14,OLED_CMD); //bit2,开启/关闭
OLED_WR_Byte(0x20,OLED_CMD); //设置内存地址模式
OLED_WR_Byte(0x02,OLED_CMD); //[1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10;
OLED_WR_Byte(0xA1,OLED_CMD); //段重定义设置,bit0:0,0->0;1,0->127;
OLED_WR_Byte(0xC0,OLED_CMD); //设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数
OLED_WR_Byte(0xDA,OLED_CMD); //设置COM硬件引脚配置
OLED_WR_Byte(0x12,OLED_CMD); //[5:4]配置
OLED_WR_Byte(0x81,OLED_CMD); //对比度设置
OLED_WR_Byte(0xEF,OLED_CMD); //1~255;默认0X7F (亮度设置,越大越亮)
OLED_WR_Byte(0xD9,OLED_CMD); //设置预充电周期
OLED_WR_Byte(0xf1,OLED_CMD); //[3:0],PHASE 1;[7:4],PHASE 2;
OLED_WR_Byte(0xDB,OLED_CMD); //设置VCOMH 电压倍率
OLED_WR_Byte(0x30,OLED_CMD); //[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc;
OLED_WR_Byte(0xA4,OLED_CMD); //全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏)
OLED_WR_Byte(0xA6,OLED_CMD); //设置显示方式;bit0:1,反相显示;0,正常显示
OLED_WR_Byte(0xAF,OLED_CMD); //开启显示
OLED_Clear();
}
3.3 max30102传感器程序解析
#include "max30102.h"
#include "myiic.h"
#include "delay.h"
u8 max30102_Bus_Write(u8 Register_Address, u8 Word_Data)
{
/* 采用串行EEPROM随即读取指令序列,连续读取若干字节 */
/* 第1步:发起I2C总线启动信号 */
IIC_Start();
/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */
/* 第3步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第4步:发送字节地址 */
IIC_Send_Byte(Register_Address);
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第5步:开始写入数据 */
IIC_Send_Byte(Word_Data);
/* 第6步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 发送I2C总线停止信号 */
IIC_Stop();
return 1; /* 执行成功 */
cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
/* 发送I2C总线停止信号 */
IIC_Stop();
return 0;
}
u8 max30102_Bus_Read(u8 Register_Address)
{
u8 data;
/* 第1步:发起I2C总线启动信号 */
IIC_Start();
/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */
/* 第3步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第4步:发送字节地址, */
IIC_Send_Byte((uint8_t)Register_Address);
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第6步:重新启动I2C总线。下面开始读取数据 */
IIC_Start();
/* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_RD); /* 此处是读指令 */
/* 第8步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第9步:读取数据 */
{
data = IIC_Read_Byte(0); /* 读1个字节 */
IIC_NAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
}
/* 发送I2C总线停止信号 */
IIC_Stop();
return data; /* 执行成功 返回data值 */
cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
/* 发送I2C总线停止信号 */
IIC_Stop();
return 0;
}
void max30102_FIFO_ReadWords(u8 Register_Address,u16 Word_Data[][2],u8 count)
{
u8 i=0;
u8 no = count;
u8 data1, data2;
/* 第1步:发起I2C总线启动信号 */
IIC_Start();
/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */
/* 第3步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第4步:发送字节地址, */
IIC_Send_Byte((uint8_t)Register_Address);
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第6步:重新启动I2C总线。下面开始读取数据 */
IIC_Start();
/* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_RD); /* 此处是读指令 */
/* 第8步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第9步:读取数据 */
while (no)
{
data1 = IIC_Read_Byte(0);
IIC_Ack();
data2 = IIC_Read_Byte(0);
IIC_Ack();
Word_Data[i][0] = (((u16)data1 << 8) | data2); //
data1 = IIC_Read_Byte(0);
IIC_Ack();
data2 = IIC_Read_Byte(0);
if(1==no)
IIC_NAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
else
IIC_Ack();
Word_Data[i][1] = (((u16)data1 << 8) | data2);
no--;
i++;
}
/* 发送I2C总线停止信号 */
IIC_Stop();
cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
/* 发送I2C总线停止信号 */
IIC_Stop();
}
void max30102_FIFO_ReadBytes(u8 Register_Address,u8* Data)
{
max30102_Bus_Read(REG_INTR_STATUS_1);
max30102_Bus_Read(REG_INTR_STATUS_2);
/* 第1步:发起I2C总线启动信号 */
IIC_Start();
/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */
/* 第3步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第4步:发送字节地址, */
IIC_Send_Byte((uint8_t)Register_Address);
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第6步:重新启动I2C总线。下面开始读取数据 */
IIC_Start();
/* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_RD); /* 此处是读指令 */
/* 第8步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第9步:读取数据 */
Data[0] = IIC_Read_Byte(1);
Data[1] = IIC_Read_Byte(1);
Data[2] = IIC_Read_Byte(1);
Data[3] = IIC_Read_Byte(1);
Data[4] = IIC_Read_Byte(1);
Data[5] = IIC_Read_Byte(0);
/* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
/* 发送I2C总线停止信号 */
IIC_Stop();
cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
/* 发送I2C总线停止信号 */
IIC_Stop();
}
void max30102_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &GPIO_InitStructure);
IIC_Init();
max30102_reset();
max30102_Bus_Write(REG_INTR_ENABLE_1,0xc0); // INTR setting
max30102_Bus_Write(REG_INTR_ENABLE_2,0x00);
max30102_Bus_Write(REG_FIFO_WR_PTR,0x00); //FIFO_WR_PTR[4:0]
max30102_Bus_Write(REG_OVF_COUNTER,0x00); //OVF_COUNTER[4:0]
max30102_Bus_Write(REG_FIFO_RD_PTR,0x00); //FIFO_RD_PTR[4:0]
max30102_Bus_Write(REG_FIFO_CONFIG,0x0f); //sample avg = 1, fifo rollover=false, fifo almost full = 17
max30102_Bus_Write(REG_MODE_CONFIG,0x03); //0x02 for Red only, 0x03 for SpO2 mode 0x07 multimode LED
max30102_Bus_Write(REG_SPO2_CONFIG,0x27); // SPO2_ADC range = 4096nA, SPO2 sample rate (100 Hz), LED pulseWidth (400uS)
max30102_Bus_Write(REG_LED1_PA,0x24); //Choose value for ~ 7mA for LED1
max30102_Bus_Write(REG_LED2_PA,0x24); // Choose value for ~ 7mA for LED2
max30102_Bus_Write(REG_PILOT_PA,0x7f); // Choose value for ~ 25mA for Pilot LED
}
void max30102_reset(void)
{
max30102_Bus_Write(REG_MODE_CONFIG,0x40);
max30102_Bus_Write(REG_MODE_CONFIG,0x40);
}
void maxim_max30102_write_reg(uint8_t uch_addr, uint8_t uch_data)
{
IIC_Write_One_Byte(I2C_WRITE_ADDR,uch_addr,uch_data);
}
void maxim_max30102_read_reg(uint8_t uch_addr, uint8_t *puch_data)
{
IIC_Read_One_Byte(I2C_WRITE_ADDR,uch_addr,puch_data);
}
void maxim_max30102_read_fifo(uint32_t *pun_red_led, uint32_t *pun_ir_led)
{
uint32_t un_temp;
unsigned char uch_temp;
char ach_i2c_data[6];
*pun_red_led=0;
*pun_ir_led=0;
//read and clear status register
maxim_max30102_read_reg(REG_INTR_STATUS_1, &uch_temp);
maxim_max30102_read_reg(REG_INTR_STATUS_2, &uch_temp);
IIC_ReadBytes(I2C_WRITE_ADDR,REG_FIFO_DATA,(u8 *)ach_i2c_data,6);
un_temp=(unsigned char) ach_i2c_data[0];
un_temp<<=16;
*pun_red_led+=un_temp;
un_temp=(unsigned char) ach_i2c_data[1];
un_temp<<=8;
*pun_red_led+=un_temp;
un_temp=(unsigned char) ach_i2c_data[2];
*pun_red_led+=un_temp;
un_temp=(unsigned char) ach_i2c_data[3];
un_temp<<=16;
*pun_ir_led+=un_temp;
un_temp=(unsigned char) ach_i2c_data[4];
un_temp<<=8;
*pun_ir_led+=un_temp;
un_temp=(unsigned char) ach_i2c_data[5];
*pun_ir_led+=un_temp;
*pun_red_led&=0x03FFFF; //Mask MSB [23:18]
*pun_ir_led&=0x03FFFF; //Mask MSB [23:18]
}
本文章参考文章:
https://blog.csdn.net/richard_liujh/article/details/49615395