USD

Design a Low-Cost Pulse Oximeter Using Off-the-Shelf Components

By Stephen Evanczuk

Contributed By Digi-Key's North American Editors

Pulse oximetry measures peripheral oxygen saturation (SpO2), which reflects the effectiveness of the cardiopulmonary system to provide oxygen-rich blood to the body. Athletes use SpO2 measurements to gauge their level of effort in workouts, but these measurements have gained greater importance during the COVID-19 pandemic. Healthcare providers watch for a decrease in SpO2 as an early warning sign of damage to lung tissue caused by the SARS-CoV-2 virus that causes COVID-19.

For affected individuals with mild symptoms told to quarantine at home, ready access to a low-cost pulse oximeter can help gauge the course of their infection and provide the warning needed to escalate healthcare.

This article briefly discusses the symptoms of COVID-19 and the need for SpO2 monitoring. It then shows how developers can use a Microchip Technology digital signal controller (DSC) and a few additional devices to design a low-cost pulse oximeter able to provide home users with early warning of symptoms consistent with advanced COVID-19 infection.

COVID-19 and the need to measure oxygen saturation levels

COVID-19 presents with a broad spectrum of symptoms resulting from the damaging effects of the SARS-CoV-2 virus. For healthcare providers, a particularly worrisome symptom relates to lung tissue damage, resulting in a compromised respiratory system and reduced oxygen uptake. Although physicians use individual chest X-rays and computed tomography (CT) scans to confirm this stage of COVID-19, they routinely use SpO2 measurements as an early indicator.

SpO2 measurement is a non-invasive alternative to measurement of arterial oxygen saturation (SaO2) determined directly by analyzing blood gas levels in samples extracted from an artery of the patient. Although some conditions may require direct arterial blood gas measurement, SpO2 has been found to provide a reliable estimate of SaO2. Perhaps most important, it can be performed just as reliably at home as in clinical settings using optical pulse oximeters.

Optical pulse oximeters measure SpO2 by taking advantage of the differences in light absorption exhibited by deoxygenated hemoglobin (Hb) and oxygenated hemoglobin (HbO2). Carried in red blood cells, hemoglobin quickly forms a reversible bond with up to four oxygen molecules within the oxygen-rich lungs. In this state as HbO2, the molecule absorbs more light at 940 nanometers (nm) than at 660 nm (Figure 1).

Graph of differences in absorption spectra between oxygenated (HbO2) and deoxygenated (Hb) blood cellsFigure 1: Pulse oximetry takes advantage of the differences in absorption spectra between oxygenated (HbO2) and deoxygenated (Hb) blood cells. (Image source: Wikipedia)

As the HbO2-carrying red blood cells pass to the periphery where the partial pressure—the pressure of a single gas component in a mixture of gases—of oxygen is lower, hemoglobin's affinity for oxygen decreases and HbO2 begins to unload its oxygen molecules, eventually becoming Hb. In this deoxygenated state, the molecule's light absorption spectrum changes, absorbing more light at 660 nm than at 940 nm.

Because HbO2 becomes Hb when oxygen partial pressure is low, SpO2 can be determined by the simple formula:

SpO2 = HbO2 / (HbO2 + Hb)

In turn, the relative concentrations of Hb and HbO2 in the blood stream can be determined by measuring light absorption at 660 nm and 940 nm wavelengths.

Pulse oximeters take advantage of the relationship between blood oxygen partial pressure, hemoglobin oxygen load, and differences in light absorption to provide reliable measurements of SpO2.

Key subsystems of a typical pulse oximeter

A typical pulse oximeter design comprises three major subsystems:

  • A light delivery subsystem including analog switches and drivers along with light emitting diodes (LEDs) at red (660 nm) and infrared (IR) (950 nm) wavelengths. Some systems also include green (530 nm) sources for use with photoplethysmography (PPG) methods that determine heart rate by monitoring changes in volume in skin blood vessels.
  • A light detection subsystem including a photodiode, signal conditioning chain, and analog-to-digital converter (ADC).
  • A DSC or microcontroller to coordinate the light delivery and detection subsystems as well as calculate SpO2 from the measured data.

Although these basic subsystems exist in any pulse oximeter, their implementation can vary significantly. In transmissive pulse oximeters, the photodiode is placed on the opposite side of the user's finger or earlobe from the LEDs. Commonly available finger clip units combine red, IR, and optional green LEDs on one side of the clip with a photodiode on the other. In reflectance pulse oximeters, the photodiode and LEDs are placed on the same side of the skin with some optical barrier placed between them to reduce artifacts. For example, OSRAM's SFH7060 is a drop-in reflectance measurement device that packages LEDs and a photodiode in a single 7.2 x 2.5 x 0.9 millimeter (mm) package.

Whether using these optical packages for transmissive or reflectance methods, designers require relatively few additional components to implement a low-cost pulse oximeter design capable of providing home users with information suggesting the need for further evaluation by healthcare professionals. An example design built around a Microchip Technology DSPIC33FJ128GP802 DSC uses the microcontroller's integrated peripherals to control illumination of the skin by red and IR LEDs and to digitize the conditioned photodiode output signal (Figure 2).

Diagram of Microchip typical pulse oximeter designFigure 2: A typical pulse oximeter design combines subsystems for LED illumination and photodiode signal processing with a microcontroller used to control timing of illumination and data acquisition. (Image source: Microchip Technology)

Pulse oximeter designs typically rely on a single photodiode with a broad spectral response curve to capture the transmitted or reflected signal regardless of the illumination source. To ensure that the received signal corresponds to just red or IR wavelengths, hardware or software control logic presents only the red or IR illumination source at a given time, alternating between the two sources to complete a sequence of measurements.

Implementing a low-cost pulse oximeter hardware design

In this design, the DSC uses an external Microchip Technology MCP4728 digital-to-analog converter (DAC) to set separate MBT2222 transistors at the level needed to drive each LED at the required intensity. To precisely time the "on" sequence for each LED, the the DSC uses two of its pulse width modulation (PWM) outputs to control Analog Devices' ADG884 analog switch (Figure 3).

Diagram of an analog switch enables drive current to red and IR LEDsFigure 3: Driven by alternating signals for red and IR channels from the digital controller, an analog switch enables drive current to red and IR LEDs. (Image source: Microchip Technology)

To process the photodiode output, a single Microchip Technology MCP6002 device provides a pair of operational amplifiers needed to implement a basic two-stage signal conditioning chain. Here, the first stage uses one MCP6002 op amp, configured as a transimpedance amplifier, to convert the photodiode's current output to a voltage signal. Following a high-pass filter to reduce noise, the second op amp in the MCP6002 provides a gain and DC offset adjustment needed to optimize the conditioned signal's swing across the full range of the ADC integrated in the DSC (Figure 4).

Diagram of two-stage signal chain conditions photodiode output for deliveryFigure 4: A two-stage signal chain conditions photodiode output for delivery to the digital controller's integrated ADC. (Image source: Microchip Technology)

In operation, the DSC uses its PWM outputs and ADC inputs to synchronize LED illumination and ADC digitization of the conditioned photodiode output signal. Here, each alternating red and IR illumination period is coordinated with signal acquisition and conversion. An additional ADC sample taken when both LEDs are off provides a measurement of ambient light used to optimize LED intensity and SpO2 measurement. The result is a precisely controlled sequence of events that coordinate LED illumination and ADC digitization to capture red wavelength results for Hb, capture ambient light, and finally capture IR wavelength results for HbO2 (Figure 5).

Diagram of Microchip low-cost pulse oximeter's functionalityFigure 5: The low-cost pulse oximeter's functionality relies on the digital signal controller's ability to manage the precise timing of sequences for illumination and data capture required to collect measurements for SpO2 determination. (Image source: Microchip Technology)

Implementing an interrupt driven software solution

Microchip provides a pulse oximeter firmware package with a sample program that demonstrates use of the DSC to perform these illumination control and data conversion sequences. Here, the program implements an interrupt driven method using a pair of DSC timers—Timer2 and Timer3—to time the separate "on" sequences of the IR LED and red LED, respectively. In turn, each timer provides the time base for two of the DSC's output compare (OC) modules, OC1 and OC2, used to control the analog switches for the IR LED and red LED, respectively.

As shown in Listing 1, the software first initializes Timer2 and Timer3 to set the desired period of the illumination cycle and enable interrupts. As part of their initialization sequence, the OC1 and OC2 modules are tied to separate output pins using the DSC's remappable pins (RP) capability. The initialization sequence then sets the illumination duty cycle and selects the associated timer for use as its time base.

Copy
    //*********************************************************************************************************
    // Initialize Timer 2 - IR light
    //*********************************************************************************************************
    T2CON = 0x0020;                    // Stop 16-bit Timer2, 1:64(40MhzFosc) Prescale, Internal clock (Fosc/2)
    TMR2 = 0x00;                    // Clear timer register
    PR2 = 1250;                        // Load the period value, OCxRS <= PRx, 4ms period = (1/(Fosc/2))*1000*64*PR2 = (1/(40000000/2))*1000*64*1250
    IPC1bits.T2IP = 2;                // Set Timer2 Interrupt Priority Level
    IFS0bits.T2IF = 0;                // Clear Timer2 Interrupt Flag
    IEC0bits.T2IE = 1;                // Enable Timer2 Interrupt
 
    //*********************************************************************************************************
    // Initialize Timer 3 - Red light
    //*********************************************************************************************************
    T3CON = 0x0020;                    // Stop 16-bit Timer3, 1:64(40MhzFosc) Prescale, Internal clock (Fosc/2)
    TMR3 = 0x00;                    // Clear timer register
    PR3 = 1250;                        // Load the period value, OCxRS <= PRx, 4ms period = (1/(Fosc/2))*1000*64*PR2 = (1/(40000000/2))*1000*64*1250
    IPC2bits.T3IP = 2;                // Set Timer3 Interrupt Priority Level
    IFS0bits.T3IF = 0;                // Clear Timer3 Interrupt Flag
    IEC0bits.T3IE = 1;                // Enable Timer3 Interrupt
 
    //*********************************************************************************************************
    // Initialize Output Compare 1 module in Continuous Pulse mode, OC1 controls IR LED switch
    //*********************************************************************************************************
    RPOR6bits.RP13R = 0b10010;        // RP13/RB13 tied to OC1 (IR)
    OC1CONbits.OCM = 0b000;         // Disable Output Compare 1 Module
    OC1R = 0;                         // Write the duty cycle for the first PWM pulse, 24=8MHzFosc(50us), 30=40MHzFosc(50us), 600=40MHzFosc(1ms)
    OC1RS = duty_cycle;             // Write the duty cycle for the second PWM pulse, OCxRS <= PRx, 499=8MHzFosc(1ms), 623=40MHzFosc(1ms), 1246=40MHzFoc,2msPeriod, 4984=40MHzFoc,8msPeriod, 280=450us D/C@40MHzFoc,2msPeriod,switch
    OC1CONbits.OCTSEL = 0;             // Select Timer 2 as output compare time base
 
    //*********************************************************************************************************
    // Initialize Output Compare 2 module in Continuous Pulse mode, OC2 controls Red LED switch
    //*********************************************************************************************************
    RPOR6bits.RP12R = 0b10011;        // RP12/RB12 tied to OC2 (Red)
    OC2CONbits.OCM = 0b000;         // Disable Output Compare 2 Module
    OC2R = 0;                         // Write the duty cycle for the first PWM pulse, 24=8MHzFosc, 30=40MHzFosc, 600=40MHzFosc(1ms)
    OC2RS = duty_cycle;             // Write the duty cycle for the second PWM pulse, OCxRS <= PRx, 499=8MHzFosc(1ms), 623=40MHzFosc(1ms), 1246=40MHzFoc,2msPeriod, 4984=40MHzFoc,8msPeriod, 280=450us D/C@40MHzFoc,2msPeriod,switch
    OC2CONbits.OCTSEL = 1;             // Select Timer 3 as output compare time base

Listing 1: The main routine from the Microchip Technology sample code package uses a short initialization sequence to set up the digital signal controller's timers and output compare modules at the heart of this low-cost pulse oximeter solution. (Code source: Microchip Technology)

This approach takes advantage of the DSC architecture's association of each timer interrupt with a specific interrupt service routine (ISR) entry point. For example, when the red LED channel's Timer3 interrupt occurs, the DSC executes the function at the _T3Interrupt entry point. Thus, when the red LED's Timer3 expires, two coordinated hardware and software events occur:

  • OC2 generates a continuous pulse to the analog switch, turning on the red LED
  • The DSC begins executing the _T3Interrupt ISR (Listing 2)
Copy
void __attribute__((__interrupt__, no_auto_psv)) _T3Interrupt(void)        //Read Red DC & AC signals from AN0 & AN1
{
    int delay;
    unsigned char i;
 
    Read_ADC_Red = 1;
    CH0_ADRES_Red_sum = 0;
    CH1_ADRES_Red_sum = 0;
 
    for (delay=0; delay<200; delay++);    //2000=delayed 256us before read ADC
 
//    LATBbits.LATB14 = 1;            // for debugging
 
    for (i=0; i<oversampling_number; i++)
    {
        //Acquires Red-DC from Channel0 (AN0)
        AD1CHS0bits.CH0SA = 0x00;        // Select AN0
        AD1CON1bits.SAMP = 1;            // Begin sampling
        while(!AD1CON1bits.DONE);        // Waiting for ADC completed
        AD1CON1bits.DONE = 0;            // Clear conversion done status bit
        CH0_ADRES_Red_sum = CH0_ADRES_Red_sum + ADC1BUF0;    // Read ADC result
 
        //Acquires Red-AC from Channel1 (AN1)
        AD1CHS0bits.CH0SA = 0x01;        // Select AN1
        AD1CON1bits.SAMP = 1;            // Begin sampling
        while(!AD1CON1bits.DONE);        // Waiting for ADC completed
        AD1CON1bits.DONE = 0;            // Clear conversion done status bit
        CH1_ADRES_Red_sum = CH1_ADRES_Red_sum + ADC1BUF0;    // Read ADC result
    }
 
    CH0_ADRES_Red = CH0_ADRES_Red_sum / oversampling_number;
    FIR_input_Red[0] = CH1_ADRES_Red_sum / oversampling_number;
 
#ifdef Sleep_Enabled
    if (CH0_ADRES_Red<=74 && CH1_ADRES_Red>=4000)    //if spo2 probe is not connected, 74=60mV, 4000=3.2V
    {
        goto_sleep = 1;
    }
    else if (CH0_ADRES_Red > Finger_Present_Threshold)    //if no finger present then goto sleep
    {
        goto_sleep = 1;
    }
    else
#endif
    {
//        LATBbits.LATB14 = 0;            // for debugging
        for (delay=0; delay<500; delay++);    //1000=delayed 256us before read ADC
//        LATBbits.LATB14 = 1;            // for debugging
 
        //Acquires Red-DC baseline from Channel0 (AN0)
        AD1CHS0bits.CH0SA = 0x00;        // Select AN0
        AD1CON1bits.SAMP = 1;            // Begin sampling
        while(!AD1CON1bits.DONE);        // Waiting for ADC completed
        AD1CON1bits.DONE = 0;            // Clear conversion done status bit
        Baseline_ambient = ADC1BUF0;
 
        Baseline_Upper_Limit = Baseline_ambient + DCVppHigh;
        Baseline_Lower_Limit = Baseline_ambient + DCVppLow;
 
        Meter_State = Calibrate_Red();
    }
 
//    LATBbits.LATB14 = 0;            // for debugging
 
    OC2RS = duty_cycle;                // Write Duty Cycle value for next PWM cycle
    IFS0bits.T3IF = 0;                // Clear Timer3 Interrupt Flag
}

Listing 2: Included in the Microchip Technology sample code package, the Timer3 ISR shown here collects red LED illumination measurements and ambient light measurements, while the Timer2 ISR needs only to collect the IR LED illumination measurements. (Code source: Microchip Technology)

As shown in Listing 2, the _T3Interrupt ISR reads the red baseline level (Red-DC) from ADC channel 0 (AN0) and the red dynamic level (Red-AC) from ADC channel 1 (AN1). If the developer opts to include a definition for Sleep_Enabled, the compiled ISR code follows data capture with a check to see if the processor should enter a sleep state. The default configuration of the Microchip software package includes a #define for Sleep_Enabled, so the variable goto_sleep will be set if the optical probe is not connected or if the user's finger is not present.

Following this probe status check, the ISR samples the ambient light level and uses this updated value to shift the baseline window limits accordingly. Using these adjusted limits, the function Calibrate_Red() increases or decreases the DAC output to the red LED driver to keep the intensity between the Baseline_Lower_Limit and Baseline_Upper_Limit.

The T2 timer interrupt service routine uses the same basic design pattern excluding the check for sleep_enabled and ambient light level measurement.

With the timer, output compare, and ISRs in place, the sample software's main routine performs a short initialization sequence and starts Timer2 and Timer3. At that point, the code enters the main loop, waiting for data processed by the ISRs. As red and IR data comes available, those values are processed by a digital finite impulse response (FIR) filter, finally calling routines to calculate SpO2 and heart rate (Listing 3).

Copy
    //********** Enable OC1 & OC2 ouputs for IR & Red LED's on/off switch **********
    OC2CONbits.OCM = 0b101;                // Select the Output Compare 2 mode, Turn on Red LED
    T3CONbits.TON = 1;                    // Start Timer3
 
    for (delay=0; delay<2200; delay++);
 
    OC1CONbits.OCM = 0b101;                // Select the Output Compare 1 mode, Turn on IR LED
    T2CONbits.TON = 1;                    // Start Timer2
 
    goto_sleep = 0;
    first_reading = 0;
    
 
    while (1)
    {
        if (goto_sleep)
        {
 
[lines clipped]
 
                Sleep();                    // Put MCU into sleep
                Nop();
            }
        }
 
        //--------- Main State Machine starts here ---------
        if (RedReady && IRReady)
        {
            RedReady = 0;
            IRReady = 0;
 
//            LATBbits.LATB14 = 1;            //for debugging
 
            FIR(1, &FIR_output_IR[0], &FIR_input_IR[0], &BandpassIRFilter);
            FIR(1, &FIR_output_Red[0], &FIR_input_Red[0], &BandpassRedFilter);
 
            CH1_ADRES_IR = FIR_output_IR[0];
            CH1_ADRES_Red = FIR_output_Red[0];
 
[lines clipped]
 
            if (Detection_Done)
            {
                //Max & Min are all found. Calculate SpO2 & Pulse Rate
                SpO2_Calculation();                //calculate SpO2
                Pulse_Rate_Calculation();        //calculate pulse rate
 
[lines clipped]
 
    }
/*****************************************************************************
 * Function Name: SpO2_Calculation()
 * Specification: Calculate the %SpO2
 *****************************************************************************/
void SpO2_Calculation (void)
{
    double Ratio_temp;
 
    IR_Vpp1 = fabs(IR_Max - IR_Min);
    Red_Vpp1 = fabs(Red_Max - Red_Min);
    IR_Vpp2 = fabs(IR_Max2 - IR_Min2);
    Red_Vpp2 = fabs(Red_Max2 - Red_Min2);
 
    IR_Vpp = (IR_Vpp1 + IR_Vpp2) / 2;
    Red_Vpp = (Red_Vpp1 + Red_Vpp2) / 2;
 
    IR_Vrms = IR_Vpp / sqrt(8);
    Red_Vrms = Red_Vpp / sqrt(8);
 
//    SpO2 = log10(Red_Vrms) / log10(IR_Vrms) * 100;
//    if (SpO2 > 100)
//    {
//        SpO2 = 100;
//    }
 
    // Using lookup table to calculate SpO2
    Ratio = (Red_Vrms/CH0_ADRES_Red) / (IR_Vrms/CH0_ADRES_IR);

Listing 3: This snippet from the main routine in the Microchip Technology sample code package shows how the code initializes timer and output compare modules and enters an endless loop, calculating SpO2 and heart rate when measurements are available, or putting the processor in low-power sleep mode when sensor functionality goes offline. (Code source: Microchip Technology)

For SpO2, the SpO2_Calculation() function converts the pulse amplitudes (Vpp) of the red and IR signals to Vrms values. Using these values, the function generates a ratio and uses a lookup table (not shown in Listing 3) to convert the ratio to a specific value of SpO2. Typically, this lookup table is derived from multiple empirical measurements. The Pulse_Rate_Calculation() uses the measurement's inter-peak timing to determine heart rate.

SpO2 design optimization options

Although the design described in this article provides an effective solution for a low-cost pulse oximeter, other devices might offer some further optimization. For example, a developer could eliminate the external MCP6002 dual op amp device by using the op amps integrated in the Microchip Technology DSPIC33CK64MP102 DSC.

In implementing this modified pulse oximeter design, however, developers will need to rewrite some key portions of the software package described earlier to account for some differences in the DSC.

For example, the DSPIC33CK64MP102 DSC provides a set of multiuse timer modules instead of the Timer2/Timer3 feature in the DSPIC33FJ128GP802 DSC, requiring developers to provide their own solution to some of the functionality described in the listings included in this article. Even so, the principles of operation remain the same and developers can, at a minimum, use the design patterns shown in the Microchip Technology sample software package to guide their own custom software design.

Conclusion

Measurement of blood oxygen saturation levels provides an important indicator of respiratory function and has become an important tool in managing health during the COVID-19 pandemic. Using straightforward optical methods, pulse oximeters provide reliable estimates of peripheral oxygen saturation (SpO2), meeting a particular need for affordable health monitoring solutions during the pandemic.

As shown, in combination with a few basic components, a DSC provides an effective hardware foundation for implementing a low-cost pulse oximeter able to reliably deliver SpO2 measurements that might indicate a need for users to seek further medical attention for a progressing COVID-19 infection.

Disclaimer: The opinions, beliefs, and viewpoints expressed by the various authors and/or forum participants on this website do not necessarily reflect the opinions, beliefs, and viewpoints of Digi-Key Electronics or official policies of Digi-Key Electronics.

About this author

Stephen Evanczuk

Stephen Evanczuk has more than 20 years of experience writing for and about the electronics industry on a wide range of topics including hardware, software, systems, and applications including the IoT. He received his Ph.D. in neuroscience on neuronal networks and worked in the aerospace industry on massively distributed secure systems and algorithm acceleration methods. Currently, when he's not writing articles on technology and engineering, he's working on applications of deep learning to recognition and recommendation systems.

About this publisher

Digi-Key's North American Editors