使用Arduino开发的血氧仪,可观察血氧、心率和温度,搭载MAX30102、LM35和SSD1306模块。

 ↵本项目第一版本实现在arduino框架下通过MAX30102 对血氧和心率 进行实时监控,通过LM35 对温度进行监控 。所有数值在 ssd 1306 上进行显示。在血氧低过一定数值的时,设备会通过蜂鸣器发出警报。

第二版本实现手机实时监控并做数据分析(后续更新)

第三版本实现远程监控(后续更新)

**********************************************

本文面向完全新手的arduino及无编程经验人员。让大家低成本的制作一台血氧仪实时监控自己和家人的健康状态。

PS:单本设备不能替代专业医疗血氧仪,仅作补充使用

器材:

LM 35 温度传感器

SSD 1306 OLED 显示器

MAX30102 心率传感器

杜邦线 公母 公公 母母

蜂鸣器 micro bit51

arduino uno 

面包板

绝缘胶布

接线

 

 

LM35

arduino LM35
5V VCC
GND GND
A1 S

 SSD1306

arduino SSD1306
3V3 3V3
GND GND
A4 SDA
A5 SCL

蜂鸣器

arduino 蜂鸣器
5V VCC
GND GND
A1 S

MAX30102

arduino MAX30102
3V3 3V3
GND GND
A4 SDA
A5 SCL

******************************

如 max30102,ssd1306 同时需要连接A4 时,可以先连接面包板再连接进Arduino A4。

*****************************

按照上述接线完成后,需要用到软件arduino

Software | Arduino

 选择你要的版本;

搜索并安装以下库,点击install 安装 

 

 将代码复制进项目里:

#include <MAX3010x.h>
#include "filters.h"
#include <Adafruit_GFX.h>        //OLED   libraries
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
//#include <MAX30105extra.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3D ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Sensor (adjust to your sensor type)
MAX30105 sensor;
//MAX30105extra particleSensor;
const auto kSamplingRate = sensor.SAMPLING_RATE_400SPS;
const float kSamplingFrequency = 400.0;

// Finger Detection Threshold and Cooldown
const unsigned long kFingerThreshold = 10000;
const unsigned int kFingerCooldownMs = 500;

// Edge Detection Threshold (decrease for MAX30100)
const float kEdgeThreshold = -2000.0;

// Filters
const float kLowPassCutoff = 5.0;
const float kHighPassCutoff = 0.5;

// Averaging
const bool kEnableAveraging = false;
const int kAveragingSamples = 5;
const int kSampleThreshold = 5;

// limitation of sop2
const int spo2limit =95;
void setup() {
  Serial.begin(9600);
  display.begin(SSD1306_SWITCHCAPVCC,   0x3C); //Start the OLED display
  delay(3000);
  tone(3,1000);                                        //And   tone the buzzer for a 100ms you can reduce it it will be better
  delay(1000);
  noTone(3); 
  display.display();
  if(sensor.begin() && sensor.setSamplingRate(kSamplingRate)) { 
    Serial.println("Sensor initialized");
  }
  else {
    Serial.println("Sensor not found");  
    while(1);
  }
}

// Filter Instances
LowPassFilter low_pass_filter_red(kLowPassCutoff, kSamplingFrequency);
LowPassFilter low_pass_filter_ir(kLowPassCutoff, kSamplingFrequency);
HighPassFilter high_pass_filter(kHighPassCutoff, kSamplingFrequency);
Differentiator differentiator(kSamplingFrequency);
MovingAverageFilter<kAveragingSamples> averager_bpm;
MovingAverageFilter<kAveragingSamples> averager_r;
MovingAverageFilter<kAveragingSamples> averager_spo2;

// Statistic for pulse oximetry
MinMaxAvgStatistic stat_red;
MinMaxAvgStatistic stat_ir;

// R value to SpO2 calibration factors
// See https://www.maximintegrated.com/en/design/technical-documents/app-notes/6/6845.html
float kSpO2_A = 1.5958422;
float kSpO2_B = -34.6596622;
float kSpO2_C = 112.6898759;

// Timestamp of the last heartbeat
long last_heartbeat = 0;

// Timestamp for finger detection
long finger_timestamp = 0;
bool finger_detected = false;

// Last diff to detect zero crossing
float last_diff = NAN;
bool crossed = false;
long crossed_time = 0;

int lowsopcount =0;

void loop() {

  unsigned int val; //定义变量val
  unsigned int dat;//定义变量dat
  val=analogRead(1);//将val设置为读取到的A0的数值
  dat=(500 * val) /1024; //计算出当前温度数字dat
  auto sample = sensor.readSample(1000);
  float current_value_red = sample.red;
  float current_value_ir = sample.ir;
  // Detect Finger using raw sensor value
  if(sample.red > kFingerThreshold) {
    if(millis() - finger_timestamp > kFingerCooldownMs) {
      finger_detected = true;
    }
  }
  else {
    // Reset values if the finger is removed
    differentiator.reset();
    averager_bpm.reset();
    averager_r.reset();
    averager_spo2.reset();
    low_pass_filter_red.reset();
    low_pass_filter_ir.reset();
    high_pass_filter.reset();
    stat_red.reset();
    stat_ir.reset();
    
    finger_detected = false;
    finger_timestamp = millis();
  }

  if(finger_detected) {
    current_value_red = low_pass_filter_red.process(current_value_red);
    current_value_ir = low_pass_filter_ir.process(current_value_ir);

    // Statistics for pulse oximetry
    stat_red.process(current_value_red);
    stat_ir.process(current_value_ir);

    // Heart beat detection using value for red LED
    float current_value = high_pass_filter.process(current_value_red);
    float current_diff = differentiator.process(current_value);

    // Valid values?
    if(!isnan(current_diff) && !isnan(last_diff)) {
      
      // Detect Heartbeat - Zero-Crossing
      if(last_diff > 0 && current_diff < 0) {
        crossed = true;
        crossed_time = millis();
      }
      
      if(current_diff > 0) {
        crossed = false;
      }
  
      // Detect Heartbeat - Falling Edge Threshold
      if(crossed && current_diff < kEdgeThreshold) {
        if(last_heartbeat != 0 && crossed_time - last_heartbeat > 300) {
          // Show Results
          int bpm = 60000/(crossed_time - last_heartbeat);
          float rred = (stat_red.maximum()-stat_red.minimum())/stat_red.average();
          float rir = (stat_ir.maximum()-stat_ir.minimum())/stat_ir.average();
          float r = rred/rir;
          float spo2 = kSpO2_A * r * r + kSpO2_B * r + kSpO2_C;
          
          if(bpm > 50 && bpm < 250) {
            // Average?
            if(kEnableAveraging) {
              int average_bpm = averager_bpm.process(bpm);
              int average_r = averager_r.process(r);
              int average_spo2 = averager_spo2.process(spo2);
              
              // Show if enough samples have been collected
              if(averager_bpm.count() >= kSampleThreshold) {
                Serial.print("Time (ms): ");
                Serial.println(millis()); 
                Serial.print("Heart Rate (avg, bpm): ");
                Serial.println(average_bpm);
                Serial.print("R-Value (avg): ");
                Serial.println(average_r);  
                Serial.print("SpO2 (avg, %): ");
                Serial.println(average_spo2);
                if( average_spo2 >100) average_spo2 = 100;  
                display.clearDisplay();                                   //Clear the display       
                display.setTextSize(2);                                   //Near   it display the average BPM you can display the BPM if you want
                display.setTextColor(WHITE);   
                display.setCursor(15,0);                
                display.println("BPM");              
                display.setCursor(70,0);                
                display.println(bpm);
                display.setCursor(15,18);                
                display.println("SpO2");              
                display.setCursor(70,18);                
                display.println((int)average_spo2);  
                display.setCursor(15,36);                
                display.println("TMP");              
                display.setCursor(70,36);                
                display.println((int)dat);  

                display.display();

                if ((int)average_spo2 < spo2limit){
                  lowsopcount++;
                  if (lowsopcount >3) {
                      tone(3,1000);                                        //And   tone the buzzer for a 100ms you can reduce it it will be better
                      delay(1000);
                      noTone(3);                    
                  }
                }
                if((int)average_spo2 >spo2limit)    lowsopcount = 0;             
              }
            }
            else {
              Serial.print("Time (ms): ");
              Serial.println(millis()); 
              Serial.print("Heart Rate (current, bpm): ");
              Serial.println(bpm);  
              Serial.print("R-Value (current): ");
              Serial.println(r);
              Serial.print("SpO2 (current, %): ");
              Serial.println(spo2);
              if( spo2 >100) spo2 = 100;   
              display.clearDisplay();                                   //Clear the display       
              display.setTextSize(2);                                   //Near   it display the average BPM you can display the BPM if you want
              display.setTextColor(WHITE);   
              display.setCursor(15,0);                
              display.println("BPM");              
              display.setCursor(70,0);                
              display.println(bpm);
              display.setCursor(15,18);                
              display.println("SpO2");              
              display.setCursor(70,18);                
              display.println((int)spo2);
              display.setCursor(15,36);                
              display.println("TMP");              
              display.setCursor(70,36);                
              display.println((int)dat);    
              display.display();
              if ((int)spo2 < spo2limit){
                lowsopcount++;
                  if (lowsopcount >3) {
                      tone(3,1000);                                        //And   tone the buzzer for a 100ms you can reduce it it will be better
                      delay(1000);
                      noTone(3);                    
                  }
                }
              if((int)spo2 >spo2limit)    lowsopcount = 0;  
            }
          }

          // Reset statistic
          stat_red.reset();
          stat_ir.reset();
        }
  
        crossed = false;
        last_heartbeat = crossed_time;
      }
    }

    last_diff = current_diff;
  }
}

将arduino 插入电脑中

 选择你所用的arduino uno板

 选择你的Port 

***********************************

每台电脑的Port 口可能不一样,不影响代码导入

**********************************

 点击上传按钮将代码烧录进arduino uno里

 显示upload success,显示器显示adafruit的图案(杨桃),蜂鸣器发出声音表示代码正常导入arduino中。

将下面的代码命名为filters.h 并放在一起

#ifndef FILTERS_H
#define FILTERS_H

/**
 * @brief Statistic block for min/nax/avg
 */
class MinMaxAvgStatistic {
  float min_;
  float max_;
  float sum_;
  int count_;
public:
  /**
   * @brief Initialize the Statistic block
   */
  MinMaxAvgStatistic() :
    min_(NAN),
    max_(NAN),
    sum_(0),
    count_(0){}

  /**
   * @brief Add value to the statistic
   */
  void process(float value) {  
    min_ = min(min_, value);
    max_ = max(max_, value);
    sum_ += value;
    count_++;
  }

  /**
   * @brief Resets the stored values
   */
  void reset() {
    min_ = NAN;
    max_ = NAN;
    sum_ = 0;
    count_ = 0;
  }

  /**
   * @brief Get Minimum
   * @return Minimum Value
   */
  float minimum() const {
    return min_;
  }

  /**
   * @brief Get Maximum
   * @return Maximum Value
   */
  float maximum() const {
    return max_;
  }

  /**
   * @brief Get Average
   * @return Average Value
   */
  float average() const {
    return sum_/count_;
  }
};

/**
 * @brief High Pass Filter 
 */
class HighPassFilter {
  const float kX;
  const float kA0;
  const float kA1;
  const float kB1;
  float last_filter_value_;
  float last_raw_value_;
public:
  /**
   * @brief Initialize the High Pass Filter
   * @param samples Number of samples until decay to 36.8 %
   * @remark Sample number is an RC time-constant equivalent
   */
  HighPassFilter(float samples) :
    kX(exp(-1/samples)),
    kA0((1+kX)/2),
    kA1(-kA0),
    kB1(kX),
    last_filter_value_(NAN),
    last_raw_value_(NAN){}

  /**
   * @brief Initialize the High Pass Filter
   * @param cutoff Cutoff frequency
   * @pram sampling_frequency Sampling frequency
   */
  HighPassFilter(float cutoff, float sampling_frequency) :
    HighPassFilter(sampling_frequency/(cutoff*2*PI)){}

  /**
   * @brief Applies the high pass filter
   */
  float process(float value) { 
    if(isnan(last_filter_value_) || isnan(last_raw_value_)) {
      last_filter_value_ = 0.0;
    }
    else {
      last_filter_value_ = 
        kA0 * value 
        + kA1 * last_raw_value_ 
        + kB1 * last_filter_value_;
    }
    
    last_raw_value_ = value;
    return last_filter_value_;
  }

  /**
   * @brief Resets the stored values
   */
  void reset() {
    last_raw_value_ = NAN;
    last_filter_value_ = NAN;
  }
};

/**
 * @brief Low Pass Filter 
 */
class LowPassFilter {
  const float kX;
  const float kA0;
  const float kB1;
  float last_value_;
public:
  /**
   * @brief Initialize the Low Pass Filter
   * @param samples Number of samples until decay to 36.8 %
   * @remark Sample number is an RC time-constant equivalent
   */
  LowPassFilter(float samples) :
    kX(exp(-1/samples)),
    kA0(1-kX),
    kB1(kX),
    last_value_(NAN){}

  /**
   * @brief Initialize the Low Pass Filter
   * @param cutoff Cutoff frequency
   * @pram sampling_frequency Sampling frequency
   */
  LowPassFilter(float cutoff, float sampling_frequency) :
    LowPassFilter(sampling_frequency/(cutoff*2*PI)){}

  /**
   * @brief Applies the low pass filter
   */
  float process(float value) {  
    if(isnan(last_value_)) {
      last_value_ = value;
    }
    else {  
      last_value_ = kA0 * value + kB1 * last_value_;
    }
    return last_value_;
  }

  /**
   * @brief Resets the stored values
   */
  void reset() {
    last_value_ = NAN;
  }
};

/**
 * @brief Differentiator
 */
class Differentiator {
  const float kSamplingFrequency;
  float last_value_;
public:
  /**
   * @brief Initializes the differentiator
   */
  Differentiator(float sampling_frequency) :
    kSamplingFrequency(sampling_frequency),
    last_value_(NAN){}

  /**
   * @brief Applies the differentiator
   */
  float process(float value) {  
      float diff = (value-last_value_)*kSamplingFrequency;
      last_value_ = value;
      return diff;
  }

  /**
   * @brief Resets the stored values
   */
  void reset() {
    last_value_ = NAN;
  }
};

/**
 * @brief MovingAverageFilter
 * @tparam buffer_size Number of samples to average over
 */
template<int kBufferSize> class MovingAverageFilter {
  int index_;
  int count_;
  float values_[kBufferSize];
public:
  /**
   * @brief Initalize moving average filter
   */
  MovingAverageFilter() :
    index_(0),
    count_(0){}

  /**
   * @brief Applies the moving average filter
   */
  float process(float value) {  
      // Add value
      values_[index_] = value;

      // Increase index and count
      index_ = (index_ + 1) % kBufferSize;
      if(count_ < kBufferSize) {
        count_++;  
      }

      // Calculate sum
      float sum = 0.0;
      for(int i = 0; i < count_; i++) {
          sum += values_[i];
      }

      // Calculate average
      return sum/count_;
  }

  /**
   * @brief Resets the stored values
   */
  void reset() {
    index_ = 0;
    count_ = 0;
  }

  /**
   * @brief Get number of samples
   * @return Number of stored samples
   */
  int count() const {
    return count_;
  }
};

#endif // FILTERS_H

备注:

Max30102可以在外围一圈包裹上绝缘胶布以提高其精准性

杜邦线也可以用绝缘胶布进行稳定

代码在upload 的时候可能会出错显示有的库未找到,再次upload就行。

物联沃分享整理
物联沃-IOTWORD物联网 » 使用Arduino开发的血氧仪,可观察血氧、心率和温度,搭载MAX30102、LM35和SSD1306模块。

发表评论