11. Arduino-Projekt: CO2-Messung mit dem Sensor MHZ19B (1/1)

Sensor und Messprinzip

Der Sensor MHZ19B, erhältlich z. B. bei Reichelt oder Banggood, ist ein Sensor zur Messung des CO2-Gehalts der Luft und der Temperatur.

 mhz19b
Quelle: www.banggood.com

Die CO2-Messung basiert auf dem sogenannten nichtdispersiven Infrarotprinzip, dabei wird ein Infrarotlichtstrahl durch eine mit der zu messenden Luft gefüllten Glasröhre geschickt. Ein spezieller Infrarotsensor misst am anderen Ende, wie viel Licht vom CO2 absorbiert wurde, indem die Differenz des einfallenden mit dem austretenden Licht gebildet wird.

ndir-prinzip.fwNDIP-Messung

Der CO2-Gehalt der Luft wird in PPMs (Parts per Million) angegeben. 1 ppm entspricht bei Gasen 1 µL pro Liter Gas. Normale Luft hat etwa 400 ppm CO2-Gehalt, das entspricht 0,04% CO2-Anteil.

Der Sensor funktioniert mit Spannungen bis 5,5 V und besitzt 5V-kompatible Signalein- und -ausgänge, was wichtig für den Betrieb am Arduino ist.

Der Messwert wird entweder als pulsweitenmoduliertes Rechtecksignal ausgegeben, bei dem die Länge der positiven Halbwelle dem PPM-Wert entspricht oder als serieller Digitalwert über die im Sensor befindliche serielle Schnittstelle (UART = Universal Asynchronous Receiver Transmitter). Genauere Angaben finden sich im Datenblatt. Der Sensor misst entweder 0-2000ppm oder 0-5000ppm. Der Wertebereich kann über die serielle Schnittstelle eingestellt werden.

mhz19bpinoutMHZ19B-Pinout

Im Datenblatt konnte kein Hinweis auf die Defaulteinstellung des Messbereichs gefunden werden, es scheint so, als ob verschiedene Typen des Sensors verkauft würden. Der eingestellte Messbereich kann über die UART abgefragt werden: mhtz19b_uart_get_range.ino

Die Hardware

Die Verdrahtung mit dem Arduino wird etwas erschwert, da die Anschlüsse des MHZ19B-Sensors nicht auf ein Steckbrett passen - die Verdrahtung erfolgt also "fliegend".

Es folgen zwei Verdrahtungsvarianten, die auch kombinierbar sind: einmal für die PWM-Messung und zweitens für die Messung über die UART (serielle Schnittstelle).

PWM-Messung (Pulse Width Modulation)

1280px-Pulse_wide_wave.svgMatthiasDD - Eigenes Werk, basierend auf: Square wave.svg - CC BY-SA 3.0

Der Messwert ist das Verhältnis von t1/T ausgelesen und dann auf den Wertebereich des CO2-Sensors hochgerechnet. Im Datenblatt kann man die entsprechenden Bezugwerte finden, so wird z. B. bei einer Messbereichswahl von 0-2000ppm der 0ppm-Wert mit einer Zeitdauer von 2ms (Millisekunden) angegeben und der Wert 2000ppm entspricht einer Pulsdauer von 1002ms. Die Gesamtpulsbreite T hat eine Dauer von 1004ms.

Der Anschluss an den Arduino ist sehr einfach und könnte so aussehen:

mhz19b_schaltung_pwm

So gestaltet sich die Verdrahtung mit dem Arduino bei der PWM-Messung:

mhz19b_steckbrett_pwm

UART-Messung von CO2 und Temperatur

Für die UART-Messung werden zwei beliebige Pins als RX/TX-Paar verwendet, im Beispiel Pin 2 und Pin 3.

Wichtig: Nicht die mit TXD und RXD gekennzeichneten Pins (0 und 1) verwenden! Sie gehören zur Hardware UART des Arduino und werden schon für die Programmierung über das USB-Kabel verwendet!

mhz19b_schaltplan_usart

Wichtig: Die UART-Verbindungen müssen "gekreuzt" werden: es müssen also der Arduino-RX-Pin (im Beispiel Pin 2) mit dem Sensor-TX-Pin verbunden werden und der Arduino-TX-Pin mit dem Sensor-RX-Pin.

mhz19b_steckbrett_usart

Die Software

Messung der CO2-Werte mit PWM

/**
 * CO2-Messung mit Sensor Typ MHZ19B
 * Messwerterfassung durch PWM-Signal
 */

// Der Sensor hängt an Pin 7

const int pwmpin = 7;

// Der eingestellte Messbereich (0-5000ppm)

const int range = 5000;

// Die setup()-Funktion

void setup() {

  // PWM-Pin auf Eingang setzen
  
  pinMode(pwmpin, INPUT);

  // Serielle Übertragung über USB initialisieren

  Serial.begin(9600);
  
}

// Die loop()-Funktion
 
void loop() {
  
  // Messung der PWM-Länge mittels einer eigenen Funktion
  
  int ppm_pwm = readCO2PWM();

  // Ausgabe der Werte über die serielle USB-Verbindung
  
  Serial.print("PPM PWM: ");
  Serial.println(ppm_pwm);

  // Messungen alle 3 Sekundn
  
  delay(3000);
  
}

// Die Messung der PWM-Länge erfolgt in einer eigenen
// Funktion readCO2PWM(), was die loop()-Schleife etwas "aufgeräumter"
// erscheinen lässt. Die Funktion gibt eine Ganzzahl zurück (int).

int readCO2PWM() {

  // Es werden die für die Umrechnung der Zeitdauer auf
  // die PPM-Werte benötigten Variablen definiert.
  // Da es sich bei th um große Werte handeln kann - die verwendete
  // Arduino-Funktion gibt Mikrosekunden zurück - wird diese Variable
  // als vorzeichenlose (unsigned) große Ganzzahl (long) definiert.
  
  unsigned long th;
  int ppm_pwm = 0;
  float pulsepercent;

  // Alles, was in der do ... while-Schleife steht, wird
  // solange ausgeführt, bis der Ausdruck nach while, hier
  // th == 0 als zutreffend (wahr) erkannt wird.
  // Da die Arduino-Funktion pulseIn() 0 zurückgibt, solange
  // sie am Messen ist, dient die Schleife dazu, auf den
  // Messwert zu warten.
  
  do {

    // pulseIn gibt die Dauer des am Pin (pwmpin) anliegenden
    // Signals in Mikrosekunden an. Die maximale Signallänge ist
    // 1004ms. Der Timeoutwert der pulseIn-Funktion muss also
    // mindestens 1004000µs betragen. Für ungünstige Fälle wird
    // sicherheitshalber ein größerer Wert von 2500000µs gewählt.
    // Die Ausgabe der pulseIn()-Funktion wird durch 1000 geteilt
    // und ergibt so für th die Signallänge in Millisekunden (ms).
    
    th = pulseIn(pwmpin, HIGH, 2500000) / 1000;

    // Pulslänge in Prozent (%)

    float pulsepercent = th / 1004.0;

    // PPM-Werte bei gegebenem Range
    
    ppm_pwm = range * pulsepercent;
    
  } while (th == 0);
  
  // Der gemessene Wert wird an die loop()-Funktion zurückgegeben,
  // wo er dann ausgegeben wird.
  
  return ppm_pwm; 
}

Folgendes ist neu:

 int ppm_pwm = readCO2PWM();

Neben den beiden Standard-Arduino-Funktionen setup() und loop() wird hier eine eigene neue Funktion verwendet: readCO2PWM(). Die Funktion gibt im Unterschied zu den anderen beiden Funktionen den Typ int zurück.

unsigned long th;

Der Typ "unsigned long" kann sehr große Ganzzahlen fassen: 0 - 4.294.967.295, wobei "unsigned" bedeutet, dass keine negativen Zahlen möglich sind.

 do {

   ...
    
  } while (Audruck);

Die do ... while-Schleife läuft, solange der Ausdruck gültig (wahr) ist. Da die Prüfung des Ausdrucks am Ende der Schleife erfolgt ("fußgesteuert") wird der Inhalt der Schleife auf alle Fälle mindestens einmal ausgeführt.

Messung der CO2-Werte mit UART

Die Messung über die UART erfolgt über standardisierte Befehle (commands) von 9 Byte Länge (s. Datenblatt) und ebenso langen Antworten (responses).

/**
 * CO2-Messung mit Sensor Typ MHZ19B
 * Messdatenerfassung über UART (serielle Schnittstelle)
 */

// Da die Hardware-UART des Arduino vom USB-Kabel belegt
// und über die Funktionen der Serial-Klasse schon 
// verwendet werden, braucht es die SoftwareSerial-Klasse
// (gehört zu den Arduino-Standardklassen) mit deren Hilfe
// beliebige Pins als RX/TX-Verbindungen verwendet werden
// können (mit Ausname von Pin 0 und Pin 1)

#include <SoftwareSerial.h>

// Hier wird eine Instanz der Klasse mit den Pins 2 (RX) und 3 (TX)
// initialisiert

SoftwareSerial co2Serial(2, 3); // define MH-Z19 RX TX

// In der setup()-Funktion werden sowohl die Hardware-
// als auch die Software UART initialisiert
 
void setup() {
  
  Serial.begin(9600);
  
  co2Serial.begin(9600);
  
}

// Die loop() Funktion liest mit Hilfe der eigenen
// Funktion readSensor() die
// Sensorwerte aus und schreibt sie über die serielle
// USB-Verbindung auf den angeschlossenen Computer.
 
void loop() {

  int ppm, temperature = 0;

  readSensor(&ppm, &temperature);

  Serial.print("PPM: ");
  Serial.print(ppm);
  Serial.print(" Temperature: ");
  Serial.println(temperature);
  
  delay(5000);
}

// Die Funktion liest die CO2-Werte über die UART des
// Sensors ein und schreibt die ermittelten Werte mit
// Hilfe der übergebenen Pointer in die Variablen ppm
// und temperature.

void readSensor(int *ppm, int *temperature){

  // Die Befehlskette zum Einlesen des PPM-Wertes laut Datenblatt
  
  byte cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};

  // Speicherplatzreservierung von 9 Byte für die Antwort des Sensors.
  // Alles Befehle und Antworten des Sensors haben eine Länge von
  // 9 Byte, wobei das letzte Byte eine Prüfsumme zur Kontrolle
  // der Übermittlung darstellt.
  
  byte response[9]; 

  // Befehl zum Auslesen schreiben
  
  co2Serial.write(cmd, 9);

  // Zuerst den Eingangsbuffer löschen (mit 0 füllen) und
  // danach in einer while-Schleife darauf warten, bis 
  // die Funktion co2Serial.available() einen Wert ungleich 0
  // zurückgibt.
  
  memset(response, 0, 9);
  while (co2Serial.available() == 0) {
    delay(1000);
  }

  // Die Antwort wird in den Speicher eingelesen.
  
  co2Serial.readBytes(response, 9);

  // Die Prüfsumme mit Hilfe einer eigenen Funk-
  // tion errechnen, um zu klären, ob die 
  // Übertragung fehlerfrei abgelaufen ist.
  
  byte check = getCheckSum(response);
  
  if (response[8] != check) {
    Serial.println("Fehler in der Übertragung!");
    return;
  }
 
  // PPM-Wert errechnen, sie finden sind
  // im 3. und 4. Byte der Antwort im Format
  // HIGH-Byte und LOW-Byte und müssen über die
  // folgende Formel zu einem Word (int) verkettet
  // werden.
  
  *ppm = 256 * (int)response[2] + response[3];

  // Temperaturwert wird als 5. Byte der Response
  // übermittelt (im Datenblatt nicht angegeben).
  // Damit auch negative Temperaturen übertragen 
  // werden können, wurde der Wert 40 dazuaddiert,
  // der jetzt wieder entfernt werden muss.
  
  *temperature = response[4] - 40;

}

// Die Funktion errechnet eine Prüfsumme über die
// durch einen Zeiger übergebene Befehls- oder
// Antwortkette. Der Algorithmus zur 
// Prüfsummenberechnung findet sich im
// Datenblatt.

byte getCheckSum(byte *packet) {
  byte i;
  byte checksum = 0;
  for (i = 1; i < 8; i++) {
    checksum += packet[i];
  }
  checksum = 0xff - checksum;
  checksum += 1;
  return checksum;
}

 Was ist neu:

void readSensor(int *ppm, int *temperature) { ... }

Die Definition der eigenen Funktion readSensor() ist wie folgt zu verstehen: die Funktion gibt keinen Wert zurück (void) und erhält zwei Zeiger (Pointer) auf int-Werte, durch das Sternchensymbol angedeutet. Der Grund für diese Vorgangsweise ist die Tatsache, dass Funktionen immer nur einen einzigen Wert zurückgeben können, wir hier aber zwei Werte haben wollen. Deswegen erhält die Funktion die Adressen von zwei Variablen, in die sie die Werte schreiben kann. Diese Werte können dann von der aufrufenden Funktion ausgelesen und weiterverwendet werden. Beim Aufruf der readSensor()-Funktion müssen die Adressen der beiden Variablen angegeben werden, was durch das vorangestellt "&" gekennzeichnet wird:

 readSensor(&ppm, &temperature);

Beim Beschreiben der Variablen ist dann ebenfalls die Pointer-Schreibweise zu verwenden, damit nicht die Adressen überschrieben werden:

*temperature = response[4] - 40;

Die Funktion

 memset(response, 0, 9);

ist fest in Arduino eingebaut. Die Funktionen

co2Serial.available(), co2Serial.write() und co2Serial.readBytes()

sind in der eingebunden SoftwareSerial-Klasse verfügbar (bzw. deren Instanz co2Serial).

 

Weiterführendes

CO2-Ampel selber bauen (Robert Helling)


Screenshots wurden mit Fritzing erstellt, falls nicht anders angegeben (https://fritzing.org)

Angerer Harald - Montag, 9. Dezember 2019 (Zuletzt geändert: Dienstag, 15. März 2022)

Kategorien: Arduino Tutorials

9 Kommentare

1 Kommentar von Fabian

04.04.2020 10:13

Hallo Harald,

Danke für den tollen Blog beitrag.

Befindet sich der Oben genannte Code schon in irgendeinem public Git Repository veröffentlicht? Wegen der Lizense, oder würdest du sagen das alles was du hier postes public domain ist?

Denn ich würde gerne meinen angepassten Code der auf deinem aufbaut, mit deinen Kommentaren in meinem Github Repo veröffentlichen. Bezüglich meiner Lizense hab ich mich noch nicht Festgelegt wahrscheinlich wird es MIT.

Danke im vorraus


Viele Grüße
Fabian

2 Kommentar von Angerer Harald

07.09.2020 12:36

Sorry, habe den Kommentar erst jetzt zur Kenntnis genommen - es gibt keine Repository für den Code, du kannst alle Inhalte des Arduino-Tutorials gern, sofern nicht fremdes Copyright angegeben ist, übernehmen, modifizieren und vor allem verbessern :) - schön wäre ein Link auf die Seite.

3 Kommentar von Felix

17.10.2020 20:25

Im PWM-Codeabschnitt sollte der timeout in der PulseIn-Funktion angepasst werden.

 th = pulseIn(pwmpin, HIGH,2500000)/1000;
Damit erfolgt ein zuverlässigeres Messen, da selbst im ungünstigsten Fall eine gesamte Periode gemessen wird.
Viel Grüße.

4 Kommentar von Angerer Harald

23.10.2020 14:42

Danke Felix! Habe den Code entsprechend editiert.

5 Kommentar von Guest

01.11.2020 14:58

Hallo Harald,

in deiner Fritzingzeichnung zur PWM-Messung hängt der Sensor an Pin 7. Im Sketch steht Pin 6, was m. E. richtig ist, da Pin 7 nicht PWM-fähig ist.

Danke für den Hinweis - stimmt natürlich, da hab ich die Pins vertauscht. Allerdings funktioniert auch Pin7, es geht nicht um PWM-Erzeugung, sondern um die Signallängenmessung via pulseIn(). Die funtioniert auch am Pin7. Da ich die Originalfritzingzeichnung nicht mehr habe, passe ich den Code an.

6 Kommentar von Andy

20.11.2020 14:25

 

Hallo Harald,

vielen Dank für die Vorstellung deines Projektes! 

Habe es so umgesetzt, wenn auch mit anderem Display und einem Servo der zusätzlich angesteuert wird. 

 

1 -1 1/2 Fragen: 

1. Über die UART-Lösung bekomme ich immer mal wieder eine Fehlermeldung, dass die Übertragung nicht funktioniert hat. Wie kann das vermieden werden? Kann das am Servo liegen? z.B. Schwankungen in der Anfangsspannung oder ähnliches?

 2. Manchmal scheint die Messung quasi das Komma zu verrücken. Aus einer 3-4stelligen Zahl wird dann eine 4-5stellige. Die Bedingungen (z.b. zwischen 600-700) reagieren trotzdem bei 6000-7000. Woran kann das liegen?

 

Beste Grüße,

Andy

Hallo Andy, zu Frage 1 kann ich nur raten: Signalwege und Servoleitungen können die Übertragung stören - kannst du ggf. probieren, ob die Fehlermeldungen auch ohne Servo auftauchen?

Zu Frage 2 bin ich ratlos - ist mir nie untergekommen und sollte bei UART-Messung auch nicht vorkommen. Hast du einen zweiten CO2-Sensor, damit man eine Fehlerquelle im Sensor ausschließen kann?

Wenn beide Fehler nicht allzuoft vorkommen, dann würde ich sie im Sketch einfach ausblenden und die entsprechenden Messungen verwerfen - die Messfrequenz braucht bei diesen Werten ja nicht sehr hoch sein.

 

7 Kommentar von Sergii

26.12.2020 23:13

Hallo Harald,

ich habe 2 MH-Z19B separat gekauft, die sehen fast gleich aus (1st mit dem Buckel oben, 2nd ohne), liefern aber zwar unterschiedlichen werten raus, sowohl PWM als auch UART. Wenn die beide draußen nimmt, liegen die Botschaften nah (400-405ppm), drin ist die Unterschied zwar gross (etwa 150-200 ppm).

Hast du etwas ähnliches erlebt? Wie kann man sagen welche Messung falsch ist? Ich verstehen, dass Kalibrierung (oder Prüfung) mithilfe eines zertifiziertes Gerät bestimmt helfen wird, gibt es von deine Erfahrung aber andere Lösung?   

       

8 Kommentar von udo

04.01.2021 19:00

@Sergii

Es gibt zwei Typen von diesen Sensoren und zwar mit unterschiedlichen Messbereich. Die einen gehen von 0-2000, die anderen von 0-5000. Vielleicht ist das die Ursache?

Um zu ermitteln, welchen Typ du hast, hab ich folgendes Skript gefunden:

/**
 * CO2-Messung mit Sensor Typ MHZ19B
 * Der Sketch ermittelt den eingestellten Messbereich
 * von 0-2000ppm oder 0-5000ppm
 * Es ist eine UART-Verdrahtung notwendig
 */

#include

SoftwareSerial co2Serial(2, 3); // define MH-Z19 RX TX
// Der Befehl ist im Datenblatt nicht dokumentiert, hier gefunden:
// https://revspace.nl/MH-Z19B
byte cmd_getrange[9] = {0xFF,0x01,0x9B,0x00,0x00,0x00,0x00,0x00,0x64};
 
void setup() {
  Serial.begin(115200);
  co2Serial.begin(9600);
  delay(1000);
  readRange();
}
 
void loop() {
  // do nothing here.
}

void readRange() {
  byte response[9]; // for answer
  co2Serial.write(cmd_getrange, 9); //request range
  memset(response, 0, 9);
  int i = 0;
  while (co2Serial.available() == 0) {
    i++;
  }
  if (co2Serial.available() > 0) {
      co2Serial.readBytes(response, 9);
  }
  unsigned int range = response[4] * 256 + response[5];
  Serial.print("Range: ");
  Serial.println(range);
}

 

9 Kommentar von Guest

10.02.2022 11:10

Moin,

danke für die Anleitung! Ich benutze den CO2 Sensor, um in einem Laborversuch Bakterienatmung über einen längeren Zeitraum zu verfolgen.

Allerdings scheint sich der Sensor immer nach etwa 21 h neu zu kalibrieren?

Kann man da im Skript etwas verändern, dass das nicht passiert?

(nutze das "Messung der CO2-Werte mit PWM" Skript)

LG

Luise

Hallo Luise, leider konnte ich dazu nichts im Datenblatt finden. Das Phänomen selber ist mir nicht aufgefallen.

Kommentar von Guest

Hallo Harald,

in deiner Fritzingzeichnung zur PWM-Messung hängt der Sensor an Pin 7. Im Sketch steht Pin 6, was m. E. richtig ist, da Pin 7 nicht PWM-fähig ist.

Danke für den Hinweis - stimmt natürlich, da hab ich die Pins vertauscht. Allerdings funktioniert auch Pin7, es geht nicht um PWM-Erzeugung, sondern um die Signallängenmessung via pulseIn(). Die funtioniert auch am Pin7. Da ich die Originalfritzingzeichnung nicht mehr habe, passe ich den Code an.

Der Kommentar wird endgültig gelöscht. Sind Sie sicher?