Schon länger wollte ich ein Pulsoxymeter bauen, um den Puls und die Sauerstoffsättigung des Blutes zu messen. Es sieht recht einfach aus: Mikrocontroller auswählen. Vier Drähtchen anlöten. Libraries installieren und Sketch flashen. Fertig! Fertig?
Es ist dann doch nicht ganz so einfach. Erwartungsgemäß läuft das kleine Gerät auf Anhieb. Der Sensor ist ein kleines Wunder an rauscharmer Signalverarbeitung und Integrationsdichte. Das macht ihn interessant, aber auch etwas empfindlich.
Das Breadboard zeigt den ersten Entwurf. Die ersten beiden Stellen des Displays zeigen die Herzfrequenz. Nach dem Doppelpunkt zeigen Stelle 3 & 4 die Sauerstoffsättigung in %.
Benötigte Hardware
![](https://cdn.shopify.com/s/files/1/1509/1638/files/Breadboard_d540dd57-af90-46ac-8b83-cb64aedd6e54_1024x1024.jpg?v=1717088861)
Das sieht doch schon mal ganz gut aus: Die Herzfrequenz auf der iWatch und die Anzeige am Prototyp stimmen exakt überein.
Das ist kein reiner Zufall, sondern kommt durch den Sketch auf dem seeeduino XIAO zustande, der einen gleitenden Mittelwert der gemessenen Herzfrequenzwerte bildet. Dafür sammelt er 15 Sekunden lang die Daten und liefert danach jede Sekunde einen Wert.
Über Abweichungen von +/- 10% sollte man sich jedoch nicht wundern.
Es gilt ein paar Verhaltensregeln zu beachten. Dazu gleich mehr.
Recherchen
Vor Beginn der Arbeiten habe ich recht eingehend recherchiert. Ganz viele Anleitungen sind im Netz zu finden. Es scheint, dass sie alle von ein paar wenigen Ur-Libraries abstammen, die sich auf GitHub finden lassen. Der MAX30100 ist schon seit fast zehn Jahren auf dem Markt. Heute werden die Nachfolger 30102 und 30105 angeboten. Auch zu diesen Sensoren gibt es Breakout-Boards.
Es sind neben dem kostenlosen eBook von AZ-Delivery viele detaillierte Beschreibungen zur Funktionsweise des Sensors, teils im Umfang einer kleinen Dissertation verfügbar. Für dieses Projekt habe ich mich an das eBook von AZ gehalten.
Zur Funktionsweise nur so viel: Eine IR-LED (880 nm) und eine rot leuchtende LED (660 nm) beleuchten deinen Finger, während eine spezielle Fotooptik das reflektierte Licht aufnimmt. Dieses Signal wird im MAX30100 ausgewertet. Die Signalverarbeitung ist tricky-dicky und braucht etwas Mathe. Wer mehr wissen will, mag sich gerne in die Formeln dazu vertiefen: https://de.wikipedia.org/wiki/Pulsoxymetrie
Über den I2C-Bus schickt der MAX30100 seine Ergebnisse an den Mikrocontroller (z.B. AT-, Seeeduino). Dort wandelt die jeweilige Bibliothek (siehe Sketch) die Rohdaten um und liefert die Pulsfrequenz und den SpO2-Wert, der über den Sauerstoffgehalt des Blutes Auskunft gibt. Ganz ähnliche Profigeräte findet man beim Hausarzt.
Die Praxis
Bei den ersten Tests bekam ich sofort plausible Werte für den SpO2. Die angezeigten Werte für die Pulsfrequenz schwankten jedoch nicht nur um mehr als 50 %, sie waren teils völlig daneben. Eine erneute Recherche im Web zeigte, dass es wohl ganz vielen Anwendern genauso geht. In meinem späteren Sketch findet sich ein „Korrekturwert“ (es ist der Wert „42“ - angeblich die Lösung vieler Probleme im Universum). Auch das Timing des Sketches ist nicht unkritisch.
Nach einer Reihe von Versuchen waren die wichtigsten Parameter klar:
Der Strom, der durch die LEDs fließt, kann über die Library eingestellt werden. Dafür gibt es eine Liste zulässiger Einstellungen (siehe GitHub-Link). Grundsätzlich gilt: Je weniger Transparenz die Haut aufweist, desto mehr Licht ist nötig. Dickhäuter bitte mit 50 mA beleuchten! Es gibt keine klare Regel, wie man die optimale Einstellung findet. Da es professionelle Geräte gibt, die für jeden Patienten korrekte Werte liefern müssen, habe ich bei den Profis recherchiert. Leider ohne Ergebnis. Dieses Geheimnis konnte - noch - nicht gelüftet werden.
Während der Messung sollte der Probant sitzen und seine Hand locker und ruhig halten. Der Finger soll nur leicht auf dem Sensor aufliegen. Schon wenn man den Finger auf den Sensor drückt, bekommt man falsche Werte. Das Gehäuse habe ich so eingerichtet, dass der Finger möglichst gut aufliegt. Aber auch diese Lösung ist nicht für jede Person ideal.
Bei den ersten Versuchen war das Gerät noch auf einem Breadboard aufgebaut. Schon das unvermeidliche Umgebungslicht führt zu Störungen. Die Ergebnisse nach dem Einbau ins Gehäuse (siehe Fotos) waren besser, als beim Versuchsaufbau.
Zu befriedigenden Ergebnissen bin ich gekommen, nachdem ich im Sketch einen gleitenden Mittelwert errechnete. Praktisch sieht das so aus, dass der Sketch für die ersten 15 Sekunden die Herzfrequenz ausblendet, während er ein Array mit den Messwerten füllt. Danach zeigt er laufend den gemessenen Mittelwert an. Den SpO2-Wert zeigt er sofort an.
Wenn die Werte einer anderen Person gemessen werden sollen, empfehle ich den Neustart des Gerätes. Zwar leert mein Sketch automatisch das Messdaten-Array, aber nicht unbedingt den Cache des MAX30100. Deswegen bitte neu starten. Es hat sich bewährt.
Prototyp des Pulsoxymeter in einer herzförmigen Holzbox
Um gute Ergebnisse zu bekommen, empfiehlt sich dieser Ablauf:
♥︎ Finger bei ausgeschaltetem Gerät auflegen, ruhig sitzen, ruhig atmen.. Oooommmm ….
♥︎ Finger nur leicht auflegen, bitte nicht drücken.
♥︎ Einschalten und messen.
♥︎ Die Herzfrequenz (der Puls) wird nach 15s erstmals angezeigt.
Die 15s „Aufwärmzeit“ haben noch den Nebeneffekt, dass sich die Herzfrequenz etwas in Richtung „Ruhepuls“ stabilisieren kann. Wer es noch besser machen will, entspannt sich ganz bewusst und gibt sich eine Ruheminute vor der Messung.
Meinen Prototypen kann ich aufklappen, so dass zu sehen ist, ob der Finger richtig aufliegt.
Aufbau der Schaltung
Die Schaltung ist denkbar einfach ausgeführt. Der Sensor ist an den SCL, SDA-Anschlüssen des seeeduino angeschlossen, d.h. an A4 und A5. Da noch jede Menge freier Anschlüsse vorhanden sind, wurde das TM1637-Display an D2 und D3 angeschlossen. Diese Verbindungen kann man frei auswählen, wenn man im Sketch die entsprechende Auswahl trifft.
Beim Aufbau des Prototypen in der Box habe ich die wenigen Verbindungen gelötet. Nur die Zuleitungen zum Display wurden als Steckverbindung ausgeführt.
Zum Flashen des Mikrocontrollers wird die Schaltung über USB an den PC angeschlossen. Danach kann sie mit zwei 1,5 V AAA - Batterien betrieben werden. Für diese Variante habe ich mich entschieden. Dafür braucht man lediglich einen zusätzlichen Ein-/ Ausschalter, den ich mit einem Magneten und einem Reedschalter realisiert habe. Wenn ich den kleinen Magneten auf die Herzspitze lege, schaltet das Gerät ein.
Das ist eher ein kleiner Gag. Ein klassischer Kippschalter tut den gleichen Dienst.
Der Sketch
Der Sketch basiert auf der Library „MAX30100_PulseOximeter.h“ des GitHub-Autors „oxullo“. Nachdem die Library geladen ist, lädst du den Sketch hoch. Mein Programm basiert auf dem „MAX30100_Minimal“- Beispiel des gleichen Autors.
Über den seriellen Port kann man dem Gerät beim Start und bei der Arbeit zuschauen. Falls die I2C-Verbindung zum Sensor nicht funktioniert, meldet der Sketch diesen Fehler.
Sobald der Sensor erstmals den Puls erkennt („onBeatDetected“), beginnt die laufende Messung mit einer Periodendauer von einer Sekunde.
In diesem Sketch ist der LED-Strom auf 7,6 mA eingestellt. Mit diesem Wert evtl. experimentieren. Wenn kein Herzschlag angezeigt wird, völlig unsinnige Werte kommen, oder auch wenn die Sauerstoffsättigungswerte seltsam erscheinen, können Sie den jeweils nächst höheren Wert ausprobieren. Dafür bitte die korrekte Syntax beachten: pox.setIRLedCurrent(MAX30100_LED_CURR_7_6MA);
Hier die Liste der möglichen Einstellungen in diesem Sketch:
- MAX30100_LED_CURR_4_4MA
- MAX30100_LED_CURR_7_6MA
- MAX30100_LED_CURR_11MA
- MAX30100_LED_CURR_14_2MA
- MAX30100_LED_CURR_17_4MA
- MAX30100_LED_CURR_20_8MA
- MAX30100_LED_CURR_24MA
- MAX30100_LED_CURR_27_1MA
- MAX30100_LED_CURR_30_6MA
- MAX30100_LED_CURR_33_8MA
- MAX30100_LED_CURR_37MA
- MAX30100_LED_CURR_40_2MA
- MAX30100_LED_CURR_43_6MA
- MAX30100_LED_CURR_46_8MA
- MAX30100_LED_CURR_50MA
Es sind nur diese Werte zulässig, die Sie auch in meinem GitHub-Repository als Liste der „zulässigen LED-Ströme“ finden. Sie können sich dort einen Wert aussuchen und über Copy-and-Paste in Ihren Sketch übertragen. Neu kompilieren-hochladen fertig.
Da unser Display auf vier Stellen beschränkt ist, wird die Herzfrequenz bis 99 BpM dargestellt - höher sollte ein Ruhepuls auch nicht sein. Für Ihren Arzt ist eine Ruhefrequenz > 88 BpM bereits zu viel. Höhere Werte werden zwar korrekt angezeigt, doch ohne die „1“ für die 100er-Stelle. Bei einem Puls von 110 zeigt das Gerät eine „10“ in den beiden linken Stellen an.
Die Sauerstoffsättigung des Blutes kann maximal 100% betragen. Da auch ohne erkannten Puls eine „00“ auf den beiden rechten Stellen angezeigt wird, erlaubt sich der Sketch einen Eingriff in den Messwert (so etwas würde ich als Physiker sonst niemals tun!!) und zeigt statt 100% eine 99% an. Es sei mir verziehen. Auch ‚echte‘ 99% werden selbstverständlich korrekt angezeigt.
Hier der Sketch (Download):
#include <Wire.h> #include "MAX30100_PulseOximeter.h" #include <TM1637.h> // Grove 4-Digit Display #define REPORTING_PERIOD_MS 1000 PulseOximeter pox; // Instanz des Objektes PulseOximeter uint32_t tsLastReport = 0; int A[15]; // A[0] wird nicht benutzt, weil der Index auch als Divisor dient int count = 1; int hrav = 0; int i = 0; int spo2 = 0; int hr = 0; const int CLK = 2; const int DIO = 3; TM1637 tm1637(CLK, DIO); void onBeatDetected() { Serial.print("♥︎ "); } void arrayclear(){ for (i=1; i==15; i++){ A[i]=0; } } void setup() { Serial.begin(115200); Serial.print("PulseoOxymeter starten ..."); if (!pox.begin()) { Serial.println("Fehler"); for(;;); } else { Serial.println("Initialisierung erfolgreich"); } pox.setIRLedCurrent(MAX30100_LED_CURR_27_1MA); // Einstellung für Pachydermen pox.setOnBeatDetectedCallback(onBeatDetected); tm1637.init(); tm1637.set(BRIGHT_TYPICAL); arrayclear(); //Ergebnisarray löschen } void loop() { pox.update(); tm1637.point(POINT_ON); if (millis() - tsLastReport > REPORTING_PERIOD_MS) { hr = pox.getHeartRate(); if (count<15){ A[count] = hr; hrav = 0; for (int i=1; i<count+1; i++ ) { hrav = hrav+A[i]; } hrav = hrav/count; count++; } else { hrav = 0; A[15] = hr; for (int i=1; i<16; i++) { hrav = hrav+A[i]; } hrav = hrav/45; // Umrechnungsfaktor for (int i=1; i<15; i++) { A[i] = A[i+1]; } } Serial.print("ø Heart rate:"); Serial.print(hrav); Serial.print("bpm / SpO2:"); spo2=int(pox.getSpO2()); Serial.print(spo2); Serial.println("%"); if (spo2 == 0){ hrav = 0; arrayclear(); count = 1; tm1637.clearDisplay(); } if (spo2 == 100){ spo2 = 99; } if (count ==15) { tm1637.display(0, (hrav / 10) % 10); tm1637.display(1, hrav % 10); } tm1637.display(2, (spo2 / 10) % 10); tm1637.display(3, spo2 % 10); tsLastReport = millis(); } }
Der Sketch wurde mit dem AT-Mega328p-Board, dem Nano V3 und dem seeeduino getestet. Letzterer wurde eingebaut, weil er besonders kompakt ist und mehr als ausreichend Anschlüsse hat.
Bekannte Problemchen
Sie können eine Online-Diskussion verfolgen - wenn Sie Spaß daran haben - bei der es darum geht, ob die oxullo-Library wirklich optimal ist. Es gibt ein paar Alternativen dazu. Besseres habe ich jedoch nicht gefunden.
Eine Eigenheit ist das Verhalten des Gerätes, wenn eine Messung erfolgt ist und dann der Finger wieder entnommen wird. Sie werden vielleicht feststellen, dass die Werte für Puls und Sauerstoffsättigung nicht auf Null gehen. Offenbar funktioniert „onBeatDetected“ nicht ganz perfekt. Das Ergebnis sind „Phantomwerte“ (so etwas lieben wir …). Es können auch Licht-Reflexionen im Sensor selbst sein, die ihn nicht erkennen lassen, dass der Finger schon weg ist.
Deswegen meine Empfehlung: Der oben erklärte Messzyklus funktioniert gut. Nehmen Sie sich die Zeit und lassen Sie Ihre Messung in Ruhe genauso ablaufen.
Messgenauigkeit
Unser DIY-Gerät ist ein nettes Spielzeug mit einem vernünftigen, professionellen Hintergrund. Es ist aber kein medizinisches Gerät, wie es Ihr Arzt benutzt. Deswegen habe ich meinen SpO2 vom Arzt messen lassen und dabei erfreut festgestellt, dass der dort (mehrfach) gemessene Wert noch gut 2-3% besser ist, als die Anzeige des Pulsoxymeter.
Eine Aussage darüber, ob man das generell annehmen darf, wage ich allerdings nicht. Gehen Sie doch mal selbst zum Doc und nehmen Ihr Gerätchen mit.
Ein Wunsch noch an die AZ-Community – speziell an Maker mit 3D-Drucker:
Dieses Projekt eignet sich doch ganz hervorragend für ein 3D-gedrucktes Gehäuse, das an die kompakten Komponenten angepasst ist. Das wäre doch ein tolles Projekt? Und vielleicht hat jemand Freude daran, ein solches Gehäuse zu entwerfen, zu drucken und die Daten bereit zu stellen. Vielleicht gibt es dann auch für Leute ohne 3D-Drucker die Möglichkeit, sich für ein paar €€€ beim Kollegen so etwas drucken zu lassen.
Viel Spaß mit dem Gerät. Ich freue mich, wenn Sie Ihre Erfahrungen als Kommentare senden.
Viele Grüße
Michael Klein
https://github.com/michael5411/pulsoxymeter_seeeduino.git
Mit dem .zip-Download von GitHub bekommen Sie alle Dateien auf einmal.
Liste der Dokumente
- Sketch zum direkten Download
- Liste der zulässigen LED-Ströme
- Fritzing-Darstellung des Breadboard-Aufbaus
- Readme-Datei
4 Kommentare
Michael Klein
Hallo Joern: Danke für die interessante Anregung. Es würde mich sehr freuen, wenn der Sketch entsprechend erweitert würde.
Hallo Willem: War gerade noch einmal auf meinem Repository. Wenn man dort unter „CODE“ auf „Download ZIP“ geht, bekommt man tatsächlich die ganzen Infos – das ist so gewollt – aber auch „Pulsoxymeter_final.ino“, als den gesuchten Sketch. Und der funktioniert auch, wie ich mich gerade überzeugt habe.
Die Fehlermeldung des IDE 2.3.2 (die ich auch benutze) ist ein Hinweis darauf, dass die installierte Library des TM1637 keine Methode „set“ kennt. Achtung bitte: Es gibt leider nicht nur ein TM1637-Library, sondern leider mehrere. Und wenn man eine andere installiert hat, als die, die in meinem Sketch verwendet wird (Schreibweise bitte genau beachten), dann fällt der Compiler auf die Bretter. Habe sogar schon mal den Fall gehabt, dass ich zwei Libraries mit unterschiedlichen Bezeichnungen hatte und – irgendwie, vielleicht ein Cache? – nahm er immer die falsche Lib. Ich musste erst die falsche Lib deinstallieren, dann töfft es auf einmal.
Viel Erfolg – und JA – für alle Rückfragen bin ich immer gerne zu haben.
Gruß
Michael
Andreas Wolter
@Willem: am Anfang des Sketches finden Sie den Downloadlink zur INO-Datei, der auch funktioniert. Alternativ können Sie auf die Github-Seite wechseln und dort ebenfalls diese Datei herunterladen. Es ist auch möglich, den Quellcode hier von der Webseite zu kopieren.
Grüße,
Andreas Wolter
AZ-Delivery Blog
Joern Becker
Hallo Michael
Vielen Dank für dein Beispielprojekt zum Pulsoximeter, welches mich ermutigt, diese Projekt auch anzugehen.
Vorab würden mich die ungefilterten Pulsdaten für die Dauer 1 Minute interessieren, die du damit misst. Die Varianz der Pulsrate ist die HRV oder Herzratenvariabilität, welche viel über den Stresslevel aussagt: hohe HRV niedriger Stresslevel und umgekehrt. Dieser Wert wird meines Wissens von keinem Produkt unter 400€ (kontinuierlich) gemessen, wobei er aber hohe prognostische Funktion hat (→Wikipedia “HRV”)
Willem
Hallo,
Sehr interessanter Beitrag. Das grosse Problem ist erstens der Downloadlink, statt des sketches bekommt man eine 327kb grosse Datei mit ganz vielen Infos aber keinen sketch.
Beim komplieren des kopierten sketche meldet meine IDE 2.3.2 folgenden Fehler:
Compilation error: ‘class TM1637’ has no member named ‘set’; did you mean ‘setDp’?
setDp funktioniert aber auch nicht.
Schade eigentlich. es liest sich alles sehr gut.
Willem