硬件:ESP8266(8.1RMB),MAX30102(8.5RMB),0.96寸IIC的OLED(10.7RMB)
连线:
SCL->D1(max30102与oled一样)
SDA->D2(max30102与oled一样)
按键一端接地,一端接D5
先上成品图(原谅它只是个原型机)
效果的话,这是与鱼跃的对比图,emmmmm,在数据稳定后,二者相差不大,效果图如下:
通过观察他们的算法应该是初始值设置成100,然后去拟合真实数据,而DIY的是由80去拟合真实数据,所以在数据开始到稳定的时间,DIY的会要30秒左右。这方面为了求数据真实性更好,就没做调整,因为如果将稳定时间缩短,会出现数据跳动幅度大的问题。
直接上源码:
#include <Wire.h>
#include "MAX30105.h" //sparkfun MAX3010X library
MAX30105 particleSensor;
#include <Arduino.h>
#include <U8g2lib.h>
#define bmp1_x 64
#define bmp1_y 64
static const unsigned char bmp1[] U8X8_PROGMEM = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xD0,0x00,0x00,0x00,0x00,0x0F,0x00,
0x00,0x90,0x07,0x00,0x00,0xC0,0x08,0x00,0x00,0x08,0x0F,0x00,0x00,0x70,0x10,0x00,0x00,0x08,0x1E,0x00,0x00,0x38,0x10,0x00,0x00,0x08,0x7D,0x00,0x00,0x1E,0x10,0x00,
0x00,0x08,0xFA,0x00,0x00,0x0F,0x30,0x00,0x00,0x04,0xF4,0x03,0xC0,0x07,0x20,0x00,0x00,0x04,0xE8,0x07,0xE0,0x13,0x30,0x00,0x00,0x04,0xFA,0xFF,0xFF,0x0B,0x20,0x00,
0x00,0x44,0xD4,0xFF,0xFF,0x27,0x20,0x00,0x00,0x84,0xE8,0xFF,0xFF,0x9F,0x30,0x00,0x00,0x04,0xF1,0xFF,0xFF,0x5F,0x30,0x00,0x00,0x04,0xFA,0x3F,0xFE,0x3F,0x10,0x00,
0x00,0x80,0xFC,0x1F,0xFC,0x7F,0x10,0x00,0x00,0x08,0xFE,0x0F,0xF8,0x7F,0x10,0x00,0x00,0x08,0xFF,0x07,0xF0,0xFF,0x10,0x00,0x00,0x88,0xFF,0x03,0xE0,0xFF,0x11,0x00,
0x00,0x88,0x0F,0x03,0xE0,0xF0,0x09,0x00,0x00,0xD0,0x03,0x00,0x00,0xC0,0x0B,0x00,0x00,0xD0,0x01,0x00,0x00,0x80,0x07,0x00,0x00,0xE0,0xE1,0x09,0x80,0x07,0x07,0x00,
0x00,0xE0,0xF0,0x03,0xC0,0x0F,0x07,0x00,0x00,0xE0,0xF8,0x17,0xE0,0x1F,0x07,0x00,0x00,0xE0,0xF8,0x07,0xE0,0x1F,0x07,0x00,0x00,0xE0,0xF8,0x07,0xE0,0x1F,0x07,0x00,
0x00,0xE0,0xF8,0x07,0xE0,0x1F,0x07,0x00,0x00,0xE0,0xF1,0x03,0xC0,0x8F,0x07,0x00,0x00,0xE0,0xE1,0x91,0x11,0xC3,0x07,0x00,0x00,0xE0,0x03,0x00,0x20,0xE0,0x07,0x00,
0x00,0xC0,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xC0,0x07,0x00,0x00,0x00,0x02,0x00,0x00,0x40,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x02,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x20,0x00,0x00,
0x00,0x00,0x08,0x00,0x00,0x10,0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0xC0,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
};
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
#define TIMEOUT 30 //Time out second to sleep
#define BOOTSOUND 440 //Hz
#define BLIPSOUND 440*2 //Hz A
// beep sounder
#define LEDC_CHANNEL_2 2
#define LEDC_TIMER_13_BIT 13
#define LEDC_BASE_FREQ 5000
int reset_key=0;
double aveRed = 0;//DC component of RED signal
double aveIr = 0;//DC component of IR signal
double sumIrRMS = 0; //sum of IR square
double sumRedRMS = 0; // sum of RED square
unsigned int i = 0; //loop counter
#define SUM_CYCLE 50
int Num = SUM_CYCLE ; //calculate SpO2 by this sampling interval
double eSpO2 = 95.0;//initial value of estimated SpO2
double fSpO2 = 0.7; //filter factor for estimated SpO2
double fRate = 0.95; //low pass filter for IR/red LED value to eliminate AC component
#define TIMETOBOOT 3000 // wait for this time(msec) to output SpO2
#define SCALE 88.0 //adjust to display heart beat and SpO2 in Arduino serial plotter at the same time
#define SAMPLING 1 //if you want to see heart beat more precisely , set SAMPLING to 1
#define FINGER_ON 50000 // if ir signal is lower than this , it indicates your finger is not on the sensor
#define MINIMUM_SPO2 80.0
#define MAX_SPO2 100.0
#define MIN_SPO2 80.0
void initSensor() {
//Setup to sense a nice looking saw tooth on the plotter
byte ledBrightness = 0x7F; //Options: 0=Off to 255=50mA
byte sampleAverage = 4; //Options: 1, 2, 4, 8, 16, 32
byte ledMode = 2; //Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
//Options: 1 = IR only, 2 = Red + IR on MH-ET LIVE MAX30102 board
int sampleRate = 200; //Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
int pulseWidth = 411; //Options: 69, 118, 215, 411
int adcRange = 16384; //Options: 2048, 4096, 8192, 16384
// Set up the wanted parameters
particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); //Configure sensor with these settings
}
char m_str[3];
int start_time,end_time,flag;
void setup()
{
Serial.begin(115200);
Serial.println("Initializing...");
u8g2.begin();
u8g2.enableUTF8Print();
pinMode(14, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(14), handleInterrupt, FALLING);
BEGIN();
// Wire.begin(6,10);
// Initialize sensor
if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) //Use default I2C port, 400kHz speed
{
Serial.println("MAX3010X was not found.");
Serial.println("Go to sleep. Bye");
}
initSensor();
}
ICACHE_RAM_ATTR void handleInterrupt() {
//interruptCounter++;
reset_key=1;
}
//
// Heart Rate Monitor by interval of zero crossing at falling edge
// max 180bpm - min 45bpm
#define FINGER_ON 70000 // if ir signal is lower than this , it indicates your finger is not on the sensor
#define LED_PERIOD 100 // light up LED for this period in msec when zero crossing is found for filtered IR signal
#define MAX_BPS 180
#define MIN_BPS 45
double HRM_estimator( double fir , double aveIr)
{
static double fbpmrate = 0.95; // low pass filter coefficient for HRM in bpm
static uint32_t crosstime = 0; //falling edge , zero crossing time in msec
static uint32_t crosstime_prev = 0;//previous falling edge , zero crossing time in msec
static double bpm = 70.0;
static double ebpm = 70.0;
static double eir = 0.0; //estimated lowpass filtered IR signal to find falling edge without notch
static double firrate = 0.85; //IR filter coefficient to remove notch , should be smaller than fRate
static double eir_prev = 0.0;
// Heart Rate Monitor by finding falling edge
eir = eir * firrate + fir * (1.0 - firrate); //estimated IR : low pass filtered IR signal
if ( ((eir - aveIr) * (eir_prev - aveIr) < 0 ) && ((eir - aveIr) < 0.0)) { //find zero cross at falling edge
crosstime = millis();//system time in msec of falling edge
//Serial.print(crosstime); Serial.print(","); Serial.println(crosstime_prev);
if ( ((crosstime - crosstime_prev ) > (60 * 1000 / MAX_BPS)) && ((crosstime - crosstime_prev ) < (60 * 1000 / MIN_BPS)) ) {
bpm = 60.0 * 1000.0 / (double)(crosstime - crosstime_prev) ; //get bpm
// Serial.println("crossed");
ebpm = ebpm * fbpmrate + (1.0 - fbpmrate) * bpm;//estimated bpm by low pass filtered
} else {
//Serial.println("faild to find falling edge");
}
crosstime_prev = crosstime;
}
eir_prev = eir;
return (ebpm);
}
int time_s=0;
unsigned int loopCnt = 0;
double SpO2 = 0; //raw SpO2 before low pass filtered
double Ebpm;//estimated Heart Rate (bpm)
double max_SpO2,max_Ebpm;
void loop()
{
if(reset_key==1){
flag=0;
initSensor();
reset_key=0;
BEGIN();
start_time=0,end_time=0;
}
uint32_t ir, red ;//raw data
double fred, fir; //floating point RED ana IR raw values
SpO2=0;
Ebpm=0;
particleSensor.check(); //Check the sensor, read up to 3 samples
if(flag!=2){
while (particleSensor.available()) {//do we have new data
red = particleSensor.getFIFOIR(); //why getFOFOIR output Red data by MAX30102 on MH-ET LIVE breakout board
ir = particleSensor.getFIFORed(); //why getFIFORed output IR data by MAX30102 on MH-ET LIVE breakout board
i++; loopCnt++;
fred = (double)red;
fir = (double)ir;
aveRed = aveRed * fRate + (double)red * (1.0 - fRate);//average red level by low pass filter
aveIr = aveIr * fRate + (double)ir * (1.0 - fRate); //average IR level by low pass filter
sumRedRMS += (fred - aveRed) * (fred - aveRed); //square sum of alternate component of red level
sumIrRMS += (fir - aveIr) * (fir - aveIr);//square sum of alternate component of IR level
Ebpm = HRM_estimator(fir, aveIr); //Ebpm is estimated BPM
if ((i % SAMPLING) == 0) {//slow down graph plotting speed for arduino Serial plotter by decimation
if ( millis() > TIMETOBOOT) {
float ir_forGraph = 2.0 * (fir - aveIr) / aveIr * SCALE + (MIN_SPO2 + MAX_SPO2) / 2.0;
float red_forGraph = 2.0 * (fred - aveRed) / aveRed * SCALE + (MIN_SPO2 + MAX_SPO2) / 2.0;
//trancation to avoid Serial plotter's autoscaling
if ( ir_forGraph > MAX_SPO2) ir_forGraph = MAX_SPO2;
if ( ir_forGraph < MIN_SPO2) ir_forGraph = MIN_SPO2;
if ( red_forGraph > MAX_SPO2 ) red_forGraph = MAX_SPO2;
if ( red_forGraph < MIN_SPO2 ) red_forGraph = MIN_SPO2;
// Serial.print(red); Serial.print(","); Serial.print(ir);Serial.print(".");
if ( ir < FINGER_ON) eSpO2 = MINIMUM_SPO2; //indicator for finger detached
#define PRINT
#ifdef PRINT
//Serial.print(bpm);// raw Heart Rate Monitor in bpm
//Serial.print(",");
if(ir > FINGER_ON&&flag==0){
flag=1;
Serial.print("开始测试");
u8g2.clearBuffer();
start_time=millis();
}
if(flag==1){
end_time=millis();
// time_s++;
time_s=(int)((end_time-start_time)/350);
if(max_SpO2<eSpO2){
max_SpO2=eSpO2;
}
print_result();
// TEST();
}
if(ir < FINGER_ON&&flag==1){
flag=2;
Errors();
break;
}
Serial.print(ir);// estimated Heart Rate Monitor in bpm
Serial.print(",");
Serial.print(Ebpm);// estimated Heart Rate Monitor in bpm
Serial.print(",");
// Serial.print(Eir - aveIr);
// Serial.print(",");
Serial.print(ir_forGraph); // to display pulse wave at the same time with SpO2 data
Serial.print(","); Serial.print(red_forGraph); // to display pulse wave at the same time with SpO2 data
Serial.print(",");
Serial.print(eSpO2); //low pass filtered SpO2
Serial.print("\n"); //low pass filtered SpO2
#else
Serial.print(fred); Serial.print(",");
Serial.print(aveRed); Serial.println();
// Serial.print(fir);Serial.print(",");
// Serial.print(aveIr);Serial.println();
#endif
}
}
if ((i % Num) == 0) {
double R = (sqrt(sumRedRMS) / aveRed) / (sqrt(sumIrRMS) / aveIr);
// Serial.println(R);
//#define MAXIMREFDESIGN
#ifdef MAXIMREFDESIGN
//https://github.com/MaximIntegratedRefDesTeam/RD117_ARDUINO/blob/master/algorithm.h
//uch_spo2_table is approximated as -45.060*ratioAverage* ratioAverage + 30.354 *ratioAverage + 94.845 ;
SpO2 = -45.060 * R * R + 30.354 * R + 94.845 ;
// SpO2 = 104.0 - 17.0*R; //from MAXIM Integrated https://pdfserv.maximintegrated.com/en/an/AN6409.pdf
#else
#define OFFSET 0.0
SpO2 = -23.3 * (R - 0.4) + 100 - OFFSET ; //http://ww1.microchip.com/downloads/jp/AppNotes/00001525B_JP.pdf
if (SpO2 > 100.0 ) SpO2 = 100.0;
#endif
eSpO2 = fSpO2 * eSpO2 + (1.0 - fSpO2) * SpO2;//low pass filter
// Serial.print(SpO2);Serial.print(",");Serial.println(eSpO2);
sumRedRMS = 0.0; sumIrRMS = 0.0; i = 0;//reset mean square at every interval
break;
}
particleSensor.nextSample(); //We're finished with this sample so move to next sample
//Serial.println(SpO2);
}
}}
void BEGIN(){
u8g2.clearBuffer();
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawXBMP(0,0, bmp1_x, bmp1_y, bmp1);
u8g2.setFont(u8g2_font_wqy12_t_gb2312b);
u8g2.drawUTF8(65, 10, "1.坐下平静");
u8g2.drawUTF8(65, 25, "2.放入食指");
u8g2.drawUTF8(65, 40, "3.轻轻按压");
u8g2.drawUTF8(65, 55, "4.等待30秒");
} while ( u8g2.nextPage() );
}
void TEST(){
char m_str[3];
strcpy(m_str, u8x8_u8toa(time_s, 3)); /* convert m to a string with two digits */
u8g2.setFont(u8g2_font_wqy12_t_gb2312b);
u8g2.drawUTF8(40, 10, "测试中....");
u8g2.drawUTF8(25, 25, "请保持平静状态");
u8g2.drawUTF8(15, 40, "以免测试结果不准确");
u8g2.drawFrame(0,50,102,12);
u8g2.drawLine(time_s,51,time_s, 61);
u8g2.drawStr(103,60,m_str);
u8g2.drawStr(122,60,"%");
u8g2.sendBuffer();
}
void Errors(){
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB10_tr);
u8g2.drawStr(20,20,"! ! Errors ! !"); // write something to the internal memory
u8g2.setFont(u8g2_font_wqy12_t_gb2312b);
u8g2.drawUTF8(30, 40, "请重新检测");
u8g2.drawUTF8(10, 55, "按下按键重新检测");
u8g2.sendBuffer();
}
void RESULT(){
u8g2.clearBuffer();
char m_str[3];
int eSP=0,BP=0;
if(eSpO2>(int)max_SpO2){
eSP=(int)max_SpO2+1;
}
else{
eSP=(int)max_SpO2;
}
if(Ebpm>(int)Ebpm){
BP=(int)Ebpm+1;
}
else{
BP=(int)Ebpm;
}
u8g2.setFont(u8g2_font_wqy12_t_gb2312b);
u8g2.drawUTF8(10, 10, "测试结果如下");
strcpy(m_str, u8x8_u8toa((int)eSP, 3));
u8g2.drawUTF8(10, 25, "血氧 :");
u8g2.drawStr(40,25,m_str);
strcpy(m_str, u8x8_u8toa((int)BP, 3));
u8g2.drawUTF8(30, 40, "心率 :");
u8g2.drawStr(70,40,m_str);
u8g2.drawUTF8(10, 55, "按下按键重新检测");
u8g2.sendBuffer();
}
void print_result(){
u8g2.clearBuffer();
char m_str[3];
int eSP=0,BP=0;
if(eSpO2>(int)eSpO2){
eSP=(int)eSpO2+1;
}
else{
eSP=(int)eSpO2;
}
if(Ebpm>(int)Ebpm){
BP=(int)Ebpm+1;
}
else{
BP=(int)Ebpm;
}
u8g2.setFont(u8g2_font_wqy12_t_gb2312b);
u8g2.drawUTF8(10, 10, "测试结果如下");
strcpy(m_str, u8x8_u8toa((int)eSP, 3));
u8g2.drawUTF8(10, 25, "血氧 :");
u8g2.drawStr(30,25,m_str);
strcpy(m_str, u8x8_u8toa((int)BP, 3));
u8g2.drawUTF8(70, 25, "心率 :");
u8g2.drawStr(100,25,m_str);
u8g2.drawUTF8(10, 55, "按下按键重新检测");
u8g2.sendBuffer();
}
有什么问题可以提出改进