基于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为什么大多数传感器都是采用的绿光呢?

我们先看看光谱的特点,从紫外线到红外线的波长是越来越长的。

之所以选择绿光作为光源是考虑到一下几个特点:

  1. 皮肤的黑色素会吸收大量波长较短的波

  2. 皮肤上的水份也会吸收大量的UV和IR部分的光

  3. 进入皮肤组织的绿光(500nm)-- 黄光(600nm)大部分会被红细胞吸收

  4. 红光和接近IR的光相比其他波长的光更容易穿过皮肤组织

  5. 血液要比其他组织吸收更多的光

  6. 相比红光,绿(绿-黄)光能被氧合血红蛋白和脱氧血红蛋白吸收

总体来说,绿光-- 红光能作为测量光源。早起多数采用红光为光源,随着进一步的研究和对比,绿光作为光源得到的信号更好,信噪比也比其他光源好些,所以现在大部分穿戴设备采用绿光为光源。但是考虑到皮肤情况的不用(肤色、汗水),高端产品会根据情况自动使用换绿光、红光和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);        //串口初始化为115200LED_Init();//OLEDOLED_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 秒)后的心率和 SpO2maxim_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 ONOLED_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 OFFOLED_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); //默认为0OLED_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) */elseIIC_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 settingmax30102_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 = 17max30102_Bus_Write(REG_MODE_CONFIG,0x03);    //0x02 for Red only, 0x03 for SpO2 mode 0x07 multimode LEDmax30102_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 LED1max30102_Bus_Write(REG_LED2_PA,0x24);      // Choose value for ~ 7mA for LED2max30102_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 registermaxim_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

项目下载:https://download.csdn.net/download/m0_57332128/85438882

基于STM32的心率血氧检测仪相关推荐

  1. 毕业设计 Stm32人体心率血氧无线监测系统 - 单片机 物联网

    文章目录 1 简介 2 绪论 2.1 课题背景与目的 3 系统设计 3.1 系统架构 3.2 关键硬件部分 3.2.1 MAX301 00 心率血氧模块 3.3 关键软件部分 3.3.1 数据读取流程 ...

  2. 京微齐力:基于HMEP060的心率血氧模块开发(1:FPGA发送多位指令)

    目录 日常·唠嗑: 实验结果 一.硬件解析 1.国产FPGA:HMEP060 2.MAX30102心率传感器模块 二.程序设计 1.波特率计算(25MHz时钟) 2.顶层模块 3.子模块 三.工程获取 ...

  3. STM32+Air202+Air530+HXDZ-30102-ACC心率血氧GPS采集上传到阿里云

    主要功能 HXDZ-30102-ACC采集心率血氧数据 STM32通过串口将数据转发到air202模块 air202将数据上传到阿里云平台进行展示与处理 整合合宙air530GPS模块,将定位数据上传 ...

  4. 【3】疯壳开源蓝牙智能健康手表(心率血压血氧心电监测可定制)_心率血氧采集

    心率血氧采集 该手表由两大块组成,分别是蓝牙 DA14580 的数据传输及显示以及内置我们疯壳优质算法的主 核心为 STM32 的"血压/血氧/心率/心电"四合一模组.在该实验中为 ...

  5. 实时监测心率血氧,血压,微循环,脉搏波,健康检测模块方案

    JFH142体表健康检测模块是惊帆科技研发的多光谱生理数据测量模块,用于人体体表的健康检测模块,可准确测量脉搏波形.心率值.血氧值和血管微循环 参数等信息.得益于获专利保护的前端传感器技术,模块灵敏度 ...

  6. 基于STM32单片机的大气压强检测仪(Proteus仿真+程序)

    编号:15 基于STM32单片机的大气压强检测仪 功能描述: 本设计由STM32单片机+BMP180大气压强检测模块+1602液晶显示模块组成. 1.主控制器是STM32单片机 2.利用BMP180传 ...

  7. 基于STM32F030、MAX30102血氧心率监测仪的设计(三)

    本篇主要记录一下开发过程中遇到的问题与解决. max30102模块是某宝上购买的,价格不贵5元左右,如下图所示. 使用引脚为SDA.SCL.INT.VIN.GND 拿到模块后,看着资料开始移植程序,我 ...

  8. 基于STM32F030、MAX30102血氧心率监测仪的设计(二)

    上篇主要讲解了MAX30102寄存器相关知识,这篇主要看下程序配置. MAX30102寄存器配置 在一般的配置中我们让设备开机直接开始进入SpO2/HR 模式(PROX_INT_EN 置 0),设置两 ...

  9. 【可穿戴算法开发】-基于PPG信号的血氧与血压检测模型

    文章目录 1.血氧检测方法 (1) 测量原理 0 检测原理 (2)标定试验 (3)基于线性回归的特征值R提取算法 (4)基于移动平均的特征值提取算法 2.血压监测方法 (1)原理 (2)计算公式 3. ...

最新文章

  1. R语言使用knitr生成机器学习模型全流程步骤示例:knitr与自动化结果报告、knitr常用参数
  2. Druid:一个用于大数据实时处理的开源分布式系统——大数据实时查询和分析的高容错、高性能开源分布式系统...
  3. boost::gil::scale_lanczos用法的测试程序
  4. PHP判断变量内容是什么编码(gbk?utf-8) mb_detect_encoding
  5. BellmanFord
  6. 引入外部化的配置文件
  7. HTTP管线化(HTTP pipelining)
  8. [网络编程] - MIME格式详细介绍[转]
  9. 基于Windows8与Visual Studio2012开发内核隐藏注册表
  10. Matplotlib 中文用户指南 4.1 文本介绍
  11. Akka构建Reactive应用《one》
  12. C#Const与static readonly的区别
  13. Day9--Python--函数入门
  14. 黄聪:C#中用ILMerge将所有引用的DLL和exe文件打成一个exe文件,有图解
  15. Nginx 多重判断
  16. JavaScript中String的replace函数
  17. hibernate教程笔记10
  18. Trapcode套装插件原创图文/视频安装教程
  19. Xmodem Ymodem Zmodem 协议
  20. 高效能人士的7个习惯

热门文章

  1. 孙玉 计算机教授,孙玉
  2. 数据挖掘(python实现)—认识数据
  3. 微信内置浏览器屏蔽网页链接怎么办,微信跳转外部浏览器的实现教程
  4. “华为区块链白皮书”重磅发布(附下载链接)
  5. 作为一名程序员,你觉得最重要的能力是什么?
  6. uniapp微信头像
  7. 根据oe抓取ebayno title fits
  8. c语言实验心得100字,实验心得100字_100个面试常见经典问题_100个面试问题和答案...
  9. VS2005 项目怎样添加“依赖”、“库目录”和“包含目录”
  10. 安装Deb软件的方法