MAX3010x Pulsoximetermodule (Teil 2)

Module Biometrie

Da wir nun mithilfe des MAX3010x Daten erfassen können, ist es an der Zeit, unseren Algorithmus zur Pulsmessung auf diese anzuwenden.

Pulsmessung mit dem MAX3010x

Im letzten Teil des Tutorials haben wir ein MAX3010x Modul an den Arduino angeschlossen und gelernt, wie man mit diesem Daten erfassen kann. Auch wenn man die Herzschläge schon sehen kann, wenn man die Daten im seriellen Plotter anzeigt, wäre es besser, wenn wir die Herzfrequenz direkt anzeigen könnten. Dazu werden wir den Algorithmus anpassen, den wir beim Experimentieren mit den einfachen Herzfrequenzmodulen verwendet haben.

Portieren des Codes

Was ist nötig, um den Code zur Pulsmessung mit dem MAX3010x verwenden zu können? Eigentlich nicht viel. Theoretisch müssen wir nur den Teil anpassen, in dem wir die Daten vom Sensor erfassen. Anstelle von analogRead rufen wir nun die Methode readSample des MAX3010x Sensors auf und initialisieren diesen in der setup Prozedur.

Der angepasste Code sieht dann wie folgt aus:

#include <MAX3010x.h>

// Sensor (adjust to your sensor type)
MAX30100 sensor;

// Constants
const float rThreshold = 0.7;
const float decayRate = 0.02;
const float thrRate = 0.05;
const int minDiff = 50;

// Current values
float maxValue = 0;
float minValue = 0;
float threshold = 0;

// Timestamp of the last heartbeat
long lastHeartbeat = 0;

// Last value to detect crossing the threshold
float lastValue = 0;

void setup() {
  Serial.begin(9600);

  if(sensor.begin()) { 
    Serial.println("Sensor initialized");
  }
  else {
    Serial.println("Sensor not found");  
    while(1);
  }
}
  
void loop() {
  auto sample = sensor.readSample(1000);
  float currentValue = sample.red;

  maxValue = max(maxValue, currentValue);
  minValue = min(minValue, currentValue);

  // Detect Heartbeat
  float nthreshold = (maxValue - minValue) * rThreshold + minValue;
  threshold = threshold * (1-thrRate) + nthreshold * thrRate;
  threshold = min(maxValue, max(minValue, threshold));
  
  if(currentValue >= threshold 
      && lastValue < threshold 
      && (maxValue-minValue) > minDiff 
      && millis() - lastHeartbeat > 300) {
        
    if(lastHeartbeat != 0) {
      // Show Results
      int bpm = 60000/(millis() - lastHeartbeat);
      if(bpm > 50 && bpm < 250) {
        Serial.print("Heart Rate (bpm): ");
        Serial.println(bpm);
      }
    }
    lastHeartbeat = millis();
  }

  // Decay for max/min
  maxValue -= (maxValue-currentValue)*decayRate;
  minValue += (currentValue-minValue)*decayRate;

  lastValue = currentValue;
}

Wenn man den neuen Code mit dem alten vergleicht, kann man sehen, dass auch einige Konstanten angepasst wurden. Da der Wertebereich des MAX3010x viel größer ist, habe ich die Mindestdifferenz zur Erkennung eines Herzschlags von 5 auf 50 erhöht. Bei Verwendung des MAX30101, MAX30102 oder MAX30105 kann ein noch höherer Wert erforderlich sein, um zu verhindern, dass zufälliges Sensorrauschen bereits als Herzschlag erkannt wird. Der Grund dafür ist, dass diese drei Sensoren eine höhere Auflösung haben als der MAX30100 und somit der Wertebereich noch größer ist. Eine Änderung von 50 bedeutet für sie nur eine sehr kleine Änderung.

Darüber hinaus wurden die Anfangswerte für minValue, maxValue und threshold auf 0 gesetzt. Im alten Code war der Minimalwert auf den höchstmöglichen Wert und das Maximum auf den niedrigsten Wert gesetzt, sodass sie sofort durch den ersten gemessenen Wert ersetzt wurden. Der Schwellwert wurde früher auf die Mitte des erwarteten Wertebereichs gesetzt. Bei den MAX3010x Modulen unterscheidet sich der Wertebereich jedoch abhängig vom Sensortyp. Beim MAX30100 ist er, wie gesagt, kleiner. Das bedeutet, dass es nicht mehr möglich ist, mit dieser Taktik zu arbeiten und es ist auch nicht wirklich sinnvoll. Aufgrund der Umgebungslichtkompensation bei den MAX3010x Sensoren ist der Messwert ohne Finger auf dem Sensor praktisch null. Die Anfangswerte auf null zu setzen, ist deshalb die sinnvollere Standardeinstellung für die MAX3010x-Sensoren.

Funktioniert der Code? Ja, er tut es. In der Grafik unten sind einige Testdaten dargestellt, die mit einem MAX30100 aufgezeichnet wurden. Man sieht, dass die Herzschläge vom Algorithmus erfolgreich erkannt werden.

Relativer Schwellwert (%)
Mindestdifferenz
Anpassungsrate für Minimum und Maximum
Anpassungsrate für den Schwellwert


Es gibt allerdings ein Problem, das diese Grafik nicht zeigt - es dauert eine ganze Weile, bis die ersten Pulsmessungen auf der seriellen Konsole angezeigt werden. Man muss etwa 20 Sekunden warten, nachdem man den Finger auf den Sensor gelegt hat. Warum dauert das so lange?

Das unten stehende Diagramm zeigt die aufgezeichneten Daten direkt nachdem der Finger auf den Sensor gelegt wurde. Es macht das Problem deutlich: Minimum und Maximum werden nicht schnell genug angepasst. Insbesondere das Maximum sinkt nicht schnell genug. Das Interessante daran ist, dass dies nur am Anfang ein Problem ist, da der Gleichspannungsversatz des Sensorsignals kontinuierlich abnimmt, nachdem der Finger auf den Sensor gelegt wurde. Ein Grund dafür ist einerseits, dass es unmöglich ist, den Finger immer mit genau dem gleichen Druck auf den Sensor zu halten. Ein weiteres Problem ist jedoch, dass sich der Sensor erwärmt, wenn man den Finger darauf legt. Dadurch ändern sich die Messwerte, da u.a. die Wellenlänge der LED mit der Temperatur leicht variiert.

Relativer Schwellwert (%)
Mindestdifferenz
Anpassungsrate für Minimum und Maximum
Anpassungsrate für den Schwellwert


Lässt sich dieses Problem beheben? Nun, wir können es versuchen, indem wir die Anpassungsrate für Maximum, Minimum und Schwellwert erhöhen. Eine schnellere Anpassung von Maximum und Minimum bewirkt eine frühere Erkennung des Herzschlags. Wenn die Werte jedoch zu hoch sind, führt dies ebenfalls zu Problemen bei der Herzschlagerkennung, da sich die Werte nun zwischen den einzelnen Herzschlägen nun zu schnell anpassen.

Wir lassen dieses Problem erst einmal beiseite. Es gibt andere Probleme, die etwas einfacher zu beheben sind. Nämlich das, dass der Code die Erkennung von Herzschlägen in den Daten nicht verhindert, wenn gar kein Finger auf dem Sensor liegt. Dank der Umgebungslichtunterdrückung können wir das jetzt leicht überprüfen. Hierfür passen den Code noch einmal an.

Fingererkennung

Wie können wir erkennen, dass ein Finger auf dem Sensor liegt? Nun, wir können nicht sagen, ob es wirklich ein Finger ist, aber es ist einfach zu erkennen, ob sich etwas vor dem Sensor befindet. Wenn sich dort nichts befindet, liegen die Messwerte annähernd bei null. Befindet sich ein Finger auf dem Sensor, liegt der Wert beim MAX30100 bei über 10000. Dies wird unser Schwellwert für die Fingererkennung. Für die MAX30101, MAX30102 und MAX30105 könnte man einen deutlich höheren Wert nutzen, aber ein Schwellwert von 10000 funktioniert auch für sie.

Was tun wir nun aber, wenn kein Finger auf dem Sensor liegt? Es liegt auf der Hand, dass wir in diesem Fall den Algorithmus zur Pulsmessung nicht ausführen wollen. Außerdem sollten wir die gespeicherten Werte zurücksetzen. Wenn wir den Finger das nächste Mal auf den Sensor legen, ist er wahrscheinlich etwas anders positioniert, und wir müssten ohnehin ein neues Minimum, Maximum und einen neuen Schwellwert für die Herzschlagerkennung bestimmen.

Hier siehst du, wie der Code aussieht, wenn das alles implementiert ist:

#include <MAX3010x.h>

// Sensor (adjust to your sensor type)
MAX30100 sensor;

// Constants
const float rThreshold = 0.7;
const float decayRate = 0.02;
const float thrRate = 0.05;
const int minDiff = 50;

// Current values
float maxValue = NAN;
float minValue = NAN;
float threshold = NAN;

// Timestamp of the last heartbeat
long lastHeartbeat = 0;

// Last value to detect crossing the threshold
float lastValue = NAN;

void setup() {
  Serial.begin(9600);

  if(sensor.begin()) { 
    Serial.println("Sensor initialized");
  }
  else {
    Serial.println("Sensor not found");  
    while(1);
  }
}
  
void loop() {
  auto sample = sensor.readSample(1000);
  float currentValue = sample.red;

  if(currentValue > 10000) {
    maxValue = max(maxValue, currentValue);
    minValue = min(minValue, currentValue);
  
    // Detect Heartbeat
    float nthreshold = (maxValue - minValue) * rThreshold + minValue;
    if(isnan(threshold)) {
      threshold = nthreshold;
    }
    else {
      threshold = threshold * (1-thrRate) + nthreshold * thrRate;
      threshold = min(maxValue, max(minValue, threshold));
    }
    
    if(currentValue >= threshold 
        && lastValue < threshold 
        && (maxValue-minValue) > minDiff 
        && millis() - lastHeartbeat > 300) {
          
      if(lastHeartbeat != 0) {
        // Show Results
        int bpm = 60000/(millis() - lastHeartbeat);
        if(bpm > 50 && bpm < 250) {
          Serial.print("Heart Rate (bpm): ");
          Serial.println(bpm);
        }
      }
      lastHeartbeat = millis();
    }
  
    // Decay for max/min
    maxValue -= (maxValue-currentValue)*decayRate;
    minValue += (currentValue-minValue)*decayRate;
  
    lastValue = currentValue;
  }
  else {
    // No finger -> Reset Values  
    lastHeartbeat = 0;
    lastValue = NAN;
    maxValue = NAN;
    minValue = NAN;
    threshold = NAN;
  }
}

Im Code wird der Wert NAN, was für ''not a number'' steht, verwendet, um zu signalisieren, dass kein gültiger Wert gesetzt wurde. Der Algorithmus verwendet dann die ersten gültigen Daten, um die Variablen neu zu initialisieren.

Bei normalem Betrieb mit einem Finger auf dem Sensor haben die Änderungen keine Auswirkungen. Dies lässt sich gut in der nachfolgenden Grafik erkennen. Fällt der gemessene Wert jedoch unter den Schwellwert, wird die Pulsmessung ausgesetzt. Du kannst dies ausprobieren, indem du den Schwellwert für das unten stehende Diagramm manuell erhöhst.

Relativer Schwellwert (%)
Mindestdifferenz
Anpassungsrate für Minimum und Maximum
Anpassungsrate für den Schwellwert
Schwellwert für Fingererkennung

Das Ergebnis

Werfen wir einen Blick auf das Ergebnis:
Pulsmessungen im seriellen Monitor

Um ehrlich zu sein, ist das Ergebnis ein wenig enttäuschend. Ja, wir können jetzt zuverlässig erkennen, wenn kein Finger auf dem Sensor liegt. Allerdings ist die Herzschlagerkennung instabil und ob sie funktioniert, hängt stark von der Positionierung und dem Druck auf den Finger ab. Wenn es funktioniert, dann haben wir eine hohe Variabilität zwischen einzelnen Messungen. Wie bereits in Teil 3 der Serie über einfache Herzfrequenzmodule erläutert, ist dies zu erwarten. Allerdings sind nicht alle Schwankungen auf die natürliche Herzfrequenzvariabilität zurückzuführen. Wo liegt das Problem?

Nun, das erste Problem ist, dass die Samplingrate zu niedrig ist. Dieses Problem hatten wir auch bei einfacheren Herzfrequenzmodulen. Die einfache Lösung dafür ist, die Samplingrate zu erhöhen. Nach der Anpassung sollten die Stufen zwischen den einzelnen Werten weniger auffällig werden.

Es gibt aber noch ein grundsätzliches Problem: Das mit den MAX3010x-Modulen aufgezeichnete Signal hat eine andere Form, was auf die Verwendung einer roten statt einer grünen LED zurückzuführen ist. Wir haben keine klare Spitze mehr, auf die wir triggern können. Wenn man sich die Signalform ansieht, scheint es viel sinnvoller zu sein, auf die fallende Flanke nach jedem Herzschlag zu triggern. Außerdem haben wir immer noch das Problem mit dem sich verschiebenden DC-Offset direkt nach dem Auflegen des Fingers auf den Sensor. Beide Probleme führen dazu, dass die Herzschlagerkennung unzuverlässig ist. Im nächsten Teil dieses Tutorials werden wir beide Probleme lösen, indem wir eine andere, wissenschaftlichere Methode zur Herzfrequenzmessung verwenden.

Vorheriger Beitrag Nächster Beitrag