DIY Nauwkeurige Oximeter Sensor Bouwen – IR, Arduino, LCD & Componenten Gids
Onderdelen en Benodigdheden
| Onderdeel | Aantal | Opmerkingen |
|---|---|---|
| Infrarood emitter (18°) | 1 | Golflengte tussen 805nm en 1000nm |
| Fotodiodo LPT80A | 1 | Breedbereik fotodiodo, moet zowel rood (600nm) als infrarood (940nm) kunnen ontvangen |
| 5 mm Rode LED | 1 | Golflengte rond 600nm |
| Weerstand 10k ohm | 1 | |
| Weerstand 330 ohm | 2 | |
| DFRobot I2C 16x2 Arduino LCD Display Module | 1 | Of een andere LCD-display om waarden weer te geven |
| Arduino UNO | 1 | |
| Draadjes (generic) | 1 | |
| Hooghelderheid Witte LED | 1 | Optionele derde LED, zie verbeteringssectie |
Benodigde Tools en Machines
- Solderbout (Gull Wing tip)
Apps en Online Services
- Arduino IDE
Over Dit Project
In deze isolatieperiode heb ik een oximeter gebouwd met onderdelen die ik al in huis had. Een oximeter bestaat immers slechts uit twee LEDs en een fotodiodo.
Ik ben geen medisch expert en op dit stadium kan ik niet garanderen dat dit project diagnostische waarde heeft, maar het is een uitstekend educatief project om te begrijpen hoe een oximeter werkt. Met een paar verbeteringen zou het tot een zelfgemaakt medisch hulpmiddel kunnen uitgroeien.
Zuurstofverzadiging en COVID-19
In deze ongewone tijd hebben we veel geleerd over virussen, longen, mondmaskers, zeep en handen wassen. Iedereen leest over symptomen zoals hoesten, koorts en benauwdheid. We weten nu ook dat een manier om benauwdheid te meten, is door de hoeveelheid zuurstof in het bloed te bepalen.
Deze meting kan indirect worden uitgevoerd met een medisch apparaat genaamd oximeter. Je hebt er waarschijnlijk al eens gezien: een non-invasief apparaat dat op een vinger wordt geplaatst met pulserende lichten die het werk doen.
Normaal gesproken heb je een zuurstofverzadiging (SpO2) van ongeveer 95% of hoger. Wanneer de verzadiging onder de 90% daalt en je hoest en koorts hebt, is er sprake van een probleem.
Als elke maker een oximeter kan bouwen, zou het ontdekken van een infectie makkelijker worden en mensen helpen beslissen of ze naar het ziekenhuis moeten gaan bij een echt probleem, niet bij een paniekaanval.
Eerst Begrijpen Hoe Een Hartslagsensor Werkt
Ik startte dit project met een KY-039 hartslagsensor uit een sensorkit die velen thuis hebben. Zoals je in het schema ziet, bestaat het uit een infrarode LED die een fotodiodo belicht, plus twee weerstanden om de LED te beschermen en het kleine signaal te lezen.
Als je geen KY-039 sensor hebt, kun je er eenvoudig zelf een bouwen met enkele componenten.

De vinger wordt tussen de sensor en de fotodiodo geplaatst:

Het infrarode licht wordt deels geabsorbeerd door de nagel, huid en overige vingerweefsels. De absorptie is niet constant; deze verandert met de bloedstroom in de aderen. Bij elke hartslag wordt bloed door de aderen geduwd, wat de lichtabsorptie verandert. We meten de stroom die de fotodiodo genereert door het bereikende infrarode licht.
De KY-039 sensor heeft een S (signaal) pin om deze veranderende waarde uit te lezen.
Hartslag Meten Door Pieken Te Tellen
Het uitlezen van een variabel signaal is niet eenvoudig: er is veel ruis, het signaal is zwak, en er is wiskunde nodig om de juiste waarden te filteren voor plotten.
Dankzij een nuttige post van Johan Ha, die uitlegt hoe het gemiddelde van het signaal te berekenen en hoe ruis van huishoudlampen (die licht ook ruis is!) te verwijderen.
De truc is een array waarin we een waarde toevoegen en de oudste verwijderen om het gemiddelde van de laatste X metingen te behouden. Hij beschrijft ook een manier om de stijgende flank te detecteren door N opeenvolgende stijgende waarden te tellen. Als een waarde N keer groter is dan de voorgaande, is er een piek.
Met de Arduino Serial Plotter of tools als SerialPlot, en door te experimenteren met waarden, kunnen we een correct N bepalen (rise_threshold in de code). Een te grote of te kleine waarde laat pieken missen of telt een dicrotische notch als hartslag.
Zodra je de pieken kunt detecteren, tel je ze of bereken je de tijd tussen een reeks hartslagen om de BPM (Beats Per Minute) te bepalen.
De Oximeter Bouwen (KY-039 Hacken) Voor Zuurstofverzadiging
Ons bloed absorbeert licht verschillend afhankelijk van de golflengte. Rood licht (~600nm) wordt beter geabsorbeerd door zuurstofrijk bloed. Door metingen met de infrarode LED (~950nm) te vergelijken met die van de rode LED, kunnen we het percentage zuurstof in het bloed bepalen. Deze waarde heet SpO2% (perifere capillaire zuurstofverzadiging).
Omdat ik een KY-039 sensor had, besloot ik deze te modificeren. De sensor heeft alleen een infrarode LED, dus voegde ik een RODE LED toe, koppelde de IR-LED los van Vcc en verbond beide LEDs via een 330 ohm weerstand met twee verschillende Arduino-pinnen.
(Als je geen KY-039 hebt om te hacken, kun je de sensor zelf bouwen: het zijn slechts twee LEDs, een fotodiodo en drie weerstanden. Het schema is heel simpel!)
Hier is het schema van de gemodificeerde sensor:

Zo kunnen we de IR-LED aanzetten en de waarde van de S-pin lezen, daarna de IR-LED uitschakelen, de rode LED aanzetten en opnieuw de S-pin uitlezen.

Als je de twee signalen plot, zie je dat IR-waarden altijd lager liggen dan de rode waarden.

Voor een goed signaal: plaats de vingertop correct op de fotodiodo, de LEDs moeten de nagel aanraken. Zodra je een comfortabele positie met goede metingen hebt, verander er niet meer.
Omdat de signalen zwak zijn en ruis problematisch is, heb ik ontdekt dat goede omgevingslicht altijd nodig is. Beweeg niet met je vinger tijdens meten en verander niet het licht; zelfs een schaduw op de sensor kan alles veranderen.
Hoe Wordt SpO2% Gemeten?
De zuurstofverzadiging (SpO2) is de fractie van zuurstofverzadigd hemoglobine ten opzichte van totaal hemoglobine en is een functie van parameter R (gevonden in een academisch paper van het Politecnico di Milano), berekend met minimum- en maximumwaarden van de twee signalen:
R = ((REDmax - REDmin) / REDmin) / ((IRmax - IRmin) / IRmin)
Elk instrument heeft zijn eigen R en kalibratie is nodig om de curve (functie) te vinden die R koppelt aan SpO2%.
We hebben de pieken geteld, nu moeten we de max en min waarden van beide curven (rode en IR LED) vinden.
Daarvoor bepalen we de "periode" van de hartslag (hoeveel milliseconden een hartslag duurt) en delen dit door de samplefrequentie om te weten hoeveel samples een periode vormen. De samplefrequentie is in ons geval 40 milliseconden (20 ms IR, 20 ms rood).
De periode is de tijd tussen twee stijgende flanken in het signaal.
We analyseren de laatste L samples (L = periode / 40), opgeslagen in een array, om REDmax, REDmin, IRmax en IRmin te vinden.
Met deze maximum- en minimumwaarden berekenen we R. R, L en periode worden elke hartslag berekend, dus R ook.
Van R Naar SpO2%: Hoe Kalibreer Je De Oximeter?
De functie die R koppelt aan SpO2 kan worden vereenvoudigd tot een rechte lijn:
SpO2 = K * R + M
We hebben twee punten (twee paren van SpO2 en R) nodig om K en M te bepalen. De enige manier is een referentie-oximeter gebruiken en de waarden van de display af te lezen.
De nieuwe oximeter is de referentie: we lezen de SpO2-waarde terwijl we de R-waarden van onze eigen oximeter meten.
- Adem normaal, noteer SpO2 en R.
- Houd je adem in; na 10-20 seconden daalt de SpO2 op de referentie-oximeter, terwijl de R-parameter van onze oximeter stijgt. Net voor je in val staat, noteer de bereikte SpO2 en de R-waarde.
- Los de twee onbekenden op (twee vergelijkingen) om K en M te vinden.
Nu kunnen we zowel bpm als SpO2 berekenen voor elke meting van R.
Ik heb ook een display toegevoegd om alle getallen te tonen. Waarden worden alleen getoond als er minstens 5 metingen van periodes zijn die niet te veel afwijken (±10% van de periodelengte). Zo filteren we waarden die door slechte componenten, veranderend omgevingslicht of vingerbeweging te veel wisselen.
De c-waarde geeft aan hoeveel stabiele metingen gebruikt zijn voor de berekening.
Projectverbetering: Omgevingslichtvariabiliteit Elimineren
Na enkele dagen experimenteren vond ik een verbetering.
Ik merkte dat met deze goedkope componenten (alleen LEDs en een fotodiodo!) de metingen te sterk afhingen van omgevingslicht, wat niet ideaal is voor een betrouwbare werkomgeving. Omdat de resultaten op een zonnige dag beter waren dan bij bewolkte lucht of kunstlicht 's avonds, besloot ik een derde LED toe te voegen die continu brandt en het vingertopje belicht.

Met deze 3-LED sensor worden de metingen ook onder een zwart doek genomen om omgevingslicht volledig uit te sluiten.

De resultaten zijn nu beter en niet meer afhankelijk van omgevingslicht.
Ik moest de oximeter opnieuw kalibreren; zo zie je in de video dat na enkele seconden correct bpm en SpO2% worden gevonden.
Code
Bestand: oximeter-diy-ver-0.92.ino
/*
* Een DIY oximeter v0.92 (minore fixes)
* Door een KY-039 hartslagsensor te hacken of een IR-led, rode led en fotodiodo te gebruiken.
* https://hackaday.io/project/170752-oximeter-do-it-yourself
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#define maxperiod_siz 80 // max aantal samples in een periode
#define measures 10 // aantal opgeslagen periodes
#define samp_siz 4 // aantal samples voor gemiddelde
#define rise_threshold 3 // aantal stijgende metingen voor piekdetectie
LiquidCrystal_I2C lcd(0x3F, 16, 2);
int T = 20; // slot in ms om sensorwaarde te lezen
int sensorPin = A1;
int REDLed = 3;
int IRLed = 4;
byte sym[3][8] = {
{ B00000, B01010, B11111, B11111, B01110, B00100, B00000, B00000 },
{ B00000, B00000, B00000, B11000, B00100, B01000, B10000, B11100 },
{ B00000, B00100, B01010, B00010, B00100, B00100, B00000, B00100 }
};
void setup() {
Serial.begin(9600);
Serial.flush();
pinMode(sensorPin, INPUT);
pinMode(REDLed, OUTPUT);
pinMode(IRLed, OUTPUT);
lcd.init();
lcd.backlight();
digitalWrite(REDLed, LOW);
digitalWrite(IRLed, LOW);
for (int i = 0; i < 8; i++) lcd.createChar(i, sym[i]);
}
void loop() {
bool finger_status = true;
float readsIR[samp_siz], sumIR, lastIR, reader, start;
float readsRED[samp_siz], sumRED, lastRED;
int period, samples;
period = 0; samples = 0;
int samplesCounter = 0;
float readsIRMM[maxperiod_siz], readsREDMM[maxperiod_siz];
int ptrMM = 0;
for (int i = 0; i < maxperiod_siz; i++) { readsIRMM[i] = 0; readsREDMM[i] = 0; }
float IRmax = 0, IRmin = 0, REDmax = 0, REDmin = 0;
double R = 0;
float measuresR[measures];
int measuresPeriods[measures];
int m = 0;
for (int i = 0; i < measures; i++) { measuresPeriods[i] = 0; measuresR[i] = 0; }
int ptr;
float beforeIR;
bool rising;
int rise_count;
int n;
long int last_beat;
for (int i = 0; i < samp_siz; i++) { readsIR[i] = 0; readsRED[i] = 0; }
sumIR = 0; sumRED = 0;
ptr = 0;
while (1) {
// IR LED aan
digitalWrite(REDLed, LOW);
digitalWrite(IRLed, HIGH);
// Gemiddelde berekenen over 20 ms (elimineert 50Hz netruis)
n = 0; start = millis(); reader = 0.;
do { reader += analogRead(sensorPin); n++; } while (millis() < start + T);
reader /= n;
sumIR -= readsIR[ptr];
sumIR += reader;
readsIR[ptr] = reader;
lastIR = sumIR / samp_siz;
// RODE LED aan
digitalWrite(REDLed, HIGH);
digitalWrite(IRLed, LOW);
n = 0; start = millis(); reader = 0.;
do { reader += analogRead(sensorPin); n++; } while (millis() < start + T);
reader /= n;
sumRED -= readsRED[ptr];
sumRED += reader;
readsRED[ptr] = reader;
lastRED = sumRED / samp_siz;
// R BEREKENING
readsIRMM[ptrMM] = lastIR;
readsREDMM[ptrMM] = lastRED;
ptrMM++; ptrMM %= maxperiod_siz;
samplesCounter++;
if (samplesCounter >= samples) {
samplesCounter = 0;
IRmax = 0; IRmin = 1023; REDmax = 0; REDmin = 1023;
for (int i = 0; i < maxperiod_siz; i++) {
if (readsIRMM[i] > IRmax) IRmax = readsIRMM[i];
if (readsIRMM[i] > 0 && readsIRMM[i] < IRmin) IRmin = readsIRMM[i];
readsIRMM[i] = 0;
if (readsREDMM[i] > REDmax) REDmax = readsREDMM[i];
if (readsREDMM[i] > 0 && readsREDMM[i] < REDmin) REDmin = readsREDMM[i];
readsREDMM[i] = 0;
}
R = ((REDmax - REDmin) / REDmin) / ((IRmax - IRmin) / IRmin);
}
// Vingeraanwezigheid controleren
if (lastRED < lastIR) {
if (finger_status == true) {
finger_status = false;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Geen vinger?");
}
} else {
if (finger_status == false) {
lcd.clear();
finger_status = true;
lcd.setCursor(10, 0); lcd.print("c=");
lcd.setCursor(0, 0); lcd.print("bpm");
lcd.setCursor(0, 1); lcd.print("SpO"); lcd.write(1);
lcd.setCursor(10, 1); lcd.print("R=");
}
}
float avR = 0; int avBPM = 0;
if (finger_status == true) {
if (lastIR > beforeIR) {
rise_count++;
if (!rising && rise_count > rise_threshold) {
lcd.setCursor(3, 0); lcd.write(0); // hartje
rising = true;
measuresR[m] = R;
measuresPeriods[m] = millis() - last_beat;
last_beat = millis();
int period = 0;
for (int i = 0; i < measures; i++) period += measuresPeriods[i];
period = period / measures;
samples = period / (2 * T);
int avPeriod = 0; int c = 0;
for (int i = 1; i < measures; i++) {
if (measuresPeriods[i] < measuresPeriods[i-1] * 1.1 && measuresPeriods[i] > measuresPeriods[i-1] / 1.1) {
c++; avPeriod += measuresPeriods[i]; avR += measuresR[i];
}
}
m++; m %= measures;
lcd.setCursor(12, 0); lcd.print(String(c) + " ");
avBPM = 60000 / (avPeriod / c);
avR = avR / c;
lcd.setCursor(12, 1);
if (c == 0) lcd.print(" "); else lcd.print(String(avR) + " ");
if (c > 4) {
int SpO2 = -19 * R + 112; // Kalibratieformule
lcd.setCursor(4, 0);
if (avBPM > 40 && avBPM < 220) lcd.print(String(avBPM) + " ");
lcd.setCursor(4, 1);
if (SpO2 > 70 && SpO2 < 150) lcd.print(" " + String(SpO2) + "% ");
} else {
if (c < 3) {
lcd.setCursor(3, 0); lcd.write(2); // vraagteken
lcd.setCursor(4, 1); lcd.write(2);
}
}
}
} else {
rising = false; rise_count = 0;
lcd.setCursor(3, 0); lcd.print(" ");
}
beforeIR = lastIR;
}
// Serial plot data
Serial.print(lastIR); Serial.print(","); Serial.print(lastRED); Serial.println();
ptr++; ptr %= samp_siz;
}
}
Schema's
In mijn project heb ik een KY-039 gemodificeerd, maar die sensor zit niet in de Fritzing-bibliotheek. Ik heb hem daarom opgebouwd uit de losse componenten, maar kon geen passende fotodiodo vinden in Fritzing.

Een eenvoudige oximeter om zuurstof in het bloed te meten kan gemaakt worden door een KY-039 sensor te hacken of een sensor vanaf nul te bouwen.
Productieproces
- Een DIY Infrarood Bewegingssensor Bouwen voor Raspberry Pi – Stapsgewijze Handleiding
- Een LED Besturen met een PIR-bewegingssensor op Raspberry Pi
- Een Goedkope Raspberry Pi Grondvochtigheidssensor Bouwen voor Slimme Irrigatie
- De DS18B20 Temperatuursensor Testen op Raspberry Pi
- Een Raspberry Pi thuistemperatuurmonitor bouwen met MCP9808, InfluxDB & Grafana
- Sensor Telemetrie 2.0.1 – Geavanceerde Temperatuurbewaking met Azure IoT Hub
- Emotiesensor & EEG: Goedkope Hardware voor Real-time Emotiedetectie
- DIY Arduino TV-B-Gone: Universele TV-uit-schakelaar
- Precieze infrarood temperatuurmeting voor kleurgevoelige processen
- Passieve Infrarood Sensor Stroombespaar Schakeling: Ontwerp, Werking en Toepassingen
- Welke LiDAR-golflengte zal automotive toepassingen domineren?