Newer
Older
## 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.
- 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
- https://patents.google.com/patent/US7280858B2/en (active thru 2025)
- https://patents.google.com/patent/US6697656B1/en (exp 6/2020)
- https://patents.google.com/patent/US6684090B2/en (exp)
- 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).
A quick teardown of a ~$20 Zacurate 500BL from Walgreens revealed no [integrated photonics package](https://www.maximintegrated.com/en/products/interface/sensor-interface/MAX30101.html) or [signal processing ASIC](https://www.maximintegrated.com/en/products/interface/sensor-interface/MAX32664.html); 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](www.sg-micro.com/uploads/soft/20190626/1561538475.pdf) op-amp and an [STM32F100](https://www.st.com/en/microcontrollers-microprocessors/stm32f100-value-line.html)-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.
Pulse oximetry is based on the [Beer-Lambert law](https://en.wikipedia.org/wiki/Beer%E2%80%93Lambert_law), a principle that relates the concentration of a species to the attenuation of light through a sample:
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:
```math
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:
_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 (O<sub>2</sub>Hb) 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:
_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.
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
### 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

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

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:

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

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](http://fab.cba.mit.edu/classes/864.20/people/zach/final.html) 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 SpO<sub>2</sub>, 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 SpO<sub>2</sub> 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 SpO<sub>2</sub> 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 O<sub>2</sub>Hb / Hb ratio discussed above.