Pulse Oximeter With Much Improved Precision

44,817

60

76

Introduction: Pulse Oximeter With Much Improved Precision

If you recently visited a doctor, chances are that your basic vital signs were examined by a nurse. Weight, height, blood pressure, as well as heart rate (HR) and oxygen saturation in peripheral blood (SpO2). Perhaps, the last two were obtained from a red-glowing electronic finger probe that displayed relevant numbers on a tiny screen in minutes. That probe is called pulse oximeter and you can find all the basic info about it here.

One can easily buy a simple pulse oximeter, sure, but where is the fun in it? I have decided to build my own, first for the heck of it, but more importantly with a specific application in mind: nocturnal oximetry where both HR and SpO2 data would be continuously collected overnight and recorded on a micro SD card. Instructables already contains several projects of this kind, e.g., two involving Arduino here and here, and one utilizing Raspberry Pi. Mine uses slightly newer sensor MAX30102 from MAXIM Integrated and Adafruit's Feather M0 Adalogger for control and data recording.

Our project is thus not particularly innovative in terms of hardware and as such would not be worth writing this Instructable, but in the process of creating it I have made crucial advances in software that allowed me to extract data from MAX30102 with much higher consistency and much less noise than software written by MAXIM for this sensor. The performance of our signal processing algorithm is illustrated in the above chart where the two top graphs contain overnight heart rate and oxygen saturation calculated from raw signals by our method (identified by "RF"), while the bottom two graphs show MAXIM's results produced from exactly the same signals. Standard deviations for HR are 4.7 bpm and 18.1 bpm, and for SpO2 0.9% and 4.4%, for RF and MAXIM, respectively.

(Both RF graphs correspond to minimal autocorrelation threshold of 0.25 and no limit on R / IR correlation; see Steps 4 and 5 for explanation of these terms.)

Step 1: Hardware

  1. Pulse oximeter and heart rate sensor MAX30102 system board from MAXIM Integrated, Inc.
  2. Feather M0 Adalogger from Adafruit, Inc.
  3. Lithium Ion Battery from Adafruit, Inc.

Connections:

  • Adalogger pins SCL and SDA to corresponding SCL and SDA pins on MAX30102 board
  • Adalogger pin 10 to pin INT on MAX30102 board
  • Adalogger GND to MAX30102 board GND
  • Adalogger 3V to MAX30102 VIN

Step 2: Digital Signals Returned by MAX30102

The principles of the sensor operation are very simple: two LEDs, one red (660 nm) and one infrared (880 nm, IR) shine light through human skin. The light is partially absorbed by underlying tissues, including peripheral blood. Sensor's photodetector collects reflected light at both wavelengths and returns two corresponding relative intensities using I2C protocol. Since absorption spectra for oxygenated and deoxygenated hemoglobin differ for both wavelengths, the reflected light has a variable component as the amount of arterial blood that is present under the skin pulses with each heartbeat. Figuring out heart rate and oxygen saturation is up to the signal processing software.

Examples of raw signals (IR channel only) are illustrated in the above images. One can notice a periodic component overlaid on a variable baseline that is shifting due to multiple factors mentioned in the Wikipedia page. Motion induced artifacts are particularly annoying since they may mask the useful HR signal and cause bogus results. Hence, advanced commercial oximeters feature accelerometers that help nullify these artifacts.

I may add an accelerometer to the next version of my oximeter, but for nocturnal HR/SpO2 recording, when sensor remains motionless most of the time, it is sufficient to detect and omit distorted signals.

The MAX30102 sensor itself comes in a tiny surface-mounted package, but MAXIM graciously offers a breakout board (System Board 6300) plus signal processing software for Arduino and mbed - all in the reference design package MAXREFDES117#. I happily bought it expecting to just solder some wires between the sensor and Adalogger and have a working, good oximeter in a single day. I adapted the RD117_ARDUINO version of MAXIM's software to run on the Adalogger's ARM Cortex M0 processor. Basically, all I had to do was to replace incompatible SofI2C functions in max30102.cpp by the corresponding Wire library calls. The code compiled fine in the Arduino IDE v1.8.5 and ran on M0 without any errors. The net results, however, were disappointing. In the Introduction step I have already shown very high variance of both HR and SpO2. Naturally, one may claim that I have done something wrong and this was my original thought, too. However, in MAXIM's instructional video you can also observe wildly swinging HR values displayed on the screen. Moreover, comments below video confirm that others have noticed a similar phenomenon, as well.

To make a long story short, after some experimentation I have determined that the sensor is operating OK and an alternative method of digital signal processing results in much better stability. This new method, indicated by "RF", is described in the next steps.

Step 3: Signal Preprocessing

In our implementation, the raw signal is collected at the rate of 25 Hz (same as MAXIM's) for full 4 seconds (MAXIM's software collects only 1 second's worth), resulting in 100 digitized time points per end data point. Each 100-point sequence must be preprocessed in the following way:

  1. Mean-centering (a.k.a. "removal of the DC component" to electrical engineers). The raw data coming from the sensor is a time series of integers in the 105 range. The useful signal, though, is only a part of light reflected from arterial blood which varies on the order of only 102 - first figure. For meaningful signal processing, it is therefore desirable to subtract the mean from each series point. This part is no different from what the MAXIM software already does. What's different, however, is additional mean-centering of time indices themselves. In other words, instead of indexing series points by numbers from 0 to 99, the new indices are now numbers -49.5, -48.5, ... ,49.5. It may seem weird at first, but thanks to this procedure the signal curve's "center of gravity" coincides with the origin of the coordinate system (second figure). This fact becomes quite useful in the next step.
  2. Baseline leveling. Another look at the waveforms shown in Step 2 illustrates that the baseline of real oximetry signals is far from being horizontally flat, but varies through different slopes. Third figure shows a mean-centered IR signal (blue curve) and its baseline (blue straight line). In this case, the baseline's slope is negative. The signal processing method described ahead requires baseline to be horizontal. This can be achieved by simply subtracting the baseline from the mean-centered signal. Thanks to the mean-centering of both the Y and the X coordinates, the baseline's intercept is zero and its slope equation is particularly simple, as shown in the fourth figure.The baseline-leveled signal is shown by orange curve in the third figure.

Thus preprocessed signal is ready for the next step.

Step 4: The Workhorse: Autocorrelation Function

Returning back to the usual 1,...,n indexing, the first figure shows definition of the autocorrelation function rm - a quantity found to be very useful in detecting signal's periodicity as well as quality. It is simply a normalized scalar product of the signal's time series with itself shifted by lag m. In our application, though, it is convenient to scale each autocorrelation value with respect to its value at lag = 0, i.e., use relative autocorrelation defined by rm / r0.

Plot of the relative autocorrelation of a typical good quality IR signal is shown in the second figure. As expected, it's value at lag = 0 is at its global maximum equal to 1. The next (local) maximum occurs at lag = 23 and equals to 0.79. The presence of local minima and maxima in autocorrelation plot is easy to understand: as the signal shifts to the right its peaks interfere destructively with each other at first, but at certain point the interference becomes constructive and achieves maximum at the lag equal to the average period of the signal.

The last phrase is crucial: in order to determine the average time period between peaks, from which one can calculate signal's frequency (i.e., heart rate) it is sufficient to find the first local maximum of the autocorrelation function! By default, MAX30102 samples analog input at a rate of 25 points per second, therefore at given m the period in seconds is equal to m / 25. This leads to heart rate expressed in beats per minute (bpm) by:

HR = 60*25 / m = 1500 / m

Of course, it is not necessary to do expensive calculations of rm at all lag values. Our algorithm makes the first guess of heart rate = 60 bpm, which corresponds to m = 25. Autocorrelation function is evaluated at that point and compared to the value at its left neighbor, m = 24. If the neighbors value is higher, then the march continues to the left until rm-1 < rm. Thus determined final m is then returned as the lag at maximum. The next iteration starts from that value instead of 25 and the whole process repeats. If the first left neighbor is lower, then the above routine marches lag points to the right in similar manner. Most of the time, lag at maximum requires just a few evaluations of the autocorrelation function. In addition, maximum and minimum acceptable lags (corresponding to minimal and maximal heart rate, respectively) are used as limiting values.

The above works very well for good quality signals, but the real world is far from ideal. Some signals come out distorted, mostly due to motion artifacts. Such a signal is shown in the third figure. Poor periodicity is reflected in the shape of its autocorrelation function as well as in low value, 0.28, of the first local maximum at m = 11. Compare it to the maximum value of 0.79 determined for the good quality signal. Along with lag limiting values, therefore, the value of rm / r0 at maximum is a good indicator of signal quality and a requirement for it to exceed certain threshold may be used to filter out motion artifacts. The "RF" graphs shown in the introductions resulted from such threshold equal to 0.25.

Step 5: Determining Oxygen Saturation

The previous step was sufficient for determining heart rate. The SpO2 requires more work. First, the so far neglected signal in the red (R) channel must be taken into account. Next, the ratio of red to infrared signals, Z = R/IR, both reflected off the arterial blood, is calculated. The "arterial blood" part is crucial, since most of the light is actually reflected off tissues and venous blood. How to pick portion of the signal corresponding to arterial blood? Well, this is the pulsatile component that varies with each heartbeat. In words of electrical engineers, it's the "AC part", while the remaining reflected light is the "DC part". Since absolute intensities of R and IR light are not commensurate, the Z ratio is calculated from relative intensities, as shown in the first figure. In terms of actually calculated quantities, I use root-mean-square (RMS) of the mean-centered, baseline-leveled signal, y, to the already known mean of the raw signal, <Y>; see second figure. The Z ratio is only half of the work, however. The nonlinear sensor response requires an empirical calibration between Z and the final SpO2 values. I took the calibration equation from MAXIM's code:

SpO2 = (-45.06*Z + 30.354)*Z + 94.845

Keep in mind this equation is valid only for MAX30102 design board purchased in 2017! It is likely that MAXIM may recalibrate its sensors at a later date.

The above procedure still produces a lot of false SpO2 readings. The red channel suffers from many artifacts, just like the IR one. It is reasonable to assume that both signals should be strongly correlated. In fact, good quality signals, like the example in third figure, do correlate very well. The Pearson correlation coefficient is in this case as high as 0.99. This is not always the case, as illustrated in the fourth figure. Although the IR signal would pass the heart rate quality filter with its rm / r0 = 0.76, the distorted R signal results in a poor correlation coefficient between the two equal to only 0.42. This observation offers the second quality filter: having the correlation coefficient between channels greater than certain threshold.

The last two figures exemplify net effect of such quality filtering. First, the measured oxygen saturation is plotted with HR quality threshold of 0.25, but without the SpO2 filter. Next plot results from filtering out poor HR and SpO2 results at the 0.5 rm / r0 and 0.8 correlation coefficient thresholds. Overall, poor data points amounting to 12% of the total were filtered out by the stricter regime.

In our code the correlation coefficient, cc, is calculated according to the formula in fifth figure, where y represents the mean-centered, baseline-leveled signal, whereas r0 was defined in the previous step.

Step 6: The Source Code

The C source code for this project, formatted for the Arduino IDE, is available from our Github account at the following link:

https://github.com/aromring/MAX30102_by_RF

Its Readme page describes individual components.

I would like to take a moment to praise Adafruit for making such an excellent product as M0-based Adalogger. Its fast 48 MHz ARM Cortex M0 processor, with lots of RAM, certainly helped make this project viable, while directly attached SD card reader (plus Adafruit's SD library) remove all the hobbyist's pains associated with real time storage of large amounts of data.

1 Person Made This Project!

Recommendations

  • Make it Real Student Design Challenge #3

    Make it Real Student Design Challenge #3
  • Explore Science Challenge

    Explore Science Challenge
  • Box Challenge

    Box Challenge

76 Comments

0
gallaugher
gallaugher

Question 5 weeks ago

Thanks for sharing this! I've been looking for examples of using a MAX30100 with a Feather. I'm VERY new to Arduino + electronics, but am trying to adapt Jiri Praus's Beating LED Heart build to a Feather M0 Express (Bluefruit) since that's a device I had lying around with pass-through charging to eliminate some of the work in Jiri's build. I see you've got your MAX301 working by connecting SCL to SCL and SDA to SDA and also use the Wire.h library. Jiri's build doesn't show the Int pin on the heart rate sensor being used, but yours does. I'm wondering if I map the SCLs and SDAs from the MAX30100 to the Feather M0 if I should expect things to work properly or if I should be expected to do some additional modification in the code. I currently have a MAX30100 incorrectly wired up, but have a MAX30102 on order. Sincere apologies - I know JP's code isn't your build, but since you've used this hardware I figure you're experienced in the nuances of getting the sensor & the Feather M0 to communicate properly. Thanks for any insights you might have.

0
MolecularD
MolecularD

Answer 4 weeks ago

Boards of Feather fly together, hence I don't see why your M0 Express would wire any differently than my M0 Adalogger (although my project is over 3 years old, so you have to verify it with Adafruit).
Now, by "the code" you refer to Jiri's code? He does offer the main program heartRate.ino but without the necessary header files and libraries. I admit, I did not have time to read his Instructable in detail, so help me here: where did you get the missing code components?

0
gallaugher
gallaugher

Reply 4 weeks ago

Thank you so much for responding. It seems i had a bad board (or ruined my board). I've gotten everything to work with a Feather M0 & Jiri's code. Really appreciate your kindness in responding. Wishing you the best!

0
nicholastan98
nicholastan98

10 months ago

HI, have you tried this sensor on the wrist?
I tried it on the finger, the results were fine, could get above 99% spo2 and valid heart rate.

But when I tried it on the wrist, I could not get any results of both, all -999.

On the wrist:
The raw value of Red is lower than IR about 50000

On the finger:
The raw value of Red is only lower than IR about 10000.

0
Anyeos
Anyeos

Reply 5 months ago

Only the green led work on the wrist. I am not sure but I guess the MAX30105 comes with the green led. But as I know you cannot get spO2 with the green led, so it will not work to get spO2 anyway.

0
nieljosiah
nieljosiah

Reply 9 months ago

You can use the MAX30102. It works on the wrist

0
MolecularD
MolecularD

Reply 10 months ago

No, I haven't. It won't work on the wrist because of motion artifacts.

0
Anyeos
Anyeos

6 months ago

Hi, I have a problem, my sensor just have an xy_ratio over 2 the most of the time. I can never get an SpO2 calculation.
I tried a lot of amplitude values for both the IR and the Red led without success. My xy_ratio is always 2 or a little more. But surprisigly on my case it stays near 2.0 most of the time.
That is not a coincidence.

Doing more investigation I conclude that if I raise the amplitude of IR I can get the AC RMS value in the same range that the Red one. But the MEAN value is ten times bigger. So I never can get that ratio less than some high value because my sensor just have an offset from one signal to the other. And the AC level of both signals are different if I set the same amplitude for both leds. That is how my sensor do the job so the formula does not works for me.

Is my module failed? It is a MH-ET Live with a MAX30102.

Captura de pantalla de 2020-12-07 08-11-47.png
0
Anyeos
Anyeos

Reply 6 months ago

Just when you said that, I think it is really a clone or something because it have another problem too: It is slow. Sometimes the sensor got slow and the flow of the data comes less fluent.
It have other problem too: Some component on the PCB becomes hot and just burn me when I try to put it over my skin. I solved that using 3.3v and insulating all the PCB with plastic except the sensor portion.

But the shop where I buyed it selled it as original. And it was not economic.

0
Parasetamol
Parasetamol

8 months ago

Hello, is there anyone succeed reading die temperature from MAX30102 sensor by the code provided by MolecularD? I will appreciate for your help.

0
Anyeos
Anyeos

Reply 6 months ago

I just use a mix of code. I put the MolecularD files on the same directory as my sketch and I use the SparkFun MAX30105 library to start and read data from the sensor. So far as I know, I can just read temperature with "readTemperature()" function from MAX30105 library.
You only need "algorithm_by_RF.cpp" and "algorithm_by_RF.h" from MolecularD. Because that are the files that have the better code.
And you must start the sensor with this parameters (same parameters that MolecularD are using):
particleSensor.setup(0x24, 4, 2, 100, 411, 4096);

1
rainboy912
rainboy912

1 year ago

Hi.
Your code is amazing. i am using it for small project ventilator.
Normally the I2C of arduino is 0V(logic 0), 5V(0). But the I2C of Max30102 is 0V(logic0) 1.8(logic 1) and the MAX30102 board have 4.7k pull up resistors to 3.3V. The first time i connect Max30102 to arduino it did not work with any codes i found in github, and i ask the supplier "is that sensor died" then they tell me to make 4.7k pull-up resistors for SDA SCL to 5V and it works. Can you guys help understand Why i have to do that and did that have any bad effect to the signal or microchip. I google it but still cant understand
Thank you so much

1c7e7426de6d24337d7c.jpg
0
bbogdanmircea
bbogdanmircea

Reply 1 year ago

Hello, I am trying to use the MAX30102 PCB with ESP32 based board.
I wired the SCL, SDA to pins 21, 22 directly and GND, VIN to GND and 3.3V of the ESP32 board.
The I2C doesn't seem to work in any sketch, I checked also an I2C Scanner and it tells that it can't find any device.
I measured and there is voltage on the pins on the board.
Any ideas how to make it work?
Both the ESP32 and the MAX30102 use 3.3V so it should be no problem to connect directly?

0
MolecularD
MolecularD

Reply 1 year ago

Sorry, I have never used ESP32 board.

0
bbogdanmircea
bbogdanmircea

Reply 1 year ago

See my comment above, worked after pull-up fix for the board. Seems to be a design error.
I did not use this code yet, actually only for the SparkFun MAX30105 library the examples work, but the measurements for the SPO2 are very unstable.
I will try this repository next week to on my ESP32 board and feedback if the sensor works.
Basically for ESP32 if you just change board and port in Arduino IDE it works to compile and upload if you have the ESP32 packages installed.

0
bbogdanmircea
bbogdanmircea

Reply 1 year ago

I found this fix on another site, my understanding that the pull-up to 1.8V was a design error. Anyway I've cut the track to 1.8V and soldered a wire to 3.3V and the sensor started working for me too. MAX30102

0
bbogdanmircea
bbogdanmircea

Question 12 months ago

I looked more in detail at your code and I must first congratulate for the professional way in which you wrote the code and commented it, from 4 MAX30102 drivers that I found, yours is by far the easiest to understand and use.
And now my question, I am running your driver on an ESP32 board that is connected to the MAX30102 board only by SCL SDA (INT is not connected), so for your code the LEDs are lighting, but of course there is no measurement as the uC is waiting for the interrupt line.
But other drivers do not use interrupt line, is it possible to do a small modification to the code so that it works without an interrupt line?
I will check the datasheet in detail and try to understand how to do that by myself, but if you have a tip it would be really helpful.