I opted for the MH-Z16 sensor to measure the CO2 concentration. Its datasheet can be found here. We can communicate with this sensor via “UART”. For that we need to connect four pins on the sensor to a Arduino type board, in my case, a Wemos D1 mini. I cut the wires and solderded jumper wires to them.

Table 1: Pin layout of MH-Z16 CO2 sensor

Pin Description
3 GND
4 5V
5 RX (Receive)
6 TX (Transmit)

The sensor needs a power supply of 5V, connect pin 4 to the 5V supply of the Arduino. Obviously, a connection to ground (GND) is needes as well, connect pin 3 to the GND of the Arduino. When you transmit (TX) something from the Arduino the sensor has to receive (RX) and vice versa. So we connect the RX pin (5) of the sensor to the TX pin of the Arduino and the TX pin (6) of the sensor to the RX pin of the Arduino.

Now we need to setup the communication between the Arduino and the sensor. Now, probably you can go the easy way and download a “package/library”, but I decided to go the hard way and start from scratch, as I believe I will learn much more from this.

To connect with the sensor we need to open a serial connection. I created a serial connection by using the “SoftwareSerial Library”. Most pins on an Arduino are “GPIO” (General Purpose Input Output), which means that we have some freedom to assign tasks to them. I selected the D3 and D4 pin on the Wemos D1 mini to act as the RX (receive) and TX (transmit) pins.

#include <SoftwareSerial.h>
SoftwareSerial CO2Sensor(D3, D4); // SoftwareSerial(rxPin, txPin)

In the setup() code we open a serial connection with the sensor and a serial connection between the Arduino (Wemos in my case) and my computer, see below.

void setup() {
// put your setup code here, to run once:
Serial.begin(9600); // We open the serial connection with a Baudrate of 9600
CO2Sensor.begin(9600); // We open a serial connection with the sensor with a Baudrate of 9600
}

To get a reading we need to send a command to the sensor. According to the datasheet we need to send the following command to the sensor:


Figure 1: Command to read CO2 value from MH-Z16 sensor

We can define this command in code in the following way:

unsigned char Com_ReadCO2[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79}; //Command to read CO2 concentration

Now we create a function in which we send the command and read out the value returned by the sensor. According to the manual we would receive the following command:


Figure 2: Command to read CO2 value from MH-Z16 sensor

Byte1 is 0x86 which means “Read CO2 concentration”. The following two bytes are used to calculate the CO2 concentration according to:

\[CO_{2} \ concentration = HIGH * 256 + LOW\]

In this equation HIGH is byte2 and LOW is byte3. In the script we define an empty vector (OutputData) to collect the receiving bytes. We then iterate from 0 to 8 to start from byte0 to byte8. During iteration we read a byte from the sensor and save it to the array. After iteration we define byte2 as “High” and byte3 as “Low” and plug these numbers in equation 1 to find the CO2 concentration, which is printed to the serial communication of the PC.

//-----------------------------------------------------
//--Sensor-Read-Out------------------------------------
//-----------------------------------------------------

void Fun_CO2Sensor(){

CO2Sensor.write(Com_ReadCO2,9);

byte OutputData[9]; // Define an array to collect the "Receiving command"

for(int n=0;n<9; n++)
{
OutputData[i] = CO2Sensor.read();   
}

int High = OutputData[2];
int Low = OutputData[3];

Serial.print("HIGH = ");
Serial.println(High);
Serial.print("LOW = ");
Serial.println(Low);

int CO2_concentration = High * 256 + Low;

Serial.print("CO2 concentration (ppm) = ")
Serial.println(CO2_concentration); 
}

In the loop we call the previously created function to read the CO2 concentration:

void loop() {
// put your main code here, to run repeatedly:
Fun_CO2Sensor(); // Call the function to read the CO2 value
delay(30000); // A delay of 30 seconds (30 000 ms)
}

From expierence I have learned that asking for a value with intervals smaller than 30 seconds is not very useful. Moreover, in the datasheet a T90 of less than 60 seconds is specified. This means that it takes less than a minute for the sensor to read 90% of the applied concentration.

Complete script

The complete script looks like this:

#include <SoftwareSerial.h>
SoftwareSerial CO2Sensor(D3, D4); // SoftwareSerial(rxPin, txPin)

unsigned char Com_ReadCO2[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79}; //Command to read CO2 concentration


void setup() {
// put your setup code here, to run once:
Serial.begin(9600); // We open the serial connection with a Baudrate of 9600
CO2Sensor.begin(9600); // We open a serial connection with the sensor with a Baudrate of 9600
}

void loop() {
// put your main code here, to run repeatedly:
Fun_CO2Sensor(); // Call the function to read the CO2 value
delay(30000); // A delay of 30 seconds (30 000 ms)
}


//-----------------------------------------------------
//--Sensor-Read-Out------------------------------------
//-----------------------------------------------------

void Fun_CO2Sensor(){

CO2Sensor.write(Com_ReadCO2,9);

byte OutputData[9]; // Define an array to collect the "Receiving command"

for(int n=0;n<9; n++)
{
OutputData[n] = CO2Sensor.read();   
}

int High = OutputData[2];
int Low = OutputData[3];

Serial.print("HIGH = ");
Serial.println(High);
Serial.print("LOW = ");
Serial.println(Low);

int CO2_concentration = High * 256 + Low;

Serial.print("CO2 concentration (ppm) = ");
Serial.println(CO2_concentration); 
}

Improvements to the script

We can improve the script by adding a couple of things. Ideally a checksum is added to the script to verify integrity of the received command. The manual provides the algorithm to calculate the checksum, see Figure 3 below.


Figure 3: Command to calculate checksum

The code looks like this (call this function in the loop after void Fun_CO2Sensor()):

//-----------------------------------------------------
//--Checksum-------------------------------------------
//-----------------------------------------------------

void Checksum(byte OutputData[9]){

byte checksum;

for( int i = 1; i < 8; i++)
{
checksum += OutputData[i]; // From Byte 1 to Byte 7: 0x01 + 0x86 + 0x00 + 0x00 + 0x00 + 0x00 + 0x00 = 0x87
}

checksum = 0xff - checksum; // Negative: 0xFF - 0x87 = 0x78
checksum += 1; // Then+1:0x78 + 0x01 = 0x79

if(checksum = OutputData[8]){
Serial.println("Checksum correct");
}
else{
Serial.println("Checksum Error");
}

}

Preferabbly, serial.read() is only preferred when the serial buffer is filled (else there is nothing to read). We can therefore add Serial.available command to the script in the following manner:

if (CO2Sensor.available() > 0) {
"Code to read sensor"
}