Published on
Fall 2020

Pulse Oximeter

A homemade pulse plethysmograph and pulse oximeter using an Arduino and sensor

image of device in use

Role

Engineering Lead

Skills Used

  • Arduino C++
  • Circuitry

Collaborators

none


Background

In my Medical Instrumentation class, our final project was to create a pulse oximeter using the very limited supplies we had in our kits because we were partaking in remote learning due to COVID-19. I designed the circuit, the Arduino code, and all of the testing criteria myself.


Block Diagrams

Hardware

circuit diagram
circuit picture

Software

signal analysis diagram

Final Design

Arduino Code
#define BUZZER 10
#define BUTTONDURATION 6
#define BUTTONSOUND 7

#include <Wire.h>
#include "MAX30105.h"
#include <PeakDetection.h>

MAX30105 particleSensor;

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET     4 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

PeakDetection peakDetection;
double starttime;

void setup()
{
  Serial.begin(115200); //Initialize serial monitor/plotter

  // Initialize button and buzzer pins
  pinMode(BUZZER, OUTPUT);
  pinMode(BUTTONDURATION, INPUT);
  pinMode(BUTTONSOUND, INPUT);

  // Initialize sensor
  if (!particleSensor.begin(Wire, 200)) //Use default I2C port, 400kHz speed
  {
    //Serial.println("MAX30105 was not found. Please check wiring/power. ");
    while (1);
  }

  //Setup to sense a nice looking saw tooth on the plotter
  byte ledBrightness = 0x1F; //Options: 0=Off to 255=50mA 1F=31
  byte sampleAverage = 8; //Options: 1, 2, 4, 8, 16, 32
  byte ledMode = 3; //Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
  int sampleRate = 100; //Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
  int pulseWidth = 411; //Options: 69, 118, 215, 411
  int adcRange = 4096; //Options: 2048, 4096, 8192, 16384

  particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);

  //Set up OLED
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextSize(1);
  display.setTextColor(WHITE,BLACK);
  display.clearDisplay();
  display.setCursor(0,24);

  peakDetection.begin(10, 1, 0.6); //Peak Detection presets: lag=32, threshold=2, influence=0.5
}

int count = 0;
double firsttime = 0;
double lasttime = 0;
bool BPMbelow = true;
int bpm1 = 0;
double redarray[100];
double irarray[100];
double peakarray[100];
int maximumred = 0;
int minimumred = 150000;
int maximumir = 0;
int minimumir = 150000;
float acquisitionduration = 10;
float difference = 0;
int k = 0;
double R = 0;

void loop()
{
  //Collect infrared and red light data, use peak detection on ir data
  double ir = particleSensor.getIR();
  double red = particleSensor.getRed();
  peakDetection.add(ir);
  double peak = peakDetection.getPeak();
  double peakprint = 1000 * peak + 120000;

  /*
  //Plots IR and red data as well as peaks
  Serial.print(ir); //Send raw data to plotter
  Serial.print(","); //Send raw data to plotter
  Serial.print(red); //Send raw data to plotter
  Serial.print(",");
  Serial.println(peakprint);
*/
////////////////////////////////////////////////////////////////////////////////////////////

  //Change acquisition duration to 5 seconds if button pressed
  if (digitalRead(BUTTONDURATION) == HIGH){
    acquisitionduration = 5;
  }

  else if (digitalRead(BUTTONDURATION) == LOW){
    acquisitionduration = 10;
  }

  //Measure Heart Rate
  if (peak > 0 && BPMbelow) //If detects peak, and there was not a peak before
  {
    BPMbelow = false; //Peak is occuring, measurement is not below threshold
    if (count == 0) //Takes time at first data point
    {
      firsttime = millis();
    }
  }

  if (peak <= 0 && !BPMbelow)
  {
    BPMbelow = true; //Peak is not occuring, measurement is below threshold
    count++; //Counts at the end of each beat
    lasttime = millis(); //Takes time at end of peak
    difference = (lasttime - firsttime) / 1000;
    if (digitalRead(BUTTONSOUND) == HIGH){
      tone(BUZZER,60,100); //If button pressed, piezo buzzer makes sound at every beat
    }
    //particleSensor.nextSample(); //Moves to next sample
  }

////////////////////////////////////////////////////////////////////////////////////////////

  redarray[k] = red; //Create array for red light values
  irarray[k] = ir; //Create array for IR light values

  //Measure modulation ratio R
  if (redarray[k] > maximumred){
    maximumred = redarray[k]; //Sets maximum of red
  }
  if (irarray[k] > maximumir){
    maximumir = irarray[k]; //Sets maximum of ir
  }
  if (redarray[k] < minimumred){
    minimumred = redarray[k]; //Sets minimum or red
  }
  if (irarray[k] <details minimumir){
    minimumir = irarray[k]; //Sets minimum of ir
  }

  if (k <div 99){
    k++;
  }
  else if (k == 99){ //Iterates through 100 samples, then resets
    k = 0;
    maximumred = 0;
    minimumred = 150000;
    maximumir = 0;
    minimumir = 150000;
  }

////////////////////////////////////////////////////////////////////////////////////////////

  if (difference >= acquisitionduration){ //Once time has reached acquisition duration
    bpm1 = count * 60 / difference; //Calculates bpm
    count = 0; //Resets count of beats to zero
    double redAC = maximumred - minimumred;
    double redDC = (minimumred+maximumred)/2;
    double redmeasure = redAC/redDC;
    double irAC = maximumir - minimumir;
    double irDC = (minimumir+maximumir)/2;
    double irmeasure = irAC/irDC;
    R = fabs(redmeasure/irmeasure); //Calculates modulation ratio
    difference = 0; //Resets time to zero
  }

////////////////////////////////////////////////////////////////////////////////////////////
  //Display values on OLED
  display.setCursor(0,24);
  display.clearDisplay();
  display.print("Pulse: ");
  display.print(bpm1); //Display current pulse on OLED in BPM
  display.setCursor(0,34);
  display.print("R: ");
  display.print(R); //Display current R for oxygen saturation
  display.display();
}

Testing

Heart Rate

Precision

circuit diagram

Accuracy

circuit picture

Oxygen Modulation Ratio (R)

Precision

circuit diagram

Accuracy

circuit picture

Limitations

  • Sensitive to movement, talking, and irregular breathing
  • Peak detection accuracy varies with age and skin type
  • Calculates and displays in void loop without interrupts - inaccurate time collection
  • Displays every 5 or 10 seconds rather than every heat beat or second
  • Heart rate
    • Precision specification met at 10 seconds, not 5 seconds
    • Almost all accuracy specifications met at 5 seconds, some met at 10 seconds
  • Modulation Ratio
    • Precision and accuracy specifications met in all cases
    • Calibration needed to acquire SpO2

Reflection

This project was quite challenging because we were sent a box of supplies at the beginning of the remote learning semester and any missing or broken components could not be replaced. Learning how to design and construct circuitry was also very difficult through a screen. However, given the circumstances, I am proud of the circuit and accompanying Arduino code that I created. Ever since this project, I have been a teaching assistant for this class (BME 354) for three semesters! Once remote and twice in-person! I really love the skills taught in this class and carry them with me through every project!