Arduino Project

July 2022

Arduino CO2 Data Logger


Functional Requirements

  • Record CO2 between 400 and 10000 ppm, along with temperature and humidity, capturing the current and maximum CO2 value during the recording session.  
  • Log data to SD card at least every two seconds (2 Hz)
  • Latching pushbutton to start/stop data recording
  • Display CO2 ppm value on display

 

Hardware & Circuit

Adafruit Arduino Data Logging Shield

 

CO2 + Temperature + Humidity Sensor

Sparkfun PN SEN-15112

SCD30 Datasheet

Firmware

Note that due to Arduno Uno memory constraints, the real time clock in the Arduino Data Logging Shield was not utilized.   The elapsed time in milliseconds was recorded instead.  


/*

  CO2, Temp, Humidity data logger
  Writes data to SD card at 4 Hz rate
  Pushbutton starts/stops data recording
  (pushbutton LED illuminates when recording, 
  blinks on each SD card save)
  Pushbutton start recording resets max CO2 capture.
  Displays CO2 ppm when not recording data

  I2C addresses used:
    0x20    Adafruit Arduino Data Logging Shield
    0x61    SCD30 CO2 sensor
    0x68    Adafruit Arduino Data Logging Shield
    0x70    LCD I2c backpack
 
  Adafruit Arduino Data Logging Shield (older version)
  Product #1141
  https://www.adafruit.com/product/1141
  https://learn.adafruit.com/adafruit-data-logger-shield?view=all
  http://arduino.cc/en/Reference/SD
  The big SD card holder can fit any SD/MMC storage up to 
  32G and and small as 32MB (Anything formatted FAT16 or FAT32)
  Pins used:
    Digital #13 - SPI clock  (DO NOT USE PIN #13 for LED!)
    Digital #12 - SPI MISO
    Digital #11 - SPI MOSI
    Digital #10 - SD Card chip select
    SDA connected to A4 (RTC) 10k pullup on shield
    SCL connected to A5 (RTX) 10k pullup on shield
    Digital #3 red LED
    Digital #4 green LED

  Sparkfun CO2 Sensor (+ temp & humidity)
  Product #SEN-15112
  https://www.sparkfun.com/products/15112
  https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library
  I2C address: 0x61

  16x2 LCD
  Adafruit i2c/SPI LCD backpack using MCP23008 I2C expander, product #392
  

  FREE Pins: A0, A1, A3, 9, 8, 5, 2

  Configure the AArduino IDE board to "Arduino Uno"

*/

#define DEBUG false

//#define PIN_LED_RED 3   // Works, but hidden in enclosure
//#define PIN_LED_GREEN 4 // CANNOT BE CONTROLLED

void blinkLED(byte ledPIN){
  //  consumes 300 ms.
  for(int i = 5; i>0; i--){
    digitalWrite(ledPIN, HIGH);
    delay(30);
    digitalWrite(ledPIN, LOW);
    delay(30);
  }    
} // blinkLED()

void blinkERR(byte ledPIN){
  // S-O-S
  const int S = 150, O = 300;
  for(int i = 3; i>0; i--){
    digitalWrite(ledPIN, HIGH);
    delay(S);
    digitalWrite(ledPIN, LOW);
    delay(S);
  }    
  delay(200);
  for(int i = 3; i>0; i--){
    digitalWrite(ledPIN, HIGH);
    delay(O);
    digitalWrite(ledPIN, LOW);
    delay(O);
  }    
  delay(200);
  for(int i = 3; i>0; i--){
    digitalWrite(ledPIN, HIGH);
    delay(S);
    digitalWrite(ledPIN, LOW);
    delay(S);
  }    
  delay(200);
} // blinkERR()


#define PIN_PUSHBUTTON_LED 9
#define PIN_PUSHBUTTON_PULLDOWN 8
byte buttonState = LOW;     // buttonState & lastButtonState are initialized for a pulldown resistor         
byte lastButtonState = LOW;
unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 50;    // The switch debounce time.  50 to 100 ms

//////////////////////////////////////////////////////////////////////////////
// The optimal balance between current consumption and response time is 15 seconds.
// Read the CO2 sensor every timerInterval.
// This sketch sets the CO2 sensor update interval to 2 sec (min is 2 sec).
float CO2ppm = 0.0;
float CO2ppmMax = 0.0;
float TempC = 0.0;
float HumidityPct = 0.0;
boolean recToSD = false;
//////////////////////////////////////////////////////////////////////////////
//  microSD card
#include <SPI.h>
#include <SD.h>
// Revise the pin # below to match your mSD card hardware for Card Detect
// or chip select (CS), or 'chip/slave select' (SS). 
#define PIN_SD_CHIP_SELECT 10  
File sdfile;
char filename[14];

void CreateSDfile() {
  // Create a filename reference to a file that doesn't exist 'ANALOG00.TXT'..'ANALOG99.TXT'
  strcpy(filename, "/LOGGER00.TXT");
  for (uint8_t i = 0; i < 100; i++) {
    filename[7] = '0' + i/10;
    filename[8] = '0' + i%10;
    // create if does not exist, do not open existing, write, sync after write
    if (SD.exists(filename)){
      #if DEBUG
      Serial.print("File '");
      Serial.print(filename);
      Serial.println("' already exists");
      #endif
    } else {
      #if DEBUG
      Serial.print("New file will be '");
      Serial.print(filename);
      Serial.println("'");
      #endif
      break;
    }
  }
  // Open file on SD Card for writing
  sdfile = SD.open(filename, FILE_WRITE);
  if (! sdfile) {
    #if DEBUG
    Serial.print("ERROR - unable to create '");
    Serial.print(filename); Serial.println("'");
    #endif
    while (1) { blinkERR(PIN_PUSHBUTTON_LED);}
  }
  sdfile.println("Time[ms];CO2[ppm];CO2max[ppm];Temp[C];Humidity[%]");
  
} // CreateSDfile()

//////////////////////////////////////////////////////////////////////////////
// Sensirion SCD30 CO2 sensor
// Detect 400 to 10000 PPM with an accuracy of ±(30ppm+3%). 
// https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library
// Note: 100kHz I2C is fine, but according to the datasheet 400kHz I2C is not supported by the SCD30
#include <Wire.h>
#include "SparkFun_SCD30_Arduino_Library.h"
SCD30 airSensor;
//const byte pinRDY = 9;   // DOES NOT WORK !!! Data ready pin.  High when data is ready for read-out.
//const byte pinSCL = A5;  // I2C Arduino UNO
//const byte pinSDA = A4;  // I2C Arduino UNO
//////////////////////////////////////////////////////////////////////////////

void pushButton() {
  // Detect a change in the pushbutton state and turn on the
  // pushbutton LED in response. 
  
  // This logic is for a button using a pulldown resistor. 
  buttonState = digitalRead(PIN_PUSHBUTTON_PULLDOWN); 
  // if millis() or timer wraps around, reset it
  if (lastDebounceTime > millis())  lastDebounceTime = millis();
  if (buttonState != lastButtonState) { 
    // Multiple changes in the buttonState can occur when a pushbutton is 
    // pressed, or a switch is toggled. Use a debounce timer to only react
    // to a change in button state after an interval of debounceDelay. 
    lastButtonState = buttonState;
    //  Check if enough time has passed to evaluate another pushbutton press.
    if ((millis() - lastDebounceTime) > debounceDelay) {
      lastDebounceTime = millis();
      if (buttonState == HIGH) {
        recToSD = true;
        CO2ppmMax = 0.0;
        digitalWrite(PIN_PUSHBUTTON_LED, HIGH);
        CreateSDfile();
      } else {
        recToSD = false;
        digitalWrite(PIN_PUSHBUTTON_LED, LOW);
        sdfile.close();
      } // buttonState
    } // millis()
  } // buttonState != lastButtonState  
} // pushButton()

//////////////////////////////////////////////////////////////////////////////
// 16x2 LCD + Adafruit i2c LCD backpack using MCP23008 I2C expander
//#include "Wire.h"
#include "Adafruit_LiquidCrystal.h"
// Default I2C address #0 (A0 to A2 not jumpered)
Adafruit_LiquidCrystal lcd(0);  // 0x70

//////////////////////////////////////////////////////////////////////////////


void setup() {  

  pinMode(PIN_PUSHBUTTON_LED, OUTPUT);
  blinkLED(PIN_PUSHBUTTON_LED);

  pinMode(PIN_PUSHBUTTON_PULLDOWN, INPUT);

  #if DEBUG
    Serial.begin(9600);
    while (!Serial) {
      delay(1);
    }
    Serial.println("Serial ready");
  #endif

  Wire.begin();

  // 16x2 LCD + Adafruit i2c LCD backpack
  lcd.begin(16,2);
  lcd.setBacklight(HIGH);
  lcd.clear();
  lcd.setCursor(0,0); // 1st row
  //        "01234567890123456"
  lcd.print("   Mechatronic");
  lcd.setCursor(0,1); // 2nd row
  //        "01234567890123456"
  lcd.print("  Solutions LLC");

  //////////////////////////////////////////////////////////////////////////////
  // Sensirion SCD30 CO2 sensor
  if (airSensor.begin() == false) {
    #if DEBUG
    Serial.println("ERROR - CO2 sensor not detected.  Check wiring");
    #endif
    while (1) { blinkERR(PIN_PUSHBUTTON_LED);}
  }
  airSensor.setMeasurementInterval(2); //Change number of seconds between measurements: 2 to 1800 sec, stored in non-volatile memory of SCD30
  //int airSensorInterval = airSensor.getMeasurementInterval();
  //#if DEBUG
  //Serial.print("airSensorInterval: ");
  //Serial.print(airSensorInterval);
  //Serial.println(" sec");
  //#endif
  //////////////////////////////////////////////////////////////////////////////
  // see if the SD card is present and can be initialized: 
  if (!SD.begin(PIN_SD_CHIP_SELECT)) {
    #if DEBUG
    Serial.println("SD card failed, or not present");
    #endif
    while (1) { blinkERR(PIN_PUSHBUTTON_LED);}
  }
  #if DEBUG
  Serial.println("SD card initialized."); 
  #endif

  //////////////////////////////////////////////////////////////////////////////
  #if DEBUG
    Serial.println("Setup complete\n");
    Serial.println("Time[ms]; CO2ppm; CO2pppMax; TempC; Humidity%");
  #endif
  
} // setup()

void loop() {

  pushButton();

  // Read SCD30 sensor data as quickly as possible.  See .setMeasurementInterval()
  if (airSensor.dataAvailable()) {
    // Get the maximum between published readings, 
    // and the overall session max as quickly as possible.
    CO2ppm = airSensor.getCO2();
    CO2ppmMax = max(CO2ppmMax, CO2ppm);
    TempC = airSensor.getTemperature();
    HumidityPct = airSensor.getHumidity();
    if (recToSD == true) {
      // toggle LED to show write activity
      digitalWrite(PIN_PUSHBUTTON_LED, LOW);
      #if DEBUG
        Serial.print(millis());
        Serial.print(";");
        Serial.print(CO2ppm);
        Serial.print(";");
        Serial.print(CO2ppmMax);
        Serial.print(";");
        Serial.print(TempC);
        Serial.print(";");
        Serial.println(HumidityPct);
      #endif
      //        "01234567890123456"
      //        "##### ppm  #####"
      //        " ##.# C    ### %
      lcd.clear();
      lcd.setCursor(0,0); // 1st row
      //        "01234567890123456"
      lcd.print(CO2ppm,0);
      lcd.print(" ppm");
      lcd.setCursor(11,0); // 2nd row
      lcd.print(CO2ppmMax,0);
      lcd.setCursor(0,1); // 2nd row
      //        "01234567890123456"
      lcd.setCursor(0,1); // 2nd row
      lcd.print(TempC,1);
      lcd.print(" C");
      lcd.setCursor(11,1); // 2nd row
      lcd.print(HumidityPct,0);
      lcd.print(" %");
      sdfile.print(CO2ppm);
      sdfile.print(";");
      sdfile.print(CO2ppmMax);
      sdfile.print(";");
      sdfile.print(TempC);
      sdfile.print(";");
      sdfile.println(HumidityPct);
      // Execute a flush() to insure it is written since no sdfile.close() will be issued.
      sdfile.flush();
      digitalWrite(PIN_PUSHBUTTON_LED, HIGH);
      CO2ppm = 0.0;
    } else {
      lcd.clear();
      //        "0123456789012345"
      //        "REC OFF    #####"
      //        "/filename00.ext"
      lcd.setCursor(0,0); // 1st row
      lcd.print("REC off ");
      lcd.setCursor(11,0); // 1st row
      lcd.print(CO2ppm,0);
      lcd.print(" ppm");
      lcd.setCursor(0,1); // 2nd row
      lcd.print(filename);
    } // recToSD
    
    // loop time w/o saving to SD card is 138 to 142 ms
    // loop time saving to SD card is 156 to 158 ms
  
  } else {
    // SCD30 sensor data not ready
    
  } // airSensor.dataAvailable()

} // loop()

 


Do you need help developing or customizing a IoT product for your needs?   Send me an email requesting a free one hour phone / web share consultation.  

 

The information presented on this website is for the author's use only.   Use of this information by anyone other than the author is offered as guidelines and non-professional advice only.   No liability is assumed by the author or this web site.