This is a blood oxygenation and pulse rate monitor implemented with analog circuitry and a Teensy 3.6 microcontroller development board (3.3V). The monitor has high accuracy (agrees to commercial pulse oximetry devices to within 2%) and could accommodate users with variations of tissue thickness, skin pigmentation, etc, providing consistent accuracy across these variations.

The photo on the right shows the reading of blood oxygen level and heart rate on Teensy Arduino platform serial communication display.

This project was completed as the final project for my biomedical instrumentation class, working with my teammate.


The theory behind pulse oximetry

The theory of pulse oximetry is based on how the blood absorbs light of different wavelengths. One example of this phenomenon is its red appearance due to absorbing more blue light than red. Blood oxygen is an indicator of how “full” the blood is. Pulse oximeters use the ratio of red and infrared (IR) lights to calculate blood oxygen level, where the red light wavelength is at 660nm, and the IR light wavelength is at 940 nm (blue).

When the red and IR light beams are projected onto the finger tissue, the photosensor mounted on the opposite side of the light sources will read signals of different amplitude due to the blood will absorb more IR light and less red light. This difference could be used to calculate both the heart rate and the blood-oxygen level.

As seen on the right (Lopez, 2012), oxygenated hemoglobin (HbO2) has greater absorption at 940nm.

Milestones and Roadmap

After understanding how pulse oximetry work, there are a couple of challenges before finally getting out the readings. First, we need to figure out how to separate the result of the photosensor into independent red and IR signals. Second, due to variations in skin tissue thickness and pigmentation, more or less light will be absorbed, resulting in different signal magnitudes. We need to figure out a way to smart-adjust the gain of the system based on these variations. Third, how would we compute our final result using the microcontroller?

Where do Red and IR signals come from?

The raw signal is acquired using the pulse oximeter probe (the finger clip) with LED lights and photodiodes. The LEDs are lit through the driving circuit powered by the Teensy microcontroller’s PWM pins (Red and IR LED uses separated pins with non-overlapping pulse trains). Light signals pass through the finger tissues are received at the photodiode, where the resulting current is amplified and converted to voltage using transimpedence amplifier (TIA) configuration The TIA design provides not only higher signal gain but also enhanced thermal noise performance and phase margin with relatively large resistance value and corresponding capacitor values.

The signal is separated into RED and IR signals using two analog switches that share the same sources as the PWM used to power the LED lights, therefore assuring simultaneity. Each switch will only allow the light signal powered by the same pin to pass when the switch is closed). The photo below shows how a single output is transformed into the red (orange) and the IR signal (green)

Next, split the TIA output into Red and IR signals

Accommodating different fingers… the automatic gain control

After getting two separate signals, we applied two identical trains of high-pass, low-pass, and inverting amplifier filters. We first fed the signal into a 2nd order Sallen-Key high-pass filter to reset the DC offset at the reference, and most importantly filter out low-frequency noise caused by unwanted motions of the subjects. The corner frequency was set at 0.5Hz to make sure we maintain our signal of interest that is between 1Hz and 4 Hz (we are dealing with pulse signals!).

Second, we used a 2nd order Sallen-key low-pass filter with gain at 5Hz corner frequency to maintain the signal of interest. This filter is very important because this would filter out unwanted 60Hz signals and reveal the shape of the pulse signals by extracting the square waves from the PWM shown above. More importantly, it is an anti-aliasing filter for when digitizing the analog signal to the Teensy controller for computing. We were using a 12-bit ADC, and Teensy 3.6 provides 3.3V, therefore, each discrete voltage level is 0.8mV apart. To ensure 2% accuracy, 0.8V should be less than 2% of the signal of interest. This means the signal should be greater than 40mV, thus we added ~50V/V gain on this filter to achieve this. The sampling rate of our ADC is at 1MHz, this means the difference between sampling frequency and bandwidth (4Hz) is approximately 1MHz. At 1MHz, this filter could have attenuation at 200dB, which would be sufficient because we only need 20log(1/2%) = 35dB attenuation.

At last, we used two inverting high-pass amplifiers with different gains to further amplify the signals (making sure they are at similar magnitude) and flip the signals upside down to make sure it has a narrow rather than wide top for the convenience of future analysis.

Initial Signal Processing

As said before, different fingers will need to be compensated with different gains from the system. The system first needs to detect the signal level before implementing appropriate adjustments. Therefore, we first average out the signals using two peak detectors with very slow decay. This “constant” value is used as an indication of the user’s signal amplitude. The following graphs depict how IR signal is averaged using the peak detector.

Our experiment with participant fingers of different pigmentation and tissue thickness helped us identify the possible range of initial amplitudes. We programmed Teensy microcontroller to read the signals using ADC and set digital thresholds that could control PWM duty cycles used to power the LEDs through a negative feedback loop: higher duty cycles would mean stronger signals and vice versa. Therefore, a person with more skin pigmentation or thicker skin tissue will be put on higher duty cycles, and a person with less skin pigmentation or thinner skin tissue will be put on the lower.

Time to find out heart rate and blood oxygen levels -- data processing with Teensy

Starting from this point, all the data processing and display are done in the Arduino software with Teensy. The same “averaged signal” from the peak detector is used to compute the “smart reference” (red line) to the red and IR signals. From there, we can calculate the period of the signals crossing the reference and thus get the heart rate. 

The diagram on the right shows a "paper design” of the code executed. We used the Teensy timers to set the three-second sampling window. For every three seconds window, we will record the timestamp of the smart reference line crossing the signal. Then, we will compute the average of their differences, the average period. Then, we can convert the period to frequency to get the heart rate/min. To avoid noise causing invalid crossing of the threshold, we implemented “hysteresis” with the 0.3s buffer where the program will only record the second timestamp when it is more than 0.3s away from the first one, as 0.3s/beat (>180bpm) is faster than the fastest heartbeat.

For the calculation of blood oxygen level SpO2, we researched different articles and case studies on pulse oximetry. After experimenting with the different formulas used in these existing studies, we realized that most of the formulas are derived from the corresponding design approaches. In our case, we have already eliminated the DC offset and our AC voltages have been through a couple of analog filters, therefore applying equations using raw DC/AC signals would not be reasonable. We did discover the commonality across these formulas is the inverse proportional relationship between SpO2 and constant R based on the Beer-Lambert model.

For our design, we applied the definition of R’ by Townsend, and derived a linear relationship between R’ and SpO2 by calibrating across existing oximetry devices: SpO2 = 109-10R’ .

The AC amplitudes for the two signals are estimated by finding the minimum and maximum values for each pulse period (between the two timestamps used for pulse rate calculations).

The SpO2 is calculated by averaging reasonable measurements of SpO2 occurring in the three-second window while rejecting false data when the three-second window only takes half of the last pulse detected in the window. This is done to ensure the last timestamp is always more than one second away from the window frame, as most pulse periods are shorter than one second (60bpm).

During the peak of the coronavirus pandemic, my family bought a pulse oximeter device from Amazon. It is said that Covid patients will have a low blood-oxygen level, which might endanger their health, and we thought it would be a fast indicator for any possible infection.

Two years later, in my biomedical instrumentation class, I was tasked to create a pulse-oximetry device that is comparable to the amazon device.

This project gave me the opportunity to experience the complete design flow for an instrumentation system. I enjoyed thinking about the workarounds for all kinds of problems encountered (such as this video taken during our debugging phase when trying to figure out an alternative SpO2 equation) and trying them out. It is so rare to have something working as expected without taking the effort and time to appreciate the problems, but it is through these challenges that I became more confident in the design and debugging, as well as approaching a complicated problem in a systematic approach such as always document the debugging process and make paper designs before trying to write any code.