Skip to content
Snippets Groups Projects

Pulse Oximetry

Pulse oximetry devices use several LEDs to measure pulse rate and blood oxygen content. The LEDs are tuned to specific wavelengths corresponding to the absorbance bands of oxygenated and reduced hemoglobin; by cycling through the LEDs rapidly the device compensates for skin differences and ambient light, returning saturation and pulse rate.

References

  • overview of pulse oximetry physics and engineering challenges from 1989: Tremper, Kevin K., and Steven J. Barker. "Pulse oximetry." Anesthesiology: The Journal of the American Society of Anesthesiologists 70.1 (1989): 98-108.
    • engineering challenges identified
      • LED center wavelength consistency
      • the other two hemoglobins (MetHb and COHb)
      • signal artifacts: physical movement, signal:noise ratio, ambient light
      • calibration curve accuracy
  • lots of IP from Masimo Corp
  • earlier overview: Yelderman, Mark, and William New. "Evaluation of pulse oximetry." Anesthesiology: The Journal of the American Society of Anesthesiologists 59.4 (1983): 349-351.
  • changing LED wavelengths with temp: ~0.1 nm/C: Reynolds, K. J., et al. "Temperature dependence of LED and its theoretical effect on pulse oximetry." British journal of anaesthesia 67.5 (1991): 638-643.
    • "... equation (2) is only an approximation and pulse oximeters are usually calibrated empirically using data obtained by inducing hypoxia in healthy volunteers."
  • detailed discussion of pulse-ox machine design: Pologe, Jonas A. "Pulse oximetry: technical aspects of machine design." International anesthesiology clinics 25.3 (1987): 137-153.
  • a design study weighing the relative merits of different pulse-ox probe types for a low-cost device: Parlato, Matthew Brian, et al. "Low cost pulse oximeter probe." Conjunction with Engineering, World Health and the MEdCal Project (2009).

Commercial Example

A quick teardown of a ~$20 Zacurate 500BL from Walgreens revealed no integrated photonics package or signal processing ASIC; instead, the device uses a bi-color IR/red LED on one side of a spring-loaded plastic clam-shell and a PCB with a decent sized photodiode on the other, paired with an SGM8634 op-amp and an STM32F100-series 32-bit Arm Cortex M3 microcontroller. The display is a custom multi-segment LED device, but the PCB labels suggest an OLED is used for an alternate model. TX/RX test points were spotted that could be investigated further; with any luck, these could be used to pull live data out of the instrument.

pulseox1

pulseox2

pulseox3

Operational Theory

Pulse oximetry is based on the Beer-Lambert law, a principle that relates the concentration of a species to the attenuation of light through a sample:

I=I_{in}e^{-(DC\epsilon)}

where I is the intensity of light transmitted through the sample; I_{in} is the intensity of the light prior to absorption by the sample; D is the optical path length; C is the solute concentration; and \epsilon is the extinction coefficient, the sample's absorption at a given wavelength of light. For a multi-species compound, the three terms D, C, and \epsilon for each individual species are combined:

I=I_{in}e^{-(D_1C_1\epsilon_1+D_2C_2\epsilon_2+\dots+D_nC_n\epsilon_n)}

Typical commercial pulse oximeters use a red LED (660 nm) and an IR LED (940 nm) to quantify the relative concentration of reduced and oxygen-rich hemoglobin in a person's bloodstream based on the following absorbance curves:

hemoglobin_curve

Figure source: Bülbül, Ali & Küçük, Serdar. (2016). Pulse Oximeter Manufacturing & Wireless Telemetry for Ventilation Oxygen Support. International Journal of Applied Mathematics, Electronics and Computers. 211-211. 10.18100/ijamec.270309.

In order to differentiate the slight intensity change caused by varying blood oxygen concentration from errors related to skin absorbance and venous blood (whose oxygen has already been taken up by cells), the signal processing algorithm isolates the AC portion of the signal, since within a reasonable range (~0.5 - 3 Hz) this corresponds to blood rushing through arteries with each heartbeat. This pulsatile arterial blood increases the optical path length of the measurement as blood pressure swells the arteries, producing periodic oscillations in the absorption signal. The other contributors to absorption, such as tissue and venous/capillary blood, are effectively constant in this frequency regime. By calculating the ratio of the AC and DC signals at each wavelength, then taking the ratio of these two absorption ratios, a value R can be determined which is only related to the relative concentration of oxyhemoglobin (O2Hb) and reduced hemoglobin (Hb):

R=\frac{A_{AC_{660}}/A_{DC_{660}}}{A_{AC_{940}}/A_{DC_{940}}}

As the photodiode sensor does not differentiate by wavelength, the device rapidly cycles between red, IR, and no LED, allowing the system to compensate for ambient light variation as well. The cycling speed must be substantially faster than the heart rate, since the ratio R assumes absorption at all wavelengths is carried out simultaneously in order to cancel out path length. R is then related to SpO2 using an empirically determined curve:

pulseox_curve

Figure source, via Ohmeda Corp: Pologe, Jonas A. "Pulse oximetry: technical aspects of machine design." International anesthesiology clinics 25.3 (1987): 137-153.

Note that methemoglobin (MetHb) and carboxyhemoglobin (COHb) are not factored in with this method and will thus cause systematic errors; the above calculation assumes these two compounds are minimally present. Additional wavelengths are needed to quantify all four hemoglobin species.

Apparatus

An apparatus was constructed to simultaneously gather raw sensor data and calculated SpO2 from the Zacurate 500BL sensor described above, along with a simple fabricated sensor. The apparatus consists of a few parts:

  • an OpenMV machine vision camera mounted on a 3D printed bracket watching the SpO2 display
  • a 3D printed cuff for the fabricated sensor with an IR and red LED, along with a photodiode and high-gain op-amp circuit
  • a Teensy 4.0 development board to perform data logging (analog and UART) and LED control

apparatus

The 3D printed parts were designed in Fusion360; both native and STEP files are available in the cad directory:

models

The OpenMV code is relatively simple; since the 3D printed bracket holds the camera in a fixed location, the segment LED states are identified by checking average illumination values for defined pixel rectangles. One could also imagine directly tapping into the LED display driver lines, but the scanning speed of the display matrix made this complicated (and this is a good excuse to try out an OpenMV board):

import sensor, image, time, ustruct
from pyb import UART

sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE) # or RGB565.
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(time = 2000)
sensor.set_auto_gain(False) # must be turned off for color tracking
sensor.set_auto_whitebal(False) # must be turned off for color tracking
clock = time.clock()
uart = UART(3, 19200)
seg_thresh = 60
SpO2 = 0;

#        ---A1---     ---A2---
#       |        |   |        |
#       F1       B1  F2       B2
#       |        |   |        |
#        ---G1---     ---G2---
#       |        |   |        |
#       E1       C1  E2       C2
#       |        |   |        |
#        ---D1---     ---D2---

while(True):
    clock.tick()
    SpO2 = 0;
    img = sensor.snapshot()

    # segment 1 (left)
    if(img.get_statistics(roi=(43,50,8,20)).mean() > seg_thresh): #F1
        if(img.get_statistics(roi=(45,131,6,17)).mean() > seg_thresh): #E1
            if(img.get_statistics(roi=(77,96,18,7)).mean() > seg_thresh): #G1
                if(img.get_statistics(roi=(117,50,9,17)).mean() > seg_thresh): #B1
                    SpO2 += 80
                else:
                    SpO2 += 60
            else:
                SpO2 += 0
        elif(img.get_statistics(roi=(79,174,15,8)).mean() > seg_thresh): #D1
            if(img.get_statistics(roi=(117,50,9,17)).mean() > seg_thresh): #B1
                SpO2 += 90
            else:
                SpO2 += 50
        else:
            SpO2 += 40
    elif(img.get_statistics(roi=(77,96,18,7)).mean() > seg_thresh): #G1
        if(img.get_statistics(roi=(70,16,22,10)).mean() > seg_thresh): #A1
            if(img.get_statistics(roi=(45,131,6,17)).mean() > seg_thresh): #E1
                SpO2 += 20
            else:
                SpO2 += 30
        else:
            SpO2 = 0
    elif(img.get_statistics(roi=(70,16,22,10)).mean() > seg_thresh): #A1
        SpO2 += 70
    elif(img.get_statistics(roi=(117,50,9,17)).mean() > seg_thresh): #B1
        SpO2 += 10
    else:
        SpO2 += 0

    # segment 2 (right)
    if(img.get_statistics(roi=(154,47,10,22)).mean() > seg_thresh): #F1
        if(img.get_statistics(roi=(156,133,10,19)).mean() > seg_thresh): #E1
            if(img.get_statistics(roi=(188,94,18,8)).mean() > seg_thresh): #G1
                if(img.get_statistics(roi=(234,43,8,22)).mean() > seg_thresh): #B1
                    SpO2 += 8
                else:
                    SpO2 += 6
            else:
                SpO2 += 0
        elif(img.get_statistics(roi=(188,176,21,8)).mean() > seg_thresh): #D1
            if(img.get_statistics(roi=(234,43,8,22)).mean() > seg_thresh): #B1
                SpO2 += 9
            else:
                SpO2 += 5
        else:
            SpO2 += 4
    elif(img.get_statistics(roi=(188,94,18,8)).mean() > seg_thresh): #G1
        if(img.get_statistics(roi=(183,13,21,8)).mean() > seg_thresh): #A1
            if(img.get_statistics(roi=(156,133,10,19)).mean() > seg_thresh): #E1
                SpO2 += 2
            else:
                SpO2 += 3
        else:
            SpO2 = 0
    elif(img.get_statistics(roi=(183,13,21,8)).mean() > seg_thresh): #A1
        SpO2 += 7
    elif(img.get_statistics(roi=(234,43,8,22)).mean() > seg_thresh): #B1
        SpO2 += 1
    else:
        SpO2 += 0
    uart.write(ustruct.pack("<b", SpO2))
    print(SpO2)

The Teensy 4.0 firmware is similarly straightforward. Some considerations are made to store a reasonably large number of samples (25k) in RAM before periodically dumping the array into the SD card, to avoid constant write operations. Precise timing is ensured by capturing samples on a microsecond-accurate timer interrupt:

#include <IntervalTimer.h>
#include <SD.h>
#include <SPI.h>
#define LED_red 0
#define LED_IR 1

int SpO2_OpenMV = 0;
int SpO2_Raw_Zacurate = 0;
int SpO2_Raw_Fab = 0;

int Arr_SpO2_OpenMV[25000];
int Arr_SpO2_Raw_Zacurate[25000];
int Arr_SpO2_Raw_Fab[25000];
int Arr_micros[25000];

int counter = 0;
int led_counter = 0;

IntervalTimer sampleTimer;
IntervalTimer ledTimer;

void writelog() {
  Arr_SpO2_OpenMV[counter] = SpO2_OpenMV;
  Arr_SpO2_Raw_Zacurate[counter] = SpO2_Raw_Zacurate;
  Arr_SpO2_Raw_Fab[counter] = SpO2_Raw_Fab;
  Arr_micros[counter] = micros();
  counter++;
}

void updateLEDs() {
  if (led_counter < 6) {
    digitalWrite(0, HIGH);
  }
  else if (led_counter < 12) {
    digitalWrite(0, LOW);
  }
  else if (led_counter < 18) {
    digitalWrite(1, HIGH);
  }
  else {
    digitalWrite(1, LOW);
  }
  led_counter++;
  if (led_counter > 72) {
    led_counter = 0;
  }
}

void setup() {
  Serial.begin(115200); //USB serial port
  Serial5.begin(19200); //OpenMV UART
  analogReadResolution(16);
  pinMode(0, OUTPUT);
  pinMode(1, OUTPUT);
  pinMode(13, OUTPUT);
  SD.begin(BUILTIN_SDCARD);
  delay(5000); //mostly to let the OpenMV board boot up
  sampleTimer.begin(writelog, 200); // 200 us period
  ledTimer.begin(updateLEDs, 100); // 100 us period
}

void loop() {
  int i;
  digitalWrite(13, HIGH);
  if (Serial5.available() > 0) {
    SpO2_OpenMV = Serial5.read();
  }
  SpO2_Raw_Zacurate = analogRead(4);
  SpO2_Raw_Fab = analogRead(9);

  if (counter == 25000) {
    noInterrupts();
    File dataFile = SD.open("datalog.txt", FILE_WRITE);
    if (dataFile) {
      for (i = 0; i < 25000; i++) {
        dataFile.print(Arr_micros[i]);
        dataFile.print(",");
        dataFile.print(Arr_SpO2_OpenMV[i]);
        dataFile.print(",");
        dataFile.print(Arr_SpO2_Raw_Zacurate[i]);
        dataFile.print(",");
        dataFile.println(Arr_SpO2_Raw_Fab[i]);  
      }
      dataFile.close();
    }
    counter = 0;
    interrupts();
  }
}

The fabricated sensor board was designed in KiCad around a commonly available op-amp and photodiode:

schematic

... and laid out for single-sided PCB milling using a 1/64" mill:

layout

After assembly, the PCBs required a bit of sanding for a snug fit in the finger sleeve. Prior to use, a cut piece of neoprene was adhered to one side to provide padding and a bit of finger size accomodation.

See my NMM final project page for a first pass at data analysis.

Practical Considerations

Commercial pulse oximeters trace their calibrations back to empirical studies on human volunteers whose blood oxygenation is simultaneously observed using an invasive measurement device. To avoid needing to repeat this process for every device that is manufactured, designers rely on pre-assembly binning or per-unit spectroscopy testing to compensate for LED wavelength variation, and likely perform extensive electrical testing to ensure photodiode and amplifier differences are accounted for. The spirit of this exercise, open, low-cost devices that can be made anywhere and remain useful, means these techniques aren't particularly useful.

A few paths exist that may be worth pursuing, given the aforementioned concerns:

  • Build an uncalibrated device that allows users to track changes in their blood oxygenation over time. Even without an absolute reference in terms of SpO2, this data could be used as an early warning for respiratory ailments. This fits with the use case, too; clinical devices need to be usable as spot-check instruments, where as a personal device could be used for weeks or months by one person.
  • Develop a calibration system that can be easily manufactured and deployed based on fundamental principles, i.e. one that does not need to be itself calibrated. One could build a spinning hollow clear plastic wheel with two chambers and controlled thickness, with the chambers filled with various concentrations of a solution whose absorption spectrum closely matches that of blood at a given oxygenation level. The wheel would be spun to simulate the heartbeat, and different wheels would represent different SpO2 values. The solution could be accurately mixed using basic laboratory equipment, such as a scale or a pipette.
  • Design an automated calibration system that uses a camera and optical character recognition to gather SpO2 values from a commercial or clinical instrument and build a calibration table for the low-cost device while it is simultaneously clipped to the patient. Caregivers could "train" the low-cost device prior to patient discharge so they can self-monitor for flare-ups or subsequent respiratory ailments.
  • Develop a methodology for cheaply and accurately characterizing LEDs and other components in the low-cost sensor, so that a master calibration file from a clinical study can be propagated to other devices as is done by traditional manufacturers.

In all cases, a reasonable first step is to design and prototype a sensor with sufficient performance to measure R, the O2Hb / Hb ratio discussed above.