11. Arduino-Projekt: CO2-Messung mit dem Sensor MHZ19B
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.
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.
NDIP-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.
MHZ19B-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)
MatthiasDD - 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:
So gestaltet sich die Verdrahtung mit dem Arduino bei der PWM-Messung:
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!
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.
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)