SCD4x CO₂-Sensoren

Über den Beitrag

In diesem Beitrag teile ich meine Erfahrungen mit den SCD40 und SCD41 Sensormodulen (kurz: SCD4x) zur Messung des Kohlendioxidgehaltes der Luft. Dabei verwende ich die Bibliothek arduino_i2c_scd4x von Sensirion.

In einem früheren Beitrag hatte ich über die CO₂-Sensoren MH-Z14 und MH-Z19 geschrieben, welche auf einem ähnlichen spektroskopischen Messprinzip basieren. Mit deren Leistung war ich sehr zufrieden. Weniger begeistert war ich von den CO₂-Sensormodulen CCS811 und SGP30, die ich hier behandelt habe. Insofern war ich sehr gespannt, wie sich die SCD4x Module schlagen würden (Spoiler: ausgezeichnet!).

Folgendes kommt in diesem Beitrag auf euch zu:

SCD4x Versionen

Verschiedene SCD4x Module
Verschiedene SCD4x Module

Geht ihr auf die Suche nach SCD4x Modulen, dann werdet ihr im Wesentlichen auf die oben abgebildeten Bauformen treffen. Sie alle besitzen Pull-up-Widerstände für die I2C-Leitungen und Kondensatoren zur Spannungsstabilisierung.

Die großen, roten Module besitzen zusätzlich Qwiic-Buchsen, mit denen ihr die Module sehr bequem mit Boards oder anderen I2C-Modulen verbinden könnt, sofern diese auch über dieses Feature verfügen. Außerdem haben diese Varianten eine Betriebs-LED. Die LED und die Pull-ups lassen sich über Lötpads auf der Rückseite trennen oder verbinden.

Ich habe noch eine weitere Variante gesehen (hier nicht abgebildet), die zusätzlich über einen 3.3 Volt Spannungsregler und einen 3.3 Volt Ausgang verfügt. Zusätzlich gibt es bei dieser Variante auch die Möglichkeit, die I2C-Leitungen auf 3.3 oder 5 Volt einzustellen. 

Die Bauform legt nicht fest, ob ein SCD40- oder ein SCD41-Sensor verbaut wurde. Passt also auf, was ihr bestellt. 

Die wesentlichen Unterschiede zwischen dem SCD40 und dem SCD41 bestehen in ihren (optimalen) Messbereichen und ihrer Genauigkeit. Außerdem beherrscht nur der SCD41 den Single Shot Modus.

Große Preisspanne

Für SCD41 Module habe ich Preise von ca. 14 bis 100 € (!) gesehen. Beim SCD40 lagen die Preise zwischen 11 und 70 Euro. Lohnen sich Markenmodule? Aus meiner Sicht in diesem Fall nicht. Die No-Name-Module waren nicht schlechter als das Sparkfun Markenprodukt. Aber ich kann euch natürlich nicht versprechen, dass ihr nicht doch irgendwo Billigschrott untergejubelt bekommt. 

Das Messprinzip der SCD4x Sensoren

Die SCD4x Sensoren basieren auf dem photoakustischen Messprinzip. Moleküle wie das CO₂ absorbieren Licht bestimmter Wellenlängen. Wenn man nun Licht durch ein Gasgemisch schickt, das nur von einer Sorte Molekülen absorbiert wird, dann werden diese Moleküle selektiv angeregt (erwärmt). Dadurch dehnt sich das Gasgemisch geringfügig aus. Schaltet man das Licht in schneller Folge ein und aus, wird sich das Gasgemisch entsprechend periodisch ausdehnen und zusammenziehen. Das erzeugt eine Druckwelle, sprich Schall, der über einen akustischen Sensor (Mikrofon) detektiert wird. Die Intensität des akustischen Signals ist proportional zur Konzentration des absorbierenden Gases im Gasgemisch. 

Als Licht kommt für die CO₂-Bestimmung das für uns nicht sichtbare Infrarotlicht zum Einsatz. Die Fähigkeit des CO₂, Infrarotlicht zu absorbieren, kennt ihr vom Treibhauseffekt.

Da die Messwerte auch durch die relative Luftfeuchte und die Temperatur beeinflusst werden, besitzen die SCD4x Module entsprechende Sensoren. Ein integrierter Mikrocontroller korrigiert die Rohdaten. Wie das genau passiert, ist das Firmengeheimnis des Herstellers Sensirion. Auch der Luftdruck beeinflusst das Messergebnis und kann korrigiert werden. Einen integrierten Sensor gibt es dafür allerdings nicht.

Dass dieses Messprinzip in derart kleinen Sensoren so gut funktioniert, finde ich schon ziemlich beeindruckend. 

Da ich neugierig war, habe ich einen Sensor geopfert und aufgeschnitten:

SCD41 Sensor, geöffnet

Leider kann ich nicht mit Bestimmtheit sagen, welches Teil wofür verantwortlich ist. Trotzdem ist der Einblick vielleicht mal ganz interessant.

Technische Daten

Der SCD40 und der SCD41 können CO₂-Konzentrationen von 0 bis zu 40000 ppm ausgeben. Eine spezifizierte Genauigkeit gibt es aber nur für Messwerte zwischen 400 und 2000 ppm (SCD40) beziehungsweise zwischen 400 und 5000 ppm (SCD41).

Ich habe hier einmal ein paar technische Daten zusammengetragen: 

Ausgewählte technische Daten der SCD4x Sensoren

Weitere wichtige Features sind:

  • Automatische oder manuelle Kalibrierung mit Wertevorgabe
  • Temperatur-, Druck-/Höhen- und Feuchtekompensation
  • Funktions-Selbsttest
  • EEPROM zur permanenten Speicherung von Einstellungen

Ein Datenblatt für den SCD40 und SCD41 findet ihr hier.

Anschluss der SCD4x Module an den Mikrocontroller

Die Module besitzen zwei Pins für die Stromversorgung und zwei für die I2C-Verbindung. Und da die SCD4x-Module – jedenfalls die von mir getesteten – über Pull-up-Widerstände verfügen, braucht ihr lediglich vier Kabel.

Es ist zu beachten, dass die Strombedarfsspitzen relativ hoch sind und die Fähigkeiten eines Mikrocontroller-Boards unter Umständen überschreiten können. Bei vielen Arduino-Boards liegt der maximale Strom des 3.3V-Ausgangs bei 50 mA. Gegebenenfalls solltet ihr also über eine separate Stromquelle nachdenken. 

Aber auch mit externen, potenten Stromquellen habe ich bei 3.3 Volt nur bedingt gute Erfahrungen gemacht, selbst mit zusätzlichen Kondensatoren. Mit 4-5 Volt liefen die SCD4x Module bei mir zuverlässiger. Auf jeden Fall solltet ihr für eine stabile Spannung sorgen. 

Hier eine Beispielschaltung mit einem klassischen Arduino Nano:

SCD4x - Anschluss an einen klassischen Arduino Nano
SCD4x Modul an einem Arduino Nano

Bibliotheken für die SCD4x Module

Wie schon erwähnt, verwende ich für die Beispiele in diesem Beitrag die Bibliothek arduino_i2c_scd4x von Sensirion, die ihr über den Bibliotheksmanager der Arduino IDE installieren könnt. Die Bibliothek ist noch recht jung (Stand heute: Release 1.0.0) und hat noch an ein paar weniger relevanten Stellen Verbesserungspotenzial. Aber sie ist von den Bibliotheken, die ich mir angeschaut habe, die vollständigste. Sie könnte auch gerne etwas mehr Beispielsketche enthalten – aber dafür habt ihr ja diesen Beitrag.

Eine ähnlich gute Alternative ist die Bibliothek SparkFun_SCD4x_Arduino_Library. Sie hat viele Beispiele, allerdings fehlen ihr ein paar Funktionen wie etwa die Einstellung des Basiswertes für die automatische Selbstkalibrierung.

Dann habe ich mir noch die Bibliothek DFRobot_SCD4X angeschaut, die allerdings hinsichtlich ihres Funktionsumfangs deutlich hinter den anderen beiden Bibliotheken zurückblieb (zu beachten: Stand heute).

Wer mehr auf C steht, der mag mit der mag mit embedded-i2c-scd4x von Sensirion glücklich werden. 

Getting started – einfache Messungen

Genug der Theorie, nun zur Praxis. Und die möchte ich euch vor allem anhand praktischer Beispiele vermitteln. Wir beginnen mit einem einfachen Sketch, der euch Messwerte für die CO₂-Konzentration, die Temperatur und die relative Luftfeuchte liefert. Dabei kommen die Standardeinstellungen zum Einsatz.

#include <SensirionI2cScd4x.h>
#include <Wire.h>

SensirionI2cScd4x sensor;

static char errorMessage[64];
static int16_t error;

void setup() {
    Serial.begin(115200);
    Wire.begin();
    sensor.begin(Wire, SCD41_I2C_ADDR_62); // alt.: SCD40_I2C_ADDR_62
    uint64_t serialNumber = 0;
    delay(30);

    error = sensor.wakeUp();
    if (error) {
        Serial.print("Error trying to execute wakeUp(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.stopPeriodicMeasurement();
    if (error) {
        Serial.print("Error trying to execute stopPeriodicMeasurement(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.reinit();
    if (error) {
        Serial.print("Error trying to execute reinit(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.getSerialNumber(serialNumber);
    if (error) {
        Serial.print("Error trying to execute getSerialNumber(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }
    Serial.print("SCD4x connected, serial number: ");
    PrintUint64(serialNumber);
    Serial.println();

    error = sensor.startPeriodicMeasurement();
    if (error) {
        Serial.print("Error trying to execute startPeriodicMeasurement(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }
}

void loop() {
    bool dataReady = false;
    uint16_t co2Concentration = 0;
    float temperature = 0.0;
    float relativeHumidity = 0.0;

    error = sensor.getDataReadyStatus(dataReady);
    if (error) {
        Serial.print("Error trying to execute getDataReadyStatus(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }

    if (dataReady) {
        sensor.readMeasurement(co2Concentration, temperature, relativeHumidity);
  
        Serial.println();
        Serial.print("CO2[ppm]: ");
        Serial.print(co2Concentration);

        Serial.print("\tTemperature[°C]: ");
        Serial.print(temperature, 1);

        Serial.print("\tHumidity[%RH]: ");
        Serial.print(relativeHumidity, 1);

        Serial.println();
    }
    else
        Serial.print(".");

    delay(500);
}

void PrintUint64(uint64_t& value) {
    Serial.print("0x");
    Serial.print((uint32_t)(value >> 32), HEX);
    Serial.print((uint32_t)(value & 0xFFFFFFFF), HEX);
}

 

Hier die Ausgabe:

SCD4x - Ausgabe von scd4x_basic_reading.ino
Ausgabe von scd4x_basic_reading.ino

Erläuterungen zum Sketch

Error Codes

Nahezu alle Funktionen der Bibliothek geben einen Error-Code zurück. Das ist sehr hilfreich, wenn es Probleme geben sollte. Da die Get-Funktionen noch zusätzliche Daten liefern sollen, Funktionen aber nur einen Wert zurückgeben können, übergebt ihr die zugehörigen Variablen als Referenz.

Die Error-Meldungen enthalten viele Zeichenketten. Wenn es dadurch im RAM eng wird, dann verwendet das F-Makro.

Initialisierungssequenz

Um den SCD4x in einen definierten Zustand zu bringen, verwendet der Sketch eine Initialisierungssequenz, die ihr auch in den meisten anderen Beispielen wiederfindet:

  1. Einbinden der Bibliotheken.
  2. Übergeben des TwoWire Objektes und der I2C-Adresse an begin().
  3. 30 Millisekunden warten, bis der SCD4x „hochgefahren“ ist.
  4. SCD4x mit wakeUp() wecken.
  5. Periodische Messungen mit stopPeriodicMeasurement() ausschalten, da der SCD4x in diesem Modus für die meisten Anweisungen „taub“ ist.
  6. Voreinstellungen mit reinit() aus dem EEPROM des SCD4x lesen. 
  7. Seriennummer mit getSerialNumber() auslesen. Das ist die beste Methode, um die Verfügbarkeit des SCD4x zu prüfen.
  8. An dieser Stelle Einstellungen vornehmen – in diesem Beispiel tun wir das noch nicht.
  9. Starten von Messungen – hier mit startPeriodicMeasurement() für den periodischen Modus.

Die Übergabe der I2C-Adresse als SCD4x_I2C_ADDR_62 mit x = 0 oder 1 erweckt den Eindruck, man würde damit die SCD4x-Variante übermitteln. Dahinter steckt aber schlicht 0x62.

Die Seriennummer ist so groß, dass dafür 64-Bit-Integer-Variable benötigt wird. Um diese mit Serial.print() ausgeben zu können, muss sie in zwei 32-Bit-Integerwerte zerlegt werden. Das macht PrintUint64().

Auslesen der Messwerte

Die Messwerte werden immer als Dreierpack aus CO₂-Konzentration, Temperatur und Luftfeuchte bereitgestellt. Ob ein neuer Messwertesatz bereitsteht, prüft ihr mit getDataReadyStatus(). Wenn das der Fall ist, holt ihr die Daten mit readMeasurement() ab. Eine Messung nimmt ca. 5 Sekunden in Anspruch.

Messmodi

Periodische Messungen

Periodische Messungen habt ihr gerade kennengelernt. Nur noch ein paar Anmerkungen dazu.

Sofern ihr keinen Batteriebetrieb plant, würde ich diese Option wählen. Der durchschnittliche Stromverbrauch beträgt 15 Milliampere bei 3.3 Volt und 11 Milliampere bei 5 Volt. 

Während periodischer Messungen reagiert der Sensor nur auf die folgenden Funktionen:

  • readMeasurement(): kennt ihr schon.
  • getDataReadyStatus(): kennt ihr schon.
  • stopPeriodicMeasurement(): auch diese Funktion kennt ihr schon. 
  • setAmbientPressure(): dazu kommen wir noch.

Low Power Messungen

Mittels Low-Power Messungen könnt ihr den durchschnittlichen Stromverbrauch auf 3.2 Milliampere bei 3.3 Volt und 2.8 Milliampere bei 5 Volt senken. In diesem Modus wird alle 30 Sekunden eine Messung durchgeführt. Zwischen den Messungen sinkt der Stromverbrauch bei 5 Volt auf ca. 1 Milliampere mit Betriebs-LED und auf ca. 0.6 Milliampere ohne Betriebs-LED. Der hohe Strombedarf während der Messung treibt den Durchschnitt nach oben. Schön wäre, wenn die SCD4x Module Interruptpins hätten, die das Vorhandensein neuer Messwerte signalisieren – haben sie aber nicht.

Der Sketch unterscheidet sich vom vorherigen lediglich dadurch, dass anstelle der periodischen Messungen die Low-Power Messungen mit startLowPowerPeriodicMeasurement() gestartet werden.

#include <SensirionI2cScd4x.h>
#include <Wire.h>

SensirionI2cScd4x sensor;

static char errorMessage[64];
static int16_t error;

void setup() {
    Serial.begin(115200);
    Wire.begin();
    sensor.begin(Wire, SCD41_I2C_ADDR_62);
    uint64_t serialNumber = 0;
    delay(30);

    error = sensor.wakeUp();
    if (error) {
        Serial.print("Error trying to execute wakeUp(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.stopPeriodicMeasurement();
    if (error) {
        Serial.print("Error trying to execute stopPeriodicMeasurement(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.reinit();
    if (error) {
        Serial.print("Error trying to execute reinit(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

     error = sensor.getSerialNumber(serialNumber);
    if (error) {
        Serial.print("Error trying to execute getSerialNumber(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }
    Serial.print("SCD4x connected, serial number: ");
    PrintUint64(serialNumber);
    Serial.println();

    error = sensor.startLowPowerPeriodicMeasurement();
    if (error) {
        Serial.print("Error trying to execute startPeriodicMeasurement(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }
    else
        Serial.println("Low power mode enabled!");

}

void loop() {
    bool dataReady = false;
    uint16_t co2Concentration = 0;
    float temperature = 0.0;
    float relativeHumidity = 0.0;

    error = sensor.getDataReadyStatus(dataReady);
    if (error) {
        Serial.print("Error trying to execute getDataReadyStatus(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }

    if (dataReady) {
        sensor.readMeasurement(co2Concentration, temperature, relativeHumidity);
  
        Serial.println();
        Serial.print("CO2[ppm]: ");
        Serial.print(co2Concentration);

        Serial.print("\tTemperature[°C]: ");
        Serial.print(temperature, 1);

        Serial.print("\tHumidity[%RH]: ");
        Serial.print(relativeHumidity, 1);

        Serial.println();
    }
    else
        Serial.print(".");

    delay(500);
}

void PrintUint64(uint64_t& value) {
    Serial.print("0x");
    Serial.print((uint32_t)(value >> 32), HEX);
    Serial.print((uint32_t)(value & 0xFFFFFFFF), HEX);
}

 

Hier die Ausgabe:

SCD4x - Ausgabe von scd4x_low_power_periodic.ino
Ausgabe von scd4x_low_power_periodic.ino

Single Shot Messungen (nur SCD41)

Der dritte Messmodus „Single Shot“ steht nur für den SCD41 zur Verfügung. Hier initiiert ihr jede Messung explizit.

#include <SensirionI2cScd4x.h>
#include <Wire.h>
#define SINGLE_SHOT_PAUSE 300000 // please adjust, but consider ASC settings
//#define USE_POWER_DOWN

SensirionI2cScd4x sensor;

static char errorMessage[64];
static int16_t error;

void setup() {
    Serial.begin(115200);
    Wire.begin();
    uint64_t serialNumber = 0;
    sensor.begin(Wire, SCD41_I2C_ADDR_62);
    delay(30);

    error = sensor.wakeUp();
    if (error) {
        Serial.print("Error trying to execute wakeUp(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.stopPeriodicMeasurement();
    if (error) {
        Serial.print("Error trying to execute stopPeriodicMeasurement(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.reinit();
    if (error) {
        Serial.print("Error trying to execute reinit(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.getSerialNumber(serialNumber);
    if (error) {
        Serial.print("Error trying to execute getSerialNumber(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }
    Serial.print("SCD4x connected, serial number: ");
    PrintUint64(serialNumber);
    Serial.println();

    error = sensor.setTemperatureOffset(2.0); 
    if (error) {
        Serial.print(F("Error trying to execute setTemperatureOffset(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

#ifdef USE_POWER_DOWN 
    error = sensor.persistSettings(); 
    if (error) {
        Serial.print(F("Error trying to execute persistSettings(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
#endif
}

void loop() {
    uint16_t co2Concentration = 0;
    float temperature = 0.0;
    float relativeHumidity = 0.0;

    Serial.println("Starting measurement");
    
#ifdef USE_POWER_DOWN 
    error = sensor.measureSingleShot();
    if (error) {
        Serial.print("Error trying to execute measureSingleShot(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
#endif
    
    error = sensor.measureAndReadSingleShot(co2Concentration, temperature, relativeHumidity);
    if (error) {
        Serial.print("Error trying to execute measureAndReadSingleShot(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    Serial.print("CO2[ppm]: ");
    Serial.print(co2Concentration);

    Serial.print("\tTemperature[°C]: ");
    Serial.print(temperature, 1);

    Serial.print("\tHumidity[%RH]: ");
    Serial.println(relativeHumidity, 1);

    error = sensor.measureSingleShotRhtOnly();
    if (error) {
        Serial.print("Error trying to execute measureSingleShotRhtOnly(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
    delay(100);
    
    error = sensor.readMeasurement(co2Concentration, temperature, relativeHumidity);
    if (error) {
        Serial.print("Error trying to execute readMeasurement(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
    
    Serial.print("RH/T only:  ");

    Serial.print("\tTemperature[°C]: ");
    Serial.print(temperature, 1);

    Serial.print("\tHumidity[%RH]: ");
    Serial.println(relativeHumidity, 1);

    Serial.println("-----");

#ifdef USE_POWER_DOWN
    error = sensor.powerDown();
    if (error) {
        Serial.print("Error trying to execute powerDown(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
#endif

    delay(SINGLE_SHOT_PAUSE);

#ifdef USE_POWER_DOWN
    error = sensor.wakeUp();
    if (error) {
        Serial.print("Error trying to execute wakeUp(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
#endif }

void PrintUint64(uint64_t& value) {
    Serial.print("0x");
    Serial.print((uint32_t)(value >> 32), HEX);
    Serial.print((uint32_t)(value & 0xFFFFFFFF), HEX);
}

 

Hier die Ausgabe:

Ausgabe von scd4x_single_shot.ino

Erläuterungen zum Code

Idle vs. Power Down

Zwischen den Messungen geht der SCD4x in den energiesparenden Idle-Zustand. Alternativ könnt ihr den SCD4x mit powerDown() ganz ausschalten. Wenn ihr das tut, dann müsst ihr ihn mit wakeUp() wieder anschalten, bevor ihr eine neue Messung durchführt. Im obigen Sketch könnt ihr durch Ein- oder Entkommentieren von #define USE_POWER_DOWN zwischen den Optionen wechseln. 

Die Funktion setTemperatureOffset(2.0) stellt den Temperaturoffset auf 2 Grad ein. Einstellungen werden grundsätzlich im RAM des SCD4x gespeichert und sind verloren, wenn ihr den SCD4x mit powerDown() außer Betrieb setzt. Entweder nehmt ihr die Einstellungen nach dem nächsten wakeUp() erneut vor, oder speichert die Einstellung dauerhaft mit persistSettings() im EEPROM des SCD4x. Im Beispiel habe ich mich für die letztere Option entschieden.

Nach einem powerDown() sollte der erste Messwert verworfen werden. Dazu verwendet ihr die Funktion measureSingleShot(). Sie initiiert eine Messung und blockiert das Programm für 5 Sekunden, bis die Messung abgeschlossen ist. 

Wichtig: Wenn ihr mit dem Power-Down-Zustand arbeitet, dann ist keine automatische Selbstkalibrierung (ASC) möglich. Verwendet ihr ASC und kein Power-Down, und weicht eure Single-Shot-Periode von 5 Minuten ab, dann müsst ihr die Perioden für die Erst- und die Standardkalibrierung ändern. Darauf komme ich wieder zurück. 

Reguläre Single Shot Messung

Mit measureAndReadSingleShot() initiiert ihr eine Messung und lest die Ergebnisse aus. Die Funktion blockiert das Programm für (ggf.: weitere) 5 Sekunden. Danach geht der SCD4x in den Idle-Zustand oder ihr schaltet ihn mit powerDown() aus.

RH / T Messungen

Im Single Shot Modus fragt ihr mit measureSingleShotRhtOnly() nur die Temperatur und die relative Luftfeuchte ab. Das nimmt ca. 50 Millisekunden in Anspruch. Die Abfrage im Beispiel, direkt nach der vollständigen Messung, macht nicht viel Sinn und dient nur der Anschauung. 

Besonderheiten der Single Shot Messungen

Während der Pausen geht der Strombedarf im Idle-Zustand auf ca. 0.6 Milliampere (@ 5V / mit LED) bzw. 0.18 Milliampere (@ 5V / ohne LED) zurück. Im Power-Down-Zustand habe ich 2.5 Mikroampere (@ 5V / ohne LED) gemessen. 

Nehmt ihr alle 5 Minuten eine Messung vor, kommt ihr laut Datenblatt auf einen durchschnittlichen Strombedarf von 0.36 Milliampere (ohne LED). Damit ist dann auch Batteriebetrieb mit akzeptablen Laufzeiten möglich. Im Anhang findet ihr zwei Beispiele, wie ihr die Single Shot Messungen mit dem Tiefschlafmodus von AVR- bzw. ESP32-basierten Boards kombinieren könnt.

Die Schwankung der Messwerte ist im Single Shot Modus höher als im Low-Power oder im periodischen Betrieb. Da kann es mal Ausreißer von +/- 30 oder gar 40 ppm geben. 

Kalibrierung der SCD4x Module

Automatische Selbstkalibrierung (ASC)

Die ASC hat zwei Phasen, nämlich die Erstkalibrierung und die folgenden Standardkalibrierungen. Die Erstkalibrierung erstreckt sich über die ersten 44 Betriebsstunden. Danach greift die Standardkalibrierung mit einer Periode von 156 Stunden. Im Prinzip nimmt der SCD4x den tiefsten Messwert innerhalb der Periode und nimmt an, dass dieser eine CO₂-Konzentration von 400 ppm repräsentiert. Das heißt allerdings, dass ihr zwischendurch gut lüften müsst.

Der durchschnittliche Wert von 400 ppm ist leider schon Geschichte. Aktuell liegen wir eher zwischen 420 und 430 ppm (schaut hier). Es gibt zudem jahreszeitliche und tageszeitliche Schwankungen. Außerdem macht es einen Unterschied, ob ihr mitten im Wald oder in der Großstadt wohnt.

Anpassung der ASC

Den ASC-Basiswert von 400 ppm ändert ihr mit der Funktion setAutomaticSelfCalibrationTarget(ascTarget). Die Abfrage des Wertes erfolgt mit getAutomaticSelfCalibrationTarget()

Die Periode für die Erst- und die Standardkalibrierung ändert ihr mit setAutomaticSelfCalibrationInitialPeriod(ascInitialPeriod) beziehungsweise  setAutomaticSelfCalibrationStandardPeriod(ascStandardPeriod). Die zu übergebenden Parameter sind die Stunden. Dabei müssen die Werte jeweils ein ganzes Vielfaches von 4 sein. Für die Abfrage stehen entsprechende Get-Funktionen zur Verfügung.

Anpassung der ASC im Single Shot Modus

Die ASC-Perioden für die Erst- und die Standardkalibrierungen sind im Single-Shot Modus auf 48 bzw. 168 Stunden festgelegt. Das gilt aber nur, wenn die Periode für die Single-Shot-Messungen 5 Minuten beträgt. Wenn ihr die Single-Shot-Periode ändert und ihr die ASC-Perioden beibehalten wollt, dann müsst ihr die ASC-Parameter entsprechend skalieren: 

    \[ \text{ascInitialParam}_{\text{mod}} = \frac{48\cdot5}{\text{SingleShotPeriodInMinutes}} \]

Beziehungsweise:

    \[ \text{ascStandardParam}_{\text{mod}} = \frac{168\cdot5}{\text{SingleShotPeriodInMinutes}} \]

Wenn Ihr beispielsweise alle 3 Minuten eine Single-Shot-Messung vornehmt, dann wäre der Parameter für die Standardkalibrierung 168*5/3 = 280. Ihr müsst den Wert auf ein Vielfaches von 4 runden, wenn nötig. Mit setAutomaticSelfCalibrationStandardPeriod(280) würdet ihr den Wert dann einstellen. 

Mehr Informationen zu Low-Power Messungen findet ihr im Dokument SCD4x Low Power Operation von Sensirion.

Und noch einmal zu Erinnerung: bei Verwendung von Power-Down gibt es kein ASC. 

Erzwungene Kalibrierung (FRC – forced Recalibration)

Alternativ könnt ihr eine erzwungene Kalibrierung (FRC) mit performForcedRecalibration(CO2_CALIB_VALUE, frcCorr) vornehmen. Der Parameter CO2_CALIB_VALUE ist die aktuelle CO₂-Konzentration in ppm, auf die ihr kalibrieren wollt. Der „forced recalibration Factor“ frcCorr ist ein Umrechnungsfaktor für die Kalibrierung. Mit der Größe lässt sich nicht viel anfangen, außer, dass ein frcCorr von 0xFFFF eine fehlgeschlagene Kalibrierung anzeigt. 

Die meisten Nutzer werden nicht die Möglichkeit haben, eine bestimmte CO₂-Konzentration vorzugeben. Dazu müsstet ihr entweder künstlich eine entsprechende Atmosphäre schaffen oder ihr bräuchtet ein Gerät, das die CO₂-Konzentration zuverlässig und absolut misst. Was ihr machen könntet, ist intensiv zu lüften und dann als Konzentration 400 ppm (oder eher 430 ppm) anzunehmen. Das ist nach meiner Erfahrung die beste Methode, um die Sensoren in Betrieb zu nehmen.

Für eine gute Kalibrierung muss der SCD4x Sensor mindestens drei Minuten im periodischen Messmodus aktiv gewesen sein. Hier ein Sketch als Vorschlag:

#include <SensirionI2cScd4x.h>
#include <Wire.h>
#define CO2_CALIB_VALUE 400

SensirionI2cScd4x sensor;

static char errorMessage[64];
static int16_t error;

void setup() {
    Serial.begin(115200);
    Wire.begin();
    sensor.begin(Wire, SCD41_I2C_ADDR_62);
    uint64_t serialNumber = 0;
    delay(30);

    sensor.wakeUp();
    sensor.stopPeriodicMeasurement();
    sensor.reinit();
    sensor.getSerialNumber(serialNumber);
    if (error) {
        Serial.print("Error trying to execute getSerialNumber(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }
    Serial.print("SCD4x connected, serial number: ");
    PrintUint64(serialNumber);
    Serial.println();

    sensor.startPeriodicMeasurement();
    Serial.println("Forced calibration - warm up phase");
    unsigned int i = 300; // warm up period in seconds
    
    while (i>0) {
        Serial.print(F("Remaining time [s]: "));
        Serial.println(i);
        i -= 5; 
        delay(5000);
    }

    sensor.stopPeriodicMeasurement();
    uint16_t frcCorr = 0;
    error = sensor.performForcedRecalibration(CO2_CALIB_VALUE, frcCorr);
    if (error) {
        Serial.print("Error trying to execute performForcedRecalibration(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }
    else {
        Serial.print("FRC Value: 0x");
        Serial.println(frcCorr, HEX);
    }

    sensor.startPeriodicMeasurement();
}

void loop() {
    bool dataReady = false;
    uint16_t co2Concentration = 0;
    float temperature = 0.0;
    float relativeHumidity = 0.0;

    error = sensor.getDataReadyStatus(dataReady);
    if (error) {
        Serial.print("Error trying to execute getDataReadyStatus(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }

    if (dataReady) {
        sensor.readMeasurement(co2Concentration, temperature, relativeHumidity);
  
        Serial.println();
        Serial.print("CO2[ppm]: ");
        Serial.print(co2Concentration);

        Serial.print("\tTemperature[°C]: ");
        Serial.print(temperature, 1);

        Serial.print("\tHumidity[%RH]: ");
        Serial.print(relativeHumidity, 1);

        Serial.println();
    }
    else
        Serial.print(".");

    delay(500);
}

void PrintUint64(uint64_t& value) {
    Serial.print("0x");
    Serial.print((uint32_t)(value >> 32), HEX);
    Serial.print((uint32_t)(value & 0xFFFFFFFF), HEX);
}

 

Hier ein Ausschnitt der Ausgabe:

SCD4x - Ausgabe von scd4x_forced_calibration.ino
Ausgabe von scd4x_forced_calibration.ino

Weitere Korrekturen und Einstellungen

Die nun folgenden Korrekturen werden standardmäßig in den RAM des SCD4x geschrieben, d. h. nach einem Neustart sind Einstellungen gelöscht. Mit persistSettings() kopiert ihr die Daten vom RAM in den EEPROM des SCD4x Moduls. 

Um Korrekturen vornehmen zu können, müsst ihr die periodischen Messungen stoppen. Eine Ausnahme ist die Einstellung des Umgebungsdrucks. 

Weiter unten findet ihr einen Sketch, der die Einstellungen abfragt. 

Korrektur der Höhe bzw. des Drucks

Der Atmosphärendruck beeinflusst die Messung der CO₂-Konzentration. Der Atmosphärendruck schwankt wetterbedingt um einen Mittelwert. Dieser ist wiederum abhängig von der Höhe über dem Meeresspiegel. Die Standardeinstellung ist 0 Meter. Mit setSensorAltitude(sensorAltitude) könnt ihr die Einstellung ändern. Dabei ist sensorAltitude die Höhe in Metern. Mit getSensorAltitude() fragt ihr die Einstellung ab.

Trotzdem bleibt noch die wetterbedingte Schwankung. Wenn ihr einen Luftdrucksensor habt, könnt ihr den Mittelwert mit dem tatsächlichen Druck überschreiben. Dazu dient die Funktion setAmbientPressure(pressure). Dabei ist pressure der Druck in Pascal (= mbar * 100). Sinnvollerweise ist diese Funktion eine der wenigen, die auch während periodischer Messungen ausgeführt werden können. Die Funktion getAmbientPressure() gibt die aktuelle Einstellung zurück. Der Rückgabewert ist allerdings 0, wenn ihr noch keinen Druck definiert habt (oder das ist noch ein Bug in der Bibliothek). 

Korrektur der Temperatur

Die SCD4x Module erwärmen sich im Betrieb. Die gemessenen Temperaturen werden deshalb um einen Offset reduziert, damit sie der Umgebungstemperatur entsprechen. Der Standard-Offset beträgt 4 °C. Damit waren die Temperaturen bei meinen Messungen im periodischen Modus um ca. 0 bis 1 °C zu niedrig. Im Low-Power-Modus und im Single-Shot-Modus waren die Temperaturen bei Verwendung des Standard-Offsets um ca. 2 – 3 °C zu niedrig.

Neben dem Messmodus beeinflusst auch die Einbausituation des Moduls den zu wählenden Offset. Es gibt als nicht die eine Empfehlung. Probiert es einfach aus und passt den Wert an. 

Mit der Funktion setTemperatureOffset(offset) stellt ihr den Offset ein. offset ist vom Datentyp float. Mit  getTemperatureOffset() fragt ihr den Offset ab. Um den Offset einzustellen, müssen die periodischen Messungen gestoppt werden.

Der Temperatur-Offset beeinflusst die Ausgabewerte für die CO₂-Konzentration nicht, wohl aber die Ausgabewerte der relativen Luftfeuchte. Das macht Sinn, da die wärmere Luft in der Messzelle eine niedrigere relative Luftfeuchte hat. Für die Berechnung der CO₂-Konzentration nutzt der SCD4x natürlich die Rohwerte der Temperatur und der Luftfeuchte, da das ja die Bedingungen in der Messzelle sind.

Factory Reset

Wenn ihr zu viel an den Einstellungen herumgeschraubt habt und alles wieder in den Urzustand bringen wollt, dann übernimmt das die Funktion performFactoryReset() für euch. 

Abfrage der Einstellungen

Der folgende Sketch fragt die aktuellen Einstellungen ab und führt mit performSelfTest() einen Selbsttest durch. 

#include <SensirionI2cScd4x.h>
#include <Wire.h>

SensirionI2cScd4x sensor;

static char errorMessage[64];
static int16_t error;

uint16_t sensorVariant;
float tOffset = 3.0;
uint16_t tOffsetRaw = 9999;
uint32_t ambientPressure = 9999;
uint16_t sensorAltitude = 9999;
uint16_t ascEnabled = 9999; 
uint16_t ascTarget = 9999;
uint16_t ascInitialPeriod = 9999;
uint16_t ascStandardPeriod = 9999;
uint16_t sensorStatus = 9999;

void setup() {
    Serial.begin(115200);
    Wire.begin();
    sensor.begin(Wire, SCD41_I2C_ADDR_62);
    uint64_t serialNumber = 0;
    delay(30);

    sensor.wakeUp();
    sensor.stopPeriodicMeasurement();
    sensor.reinit();
    sensor.getSerialNumber(serialNumber);
    if (error) {
        Serial.print(F("Error trying to execute getSerialNumber(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }
    Serial.print(F("SCD4x connected, serial number: "));
    PrintUint64(serialNumber);
    Serial.println();

    sensor.getSensorVariantRaw(sensorVariant);
    Serial.print(F("Sensor Type: 0x"));
    Serial.println(sensorVariant, HEX);
    Serial.print(F("I am an SCD4"));
    Serial.println((sensorVariant & 0x1000) >> 12);
    Serial.println();

    sensor.getTemperatureOffsetRaw(tOffsetRaw);
    Serial.print(F("Temperature Offset [°C]: "));
    Serial.println(tOffsetRaw/65535.0 * 175.0, 1);

    // sensor.setAmbientPressure(101200); // uncomment to test
    error = sensor.getAmbientPressure(ambientPressure);
    Serial.print(F("Ambient pressure   [Pa]: "));
    Serial.println(ambientPressure);
    
    // sensor.setSensorAltitude(500); // uncomment to test
    sensor.getSensorAltitude(sensorAltitude);
    Serial.print(F("Sensor Altitude     [m]: "));
    Serial.println(sensorAltitude);
    Serial.println();

    Serial.println(F("Automatic self calibration (ASC):"));
    sensor.getAutomaticSelfCalibrationEnabled(ascEnabled);
    Serial.print(F(" - enabled?           : "));
    if(ascEnabled == 1) {
        Serial.println(F("yes"));
    }
    else Serial.println(F("no")); 

    sensor.getAutomaticSelfCalibrationTarget(ascTarget);
    Serial.print(F(" - target     [ppmCO2]: "));
    Serial.println(ascTarget);

    sensor.getAutomaticSelfCalibrationInitialPeriod(ascInitialPeriod);
    Serial.print(F(" - initial period  [h]: "));
    Serial.println(ascInitialPeriod);

    sensor.getAutomaticSelfCalibrationStandardPeriod(ascStandardPeriod);
    Serial.print(F(" - standard period [h]: "));
    Serial.println(ascStandardPeriod);
    Serial.println();

    Serial.print(F("Performing self test... "));
    sensor.performSelfTest(sensorStatus); 
    Serial.print(F("Result: "));
    if(sensorStatus == 0) {
        Serial.println(F("OK"));
    }
    else {
        Serial.print(F("not OK - status is "));
        Serial.println(sensorStatus);  
    }
}

void loop() {}

void PrintUint64(uint64_t& value) {
    Serial.print(F("0x"));
    Serial.print((uint32_t)(value >> 32), HEX);
    Serial.print((uint32_t)(value & 0xFFFFFFFF), HEX);
}

 

Ausgabe von scd4x_get_settings_and_self_test.ino
Ausgabe von scd4x_get_settings_and_self_test.ino

Tests und Vergleiche

SCD41 vs. Technoline WL 1030

Ich habe einen der SCD41-Sensoren gegen ein kommerziell erhältliches CO₂-Messgerät, das Technoline WL 1030, getestet. Das Technoline WL 1030 war Testsieger der Stiftung Warentest im Jahr 2021. Der Hersteller gibt eine Genauigkeit von +/- 5% +/- 50 ppm im Bereich 400 – 5000 ppm an.

Den Test habe ich in einer selbstgebastelten Box durchgeführt, die auch schon in meinem Beitrag über die MH-Z14 und MH-Z19 Sensoren zum Einsatz kam. Die elektrischen Anschlüsse habe ich durch die Wand geführt. Ein Elektromotor mit Propeller diente zur Durchmischung. Das CO₂ habe ich meinem Soda-Streamer entnommen und mit einer Spritze durch ein kleines Loch in die Kammer überführt. Mehr Details findet ihr in dem eben erwähnten Beitrag. 

SCD4x Test: Technoline WL 1030 vs. SCD41
Test: Technoline WL 1030 vs. SCD41

Das Technoline-Gerät und das SCD41 Modul habe ich frisch gegen 400 ppm CO₂ kalibriert, um einigermaßen vergleichbare Ausgangswerte zu haben. Dann habe ich jeweils 1 Milliliter CO₂ in die Kammer gegeben, 3-5 Minuten gewartet und die Messwerte notiert. Nach 6 Millilitern habe ich aufgehört und die Kammer zunächst geschlossen gelassen (bis Messwert 8, s. u.), später dann über längere Zeit offen. 

Technoline WL 1030 vs. SCD41
Testergebnisse: Technoline WL 1030 vs. SCD41 vs. kalkuliert.

Die Wiederholung des Tests mit einem weiteren SCD41 Modul ergab ein ähnliches Ergebnis.

Auswertung und Interpretation

Ich hatte abgeschätzt, dass eine Zugabe von 1 Milliliter reinem CO₂ die CO₂-Konzentration in der Kammer um ca. 308 ppm erhöhen müsste. Das ist die graue Gerade („Calculated“). Die gemessenen Werte stimmen zunächst gut überein, knicken dann aber weg. Das liegt aber nicht an den Messgeräten, sondern an meinem Unvermögen, die Kammer wirklich dicht zu bekommen. Das hatte ich in dem Beitrag über die MH-Z14- und MH-Z19-Module schon gezeigt, indem ich eine größere CO₂-Menge in einem Schub zugegeben habe. 

Daraus schließe ich:

  1. Die Messwerte des Technoline WL 1030 und des SCD41 machen zunächst einmal Sinn hinsichtlich ihrer absoluten Größe. 
  2. Die Messgeräte zeigen eine gute Übereinstimmung. Die Abweichungen lagen bei diesem Versuch bei <= 40 ppm.

Dass die Messwerte des SCD41 tendenziell etwas oberhalb der Messwerte des Technoline Gerätes lagen, hat sich in weiteren Versuchen bestätigt. Ich habe jeweils zwei Module über ein oder zwei Wochen parallel zu dem Technoline Gerät getestet, sodass auch automatische Kalibrierungen gegriffen haben. Die Übereinstimmung war immer gut. 

SCD4x Dauertest

Ich würde also sagen, dass man den Messwerten der SCD41 Module trauen kann und sie ihre spezifizierte Genauigkeit locker einhalten dürften, sofern ich das mit den mir zur Verfügung stehenden Mitteln beurteilen kann.  

Wenn ihr selbst vergleichende Versuche macht, dann würde ich raten, die Module parallel zu kalibrieren oder erst einmal ein paar Tage laufen zu lassen, bis die ASC greift. Ansonsten können sich die Werte auch gerne um 100 ppm oder mehr unterscheiden. 

SCD41 vs. SCD40

Zur Prüfung des SCD40 habe ich ein solches Modul parallel mit einem SCD41 Modul auf 400 ppm CO₂ kalibriert und in meinem Arbeitszimmer laufen lassen. Über den Nachmittag haben sich die Werte folgendermaßen entwickelt:

SCD4x Praxistest: SCD40 vs. SCD41
SCD4x Praxistest: SCD40 vs. SCD41

Hier lagen die Messwerte des SCD40 noch einmal höher als die des SCD41 Moduls. Dieser Trend hat sich in Messungen über mehrere Tage bestätigt. Manchmal enteilte der SCD40 auch um fast 100 ppm, jedoch glichen sich die Werte nach ein paar Minuten wieder näher an.

Ohne parallele Kalibrierung liefen der SCD40 und der SCD41 in der Anfangsphase zum Teil um 150 ppm auseinander.  

Meine Datenlage für den SCD40 ist insgesamt weniger solide, da ich nur einzelnes Modul besitze und dieses weniger als die SCD41 Module getestet habe. Aber in der Tendenz schien sich zu bestätigen, dass das SCD40 Module ungenauer misst. Da der Preisunterschied nicht so besonders groß ist, würde ich raten, zum SCD41 zu greifen. 

MH-Z14/19 vs. SCD4x

Und was ist nun besser, die MH-Z14/19-Module oder die SCD4x-Module? Preislich nehmen sich die Module nicht viel, sofern man in Fernost, beispielsweise bei AliExpress, einkauft. Sinnvolle Werte liefern sie auch alle. Es gibt aber ein paar Punkte, bei denen die SCD4x-Module die Nase vorn haben:

  • Mehr Einstellmöglichkeiten
  • Kleinere Abmessungen
  • Bessere Dokumentation, weniger Blackbox  
  • Geringerer Stromverbrauch + Low-Power Optionen

Wer einen MH-Z14 oder MH-Z19 hat und damit glücklich ist, dem würde ich nicht sagen, er müsse sich unbedingt einen SCD40 oder SCD41 zulegen. Wer aber vor einer Neuanschaffung steht, dem würde ich eher ein SCD4x empfehlen, vor allem den SCD41.

Anhang – Single-Shot Messungen mit Tiefschlafpausen

Hier nun zwei Sketche, die veranschaulichen sollen, wie ihr die Kombination aus Mikrocontroller und SCD41 Modul energiesparend einsetzt. Dafür schickt ihr die Mikrocontroller in den Tiefschlaf. Einige Mikrocontroller, etwa die guten, alten AVR-Vertreter, machen nach dem Aufwachen dort weiter, wo sie aufgehört hatten. Andere starten nach dem Aufwachen mit einem Reset, so wie beispielsweise der ESP32. Zwar gibt es auch Schlafmodi, in denen ein ESP32 keinen Reset ausführt, aber die sind erheblich weniger energiesparend. 

Mit den Sketchen will ich lediglich das Prinzip veranschaulichen. Für ein batteriebetriebenes Projekt würdet ihr die Daten sicherlich nicht auf dem seriellen Monitor, sondern auf einem energiesparenden Display ausgeben. 

Tiefschlaf mit den klassischen AVR-basierten Arduino-Boards

Den folgenden Sketch habe ich für ATmega328P basierte Boards, wie den Arduino UNO R3, den klassischen Arduino Nano oder den Arduino Pro Mini geschrieben. Oder ihr verwendet den „nackten“ ATmega328P.

Zum Wecken aus dem Tiefschlaf verwende ich den Watchdog Timer (WDT). Die anderen Timer funktionieren für diesen Zweck nicht, da sie im Tiefschlaf abgeschaltet sind. Die maximale Periode für den WDT-Interrupt beträgt 8 Sekunden. Ein Zähler wird bei jedem Aufwachen um 1 erhöht. Ist der Zähler kleiner 37, legt sich der ATmega328P wieder hin. Das kurze Aufwachen fällt energetisch kaum ins Gewicht. Beim 37sten Aufwachen wird die Messung durchgeführt. 37 x 8 ergeben 296 Sekunden, also ungefähr fünf Minuten. 

Im Power-Down-Zustand vergisst der SCD41 seine Einstellungen. Wenn ihr Power-Down nutzt und im Setup Einstellungen vornehmt, dann müsst ihr die Einstellungen nach dem Aufwachen erneut setzen oder ihr schreibt sie in den EEPROM des SCD41.

#include <Wire.h>
#include <SensirionI2cScd4x.h>
#include <avr/wdt.h>
#include <avr/sleep.h>
#define WDT_WAKE_UPS 37 // = ~5min (WDT_WAKE_UPS * 8s = Single Shot Pause) 
//#define USE_POWER_DOWN

volatile unsigned int wdtCounter = 0;

SensirionI2cScd4x sensor;

static char errorMessage[64];
static int16_t error;

void setup() {
    ADCSRA = 0; // ADC off (would consume current in deep sleep)
    Serial.begin(115200);
    Wire.begin();
    uint64_t serialNumber = 0;
    sensor.begin(Wire, SCD41_I2C_ADDR_62);
    delay(30);

    sensor.wakeUp();
    if (error) {
        Serial.print(F("Error trying to execute wakeUp(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.stopPeriodicMeasurement();
    if (error) {
        Serial.print(F("Error trying to execute stopPeriodicMeasurement(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.reinit();
    if (error) {
        Serial.print(F("Error trying to execute reinit(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    error = sensor.getSerialNumber(serialNumber);
    if (error) {
        Serial.print(F("Error trying to execute getSerialNumber(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
        return;
    }
    Serial.print(F("SCD4x connected, serial number: "));
    PrintUint64(serialNumber);
    Serial.println();

    error = sensor.setTemperatureOffset(2.0); 
    if (error) {
        Serial.print(F("Error trying to execute setTemperatureOffset(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
    Serial.println();

#ifdef USE_POWER_DOWN 
    error = sensor.persistSettings(); 
    if (error) {
        Serial.print(F("Error trying to execute persistSettings(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
#endif

    watchdogSetup();
}

void loop() {
    uint16_t co2Concentration = 0;
    float temperature = 0.0;
    float relativeHumidity = 0.0;

#ifdef USE_POWER_DOWN 
    error = sensor.measureSingleShot();
    if (error) {
        Serial.print("Error trying to execute measureSingleShot(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
#endif

    error = sensor.measureAndReadSingleShot(co2Concentration, temperature, relativeHumidity);
    if (error) {
        Serial.print(F("Error trying to execute measureAndReadSingleShot(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    Serial.print(F("CO2[ppm]: "));
    Serial.print(co2Concentration);

    Serial.print(F("\tTemperature[°C]: "));
    Serial.print(temperature, 1);

    Serial.print(F("\tHumidity[%RH]: "));
    Serial.println(relativeHumidity, 1);
    Serial.println(F("-----"));
    Serial.flush();

#ifdef USE_POWER_DOWN
    error = sensor.powerDown();
    if (error) {
        Serial.print("Error trying to execute powerDown(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
#endif

    while(wdtCounter < WDT_WAKE_UPS){
        set_sleep_mode(SLEEP_MODE_PWR_DOWN); // chose power down modus
        sleep_mode(); // sleep now!
    }
    wdtCounter = 0;
    sleep_disable(); // disable sleep after wake up
    
#ifdef USE_POWER_DOWN
    error = sensor.wakeUp();
    if (error) {
        Serial.print("Error trying to execute wakeUp(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
#endif
    
#ifdef USE_POWER_DOWN
    Serial.println(F("I woke up! Wait 10s for a result"));
#else
    Serial.println(F("I woke up! Wait 5s for a result"));
#endif
}

void watchdogSetup(void){
    cli();
    wdt_reset();
    WDTCSR |= (1<<WDCE) | (1<<WDE);
    WDTCSR = (1<<WDIE) | (0<<WDE) | (1<<WDP3) | (1<<WDP0);  // 8s / interrupt, no system reset
    sei();
}

void PrintUint64(uint64_t& value) {
    Serial.print(F("0x"));
    Serial.print((uint32_t)(value >> 32), HEX);
    Serial.print((uint32_t)(value & 0xFFFFFFFF), HEX);
}

ISR(WDT_vect){ 
  wdtCounter++;
}

 

Und so schaut dann die Ausgabe aus: 

Ausgabe von scd4x_single_shot_with_avr_wdt.ino
Ausgabe von scd4x_single_shot_with_avr_wdt.ino

Zu beachten ist noch, dass man nach dem Power-Down des SCD41 den ersten Messwert verwerfen sollte. Dadurch beträgt die Messzeit 10 Sekunden anstelle von 5. Da muss man dann rechnen, ob sich die zusätzliche Stromeinsparung während der Power-Down-Phase überhaupt lohnt.

Außerdem, auch wenn ich mich wiederhole, funktioniert die automatische Selbstkalibrierung (ASC) bei Verwendung von Power-Down nicht.

Und noch eine Wiederholung: Wenn ihr die ASC verwendet und die Schlafphase von 5 Minuten abweicht, dann müsst ihr die Perioden für die Erst- und die Standardkalibrierung anpassen.   

Tiefschlaf mit dem ESP32

Den ESP32 können wir über den Timer wecken. Die Schlafzeit geben wir in Mikrosekunden vor. 

Wenn der ESP32 aus dem Tiefschlaf erwacht, durchläuft er setup() erneut. Dort gibt es einige Anweisungen, die lediglich dazu dienen, einen sauberen, definierten Zustand für den ersten Start zu schaffen. Das brauchen wir aber nicht in den Folgestarts. Deshalb unterscheidet der folgende Sketch zwischen dem Erst- und den Folgestarts. Und auch hier werden die Einstellungen im Erstdurchlauf gespeichert, wenn Power-Down aktiviert ist. 

#include <Wire.h>
#include <SensirionI2cScd4x.h>
#define SLEEP_TIME 300000000 // = 300s = 5min
#define USE_POWER_DOWN

SensirionI2cScd4x sensor;

static char errorMessage[64];
static int16_t error;

void setup() {
    uint16_t co2Concentration = 0;
    float temperature = 0.0;
    float relativeHumidity = 0.0;

    Serial.begin(115200);
    Wire.begin();
    delay(1000);
    esp_sleep_wakeup_cause_t wakeupReason;
    wakeupReason = esp_sleep_get_wakeup_cause();
   
    sensor.begin(Wire, SCD41_I2C_ADDR_62);
    delay(30);

    if(!(wakeupReason==ESP_SLEEP_WAKEUP_TIMER)) { // initial setup
        uint64_t serialNumber = 0;
        error = sensor.wakeUp();
        if (error) {
            Serial.print(F("Error trying to execute wakeUp(): "));
            errorToString(error, errorMessage, sizeof errorMessage);
            Serial.println(errorMessage);
        }

        error = sensor.stopPeriodicMeasurement();
        if (error) {
            Serial.print(F("Error trying to execute stopPeriodicMeasurement(): "));
            errorToString(error, errorMessage, sizeof errorMessage);
            Serial.println(errorMessage);
        }

        error = sensor.reinit();
        if (error) {
            Serial.print(F("Error trying to execute reinit(): "));
            errorToString(error, errorMessage, sizeof errorMessage);
            Serial.println(errorMessage);
        }

        error = sensor.getSerialNumber(serialNumber);
        if (error) {
            Serial.print(F("Error trying to execute getSerialNumber(): "));
            errorToString(error, errorMessage, sizeof errorMessage);
            Serial.println(errorMessage);
            return;
        }
        Serial.print(F("SCD4x connected, serial number: "));
        PrintUint64(serialNumber);
        Serial.println();

        error = sensor.setTemperatureOffset(1.0); 
        if (error) {
            Serial.print(F("Error trying to execute setTemperatureOffset(): "));
            errorToString(error, errorMessage, sizeof errorMessage);
            Serial.println(errorMessage);
        }

#ifdef USE_POWER_DOWN 
        error = sensor.persistSettings(); 
        if (error) {
            Serial.print(F("Error trying to execute persistSettings(): "));
            errorToString(error, errorMessage, sizeof errorMessage);
            Serial.println(errorMessage);
        }
#endif

    } // end of initial setup
    else 
#ifdef USE_POWER_DOWN 
        error = sensor.wakeUp();
        if (error) {
            Serial.print(F("Error trying to execute wakeUp(): "));
            errorToString(error, errorMessage, sizeof errorMessage);
            Serial.println(errorMessage);
        }
        Serial.println(F("I woke up! Wait 10s for a result"));
#else 
        Serial.println(F("I woke up! Wait 5s for a result"));
#endif
  
#ifdef USE_POWER_DOWN 
    error = sensor.measureSingleShot();
    if (error) {
        Serial.print(F("Error trying to execute measureSingleShot(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
#endif
    
    error = sensor.measureAndReadSingleShot(co2Concentration, temperature, relativeHumidity);
    if (error) {
        Serial.print(F("Error trying to execute measureAndReadSingleShot(): "));
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }

    Serial.print(F("CO2[ppm]: "));
    Serial.print(co2Concentration);

    Serial.print(F("\tTemperature[°C]: "));
    Serial.print(temperature, 1);

    Serial.print(F("\tHumidity[%RH]: "));
    Serial.println(relativeHumidity, 1);
    Serial.println(F("-----"));
    Serial.flush();    

#ifdef USE_POWER_DOWN
    error = sensor.powerDown();
    if (error) {
        Serial.print("Error trying to execute powerDown(): ");
        errorToString(error, errorMessage, sizeof errorMessage);
        Serial.println(errorMessage);
    }
#endif

    esp_sleep_enable_timer_wakeup(SLEEP_TIME);
    esp_deep_sleep_start();
}

void loop(){}

void PrintUint64(uint64_t& value) {
    Serial.print(F("0x"));
    Serial.print((uint32_t)(value >> 32), HEX);
    Serial.print((uint32_t)(value & 0xFFFFFFFF), HEX);
}

 

11 thoughts on “SCD4x CO₂-Sensoren

  1. schöner Bericht. Ich habe SCD41 Sensoren bisher 3x (als Prototypen) in HomeAssistant integriert:
    1. 1-wire via AVR 1-wire emulation und DS2450 Erweiterung in HA (seit HA core 2025.3) gemerged.
    2. WLAN via ESP32 Tasmota – wird direkt erkannt. – sehr geringer Aufwand.
    3. ZigBee in ZHA mit ESP32H2 mit RGBLED Steuerung über HA, die min/max Erweiterung für die Controls (ppm > 1023) wird gerade gemerged. So kann die RGB LED über HA ein/ausgeschaltet werden und das Ampelverhalten eingestellt und weiter automatisiert werden. Das läuft schon sehr stabil und braucht sehr wenig Strom – mein Favorit, weil ich viele ZigBee Geräte schon betreibe und der Aufwand für diese Art Controls bei 1-wire schon erheblich größer ist (AVR Studio + ICE). Betrieben werden kann das als EndPoint oder sogar als Router. Die benötigten Software Erweiterungen sind soweit abgeschlossen und nun geht es an das Gehäuse Design (3D Druck), was auch einige Anforderungen an den Airflow und die Problematik Eigenerwärmung hat, siehe Sensirion white papers.
    Was mich insbesondere bei der Beschaffung der SCD41 Module beschäftigt hat war, warum den China Sensoren die QR-code Laser Markierung fehlt. Es gibt auch keine Angaben zur verbauten Produktversion. Sind die dann überhaupt Factory kalibriert? Lt. Sensirion werden seit April 2023 die SCD41 Sensoren in einer verbesserten Version SCD41-D-R2 mit Datasheet 1.6 produziert und die haben lt. Sensirion das Laser Marking. Heute sind wir bei Datasheet Version 1.7. Leider kann man (bisher) beim SCD41 die Produktversion nicht per Software auslesen. Man kauft in China also die Katze im Sack und wahrscheinlich Restbestände der SCD41-D-R1 aus der Zeit vor der Verbesserung?
    Ein PCB mit der aktuellen Version zu erstellen wäre ein Möglichkeit. SCD41 mit SCD41-D-R2 sind bei den einschlägigen PCBA Herstellern zu günstigen Konditionen verfügbar. Die PCBAs sind auch günstig, selbst bei kleinen Stückzahlen. Und das benötigte Lötprofil wird auch unterstützt. Vielleicht hat ja jemand vor diesen Weg zu gehen. Ich würde davon gerne erfahren und mich wahrscheinlich beteiligen …
    Viele Grüße
    Tom

    1. Hi Tom,

      vielen Dank für das Teilen deiner Erfahrungen. Was die China-Ware angeht, nun ja, da weiß man nie so wirklich, was man bekommt. Ich kann nur sagen, dass meine Tests des SCD41 von Sparkfun (für das ich fast 100 Euro ausgegeben habe) im ca. 2-Wochentest keine bemerkenswerten Unterschiede zu den China SCD41 zeigten (für die ich um die 15 € ausgegeben habe). Liefen alle gut. Allerdings habe ich sie zu Beginn auf 400 ppm CO2 kalibriert. Ohne Kalibrierung waren die Werte zu Beginn recht stark untereinander abweichend. Gilt aber auch für Sparkfun SCD41 vs. Technoline WL 1030. Mein No-Name SCD40 scheint chronisch etwas hoch messen. Da fehlt mir allerdings der Vergleich zum Markenprodukt und es hat ja auch eine niedrigere Genauigkeit.
      Das mit der PCB ist ein guter Vorschlag. Ich selbst brauche jetzt aber erst einmal eine SCD4x Pause nach einigen Wochen intensiver Arbeit damit.
      VG, Wolfgang

  2. Hallo Wolfgang,
    danke für den wieder sehr ausführlichen Bericht über die CO2 Sensoren!
    Alle deine Berichte finde ich großartig, sie sind alle ingenieurmäßig verfasst!!
    Kein Geschwavel, nur Fakten und alle logisch aufgebaut!
    Bitte weiter so!
    Schöne Grüße Enno Jürgens

  3. Danke Wolfgang für diesen weiteren prima Testbericht (natürlich auf passendem Lochraster gedruckt 😊 )
    👍👍👍

  4. Ich kann die Ergebnisse im großen und ganzen bestätigen, auch im Vergleich zum MHZ19. Einzige Einschränkung: ich sehe an den Messkurven keinen Unterschied zwischen SCD40 und SCD41, die bewegen sich in der dokumentierten Ungenauigkeit (die übrigens mit dem Messwert deutlich steigt). Und da zumindest ich keine Experimente auf wissenschaftlichen Niveau mit kontrollierten Messbedingungen mache, spielt die zusätzliche „Genauigkeit“ des SCD41 keine praktische Rolle im Vergleich zu den anderen Einflussfaktoren.

    Auch ich habe Markenprodukte gegen No-Name aus Ali-Land getestet und auch keine Unterschied festgestellt.

    Mein System misst alle fünf Minuten, gibt die Daten auf ein e-Ink aus und schaltet dann komplett ab. Ich habe eine Reihe von Tests gemacht die zeigen, dass in diesem Fall zumindest der erste Messwert verworfen werden sollte. Die Werte werden anschließend noch etwas besser, aber hier geht es mir um einen Kompromiss zwischen Genauigkeit und Stromverbrauch.

    Die Temp/Hum Daten werden bei mir im Dauerbetrieb nach einigen Minuten sehr gut (mit dem Default-Offset), bei meinem An/Aus-Messverfahren sind sie ähnlich fehlerbehaftet wie hier beschrieben.

    1. Hallo Bernhard,

      vielen Dank für das Teilen deiner Erfahrungen. Da bin ich aber froh, dass sie nicht grundsätzlich verschieden sind!

      VG, Wolfgang

  5. Wie passend, ich suche aktuell gerade einen CO2 Sensor der meinen Alphasense OPC N3 Sensor (Partikelzähler) unterstützt, vielen Dank für den ausführlichen Test! Habe den SDC41 direkt bestellt für 23€ bei Amazon.

  6. Hallo,
    Danke für den Beitrag.
    Ich verstehe nur nicht, warum mich der CO2 Gehalt in meiner Atemluft interessieren sollte. Der O2 Gehalt ist doch viel interessnater.

    Grüße!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert