In preparation for a student project to monitor the CO2 levels in a number of classrooms I purchased a Grove – Carbon Dioxide Sensor(MH-Z16) for evaluation.

Arduino Uno R3 and CO2 Sensor
I downloaded the seeedstudio wiki example code, compiled and uploaded it to one of my Arduino Uno R3 devices.
I increased delay between readings to 10sec and reduced the baud rate of the serial logging to 9600baud.
/*
This test code is write for Arduino AVR Series(UNO, Leonardo, Mega)
If you want to use with LinkIt ONE, please connect the module to D0/1 and modify:
// #include <SoftwareSerial.h>
// SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor Serial1
*/
#include <SoftwareSerial.h>
SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor s_serial
const unsigned char cmd_get_sensor[] =
{
0xff, 0x01, 0x86, 0x00, 0x00,
0x00, 0x00, 0x00, 0x79
};
unsigned char dataRevice[9];
int temperature;
int CO2PPM;
void setup()
{
sensor.begin(9600);
Serial.begin(9600);
Serial.println("get a 'g', begin to read from sensor!");
Serial.println("********************************************************");
Serial.println();
}
void loop()
{
if(dataRecieve())
{
Serial.print("Temperature: ");
Serial.print(temperature);
Serial.print(" CO2: ");
Serial.print(CO2PPM);
Serial.println("");
}
delay(10000);
}
bool dataRecieve(void)
{
byte data[9];
int i = 0;
//transmit command data
for(i=0; i<sizeof(cmd_get_sensor); i++)
{
sensor.write(cmd_get_sensor[i]);
}
delay(10);
//begin reveiceing data
if(sensor.available())
{
while(sensor.available())
{
for(int i=0;i<9; i++)
{
data[i] = sensor.read();
}
}
}
for(int j=0; j<9; j++)
{
Serial.print(data[j]);
Serial.print(" ");
}
Serial.println("");
if((i != 9) || (1 + (0xFF ^ (byte)(data[1] + data[2] + data[3] + data[4] + data[5] + data[6] + data[7]))) != data[8])
{
return false;
}
CO2PPM = (int)data[2] * 256 + (int)data[3];
temperature = (int)data[4] - 40;
return true;
}
The debug output wasn’t too promising there weren’t any C02 parts per million (ppm) values and the response payloads looked wrong. So I downloaded the MH-Z16 NDIR CO2 Sensor datasheet for some background. The datasheet didn’t mention any temperature data in the message payloads so I removed that code.
The response payload validation code was all on one line and hard to figure out what it was doing.
if((i != 9) || (1 + (0xFF ^ (byte)(data[1] + data[2] + data[3] + data[4] + data[5] + data[6] + data[7]))) != data[8])
{
return false;
}
To make debugging easier I split the payload validation code into several steps so I could see what was failing.
/*
This test code is write for Arduino AVR Series(UNO, Leonardo, Mega)
If you want to use with LinkIt ONE, please connect the module to D0/1 and modify:
// #include <SoftwareSerial.h>
// SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor Serial1
*/
#include <SoftwareSerial.h>
SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor s_serial
const unsigned char cmd_get_sensor[] =
{
0xff, 0x01, 0x86, 0x00, 0x00,
0x00, 0x00, 0x00, 0x79
};
unsigned char dataRevice[9];
int CO2PPM;
void setup()
{
sensor.begin(9600);
Serial.begin(9600);
Serial.println("get a 'g', begin to read from sensor!");
Serial.println("********************************************************");
Serial.println();
}
void loop()
{
if(dataRecieve())
{
Serial.print(" CO2: ");
Serial.print(CO2PPM);
Serial.println("");
}
delay(10000);
}
bool dataRecieve(void)
{
byte data[9];
int i = 0;
//transmit command data
for(i=0; i<sizeof(cmd_get_sensor); i++)
{
sensor.write(cmd_get_sensor[i]);
}
delay(10);
//begin reveiceing data
if(sensor.available())
{
while(sensor.available())
{
for(int i=0;i<9; i++)
{
data[i] = sensor.read();
}
}
}
for(int j=0; j<9; j++)
{
Serial.print(data[j]);
Serial.print(" ");
}
Serial.println("");
// First calculate then validate the check sum as there is no point in proceeding if the packet is corrupted. (code inspired by datasheet algorithm)
byte checksum = 0 ;
for(int j=1; j<8; j++)
{
checksum += data[j];
}
checksum=0xff-checksum;
checksum+=1;
if (checksum != data[8])
{
Serial.println("Error checksum");
return false;
}
// Then check the start byte to make sure response is what we were expecting
if ( data[0] != 0xFF )
{
Serial.println("Error start byte");
return false;
}
// Then check the command byte to make sure response is what we were expecting
if ( data[1] != 0x86 )
{
Serial.println("Error command");
return false;
}
CO2PPM = (int)data[2] * 256 + (int)data[3];
return true;
}
From these modifications I could see the payload was messed up and based on the datasheet message descriptions it looked like it was offset by a byte or two.
15:58:32.509 -> get a 'g', begin to read from sensor!
15:58:32.578 -> ********************************************************
15:58:32.612 ->
15:58:32.612 -> 255 134 6 238 76 0 0 1 255
15:58:32.647 -> Error checksum
15:58:42.631 -> 57 255 134 6 246 76 0 0 1
15:58:42.666 -> Error checksum
15:58:52.667 -> 49 255 134 5 125 76 0 0 1
15:58:52.702 -> Error checksum
15:59:02.704 -> 171 255 134 4 86 76 0 0 1
15:59:02.750 -> Error checksum
I had a look at the code and the delay(10) after sending the sensor reading request message caught my attention. I have found that often delay(x) commands are used to “tweak” the code to get it to work.
These “tweaks” often break when code is run on a different device or sensor firmware is updated changing the timing of individual bytes, or request-response processes.
I removed the delay(10) replaced it with a serial.flush() and changed the code to display the payload bytes in hexadecimal.
/*
This test code is write for Arduino AVR Series(UNO, Leonardo, Mega)
If you want to use with LinkIt ONE, please connect the module to D0/1 and modify:
// #include <SoftwareSerial.h>
// SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor Serial1
*/
#include <SoftwareSerial.h>
SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor s_serial
const unsigned char cmd_get_sensor[] =
{
0xff, 0x01, 0x86, 0x00, 0x00,
0x00, 0x00, 0x00, 0x79
};
unsigned char dataRevice[9];
int CO2PPM;
void setup()
{
sensor.begin(9600);
Serial.begin(9600);
Serial.println("get a 'g', begin to read from sensor!");
Serial.println("********************************************************");
Serial.println();
}
void loop()
{
if(dataRecieve())
{
Serial.print(" CO2: ");
Serial.print(CO2PPM);
Serial.println("");
}
delay(10000);
}
bool dataRecieve(void)
{
byte data[9];
int i = 0;
//transmit command data
for(i=0; i<sizeof(cmd_get_sensor); i++)
{
sensor.write(cmd_get_sensor[i]);
}
Serial.flush();
//begin reveiceing data
if(sensor.available())
{
while(sensor.available())
{
for(int i=0;i<9; i++)
{
data[i] = sensor.read();
}
}
}
for(int j=0; j<9; j++)
{
Serial.print(data[j],HEX);
Serial.print(" ");
}
Serial.println("");
// First calculate then validate the check sum as there is no point in proceeding if the packet is corrupted. (code inspired by datasheet algorithm)
byte checksum = 0 ;
for(int j=1; j<8; j++)
{
checksum += data[j];
}
checksum=0xff-checksum;
checksum+=1;
if (checksum != data[8])
{
Serial.println("Error checksum");
return false;
}
// Then check the start byte to make sure response is what we were expecting
if ( data[0] != 0xFF )
{
Serial.println("Error start byte");
return false;
}
// Then check the command byte to make sure response is what we were expecting
if ( data[1] != 0x86 )
{
Serial.println("Error command");
return false;
}
CO2PPM = (int)data[2] * 256 + (int)data[3];
return true;
}
The initial values from the sensor were a bit high, but after leaving the device running for 3 minutes (Preheat time in the documentation) they settled down into a reasonable range
16:14:31.686 -> get a 'g', begin to read from sensor!
16:14:31.721 -> ********************************************************
16:14:31.789 ->
16:14:31.789 -> 255 134 6 224 75 0 0 1 72
16:14:31.823 -> CO2: 1760
16:14:41.824 -> 255 134 6 224 75 0 0 1 72
16:14:41.824 -> CO2: 1760
16:14:51.824 -> 255 134 5 189 75 0 0 1 108
16:14:51.858 -> CO2: 1469
16:15:01.868 -> 255 134 3 157 75 0 0 1 142
16:15:01.868 -> CO2: 925
16:15:11.857 -> 255 134 3 223 75 0 0 1 76
16:15:11.892 -> CO2: 991
16:15:21.882 -> 255 134 6 56 75 0 0 1 240
16:15:21.917 -> CO2: 1592
16:15:31.911 -> 255 134 4 186 75 0 0 1 112
16:15:31.945 -> CO2: 1210
16:15:41.927 -> 255 134 3 131 75 0 0 1 168
16:15:41.962 -> CO2: 899
16:15:51.940 -> 255 134 3 30 75 0 0 1 13
16:15:51.975 -> CO2: 798
16:16:01.986 -> 255 134 2 201 75 0 0 1 99
16:16:01.986 -> CO2: 713
16:16:11.985 -> 255 134 4 133 75 0 0 1 165
16:16:12.019 -> CO2: 1157
16:16:22.020 -> 255 134 6 62 75 0 0 1 234
16:16:22.053 -> CO2: 1598
16:16:32.041 -> 255 134 5 80 75 0 0 1 217
16:16:32.041 -> CO2: 1360
16:16:42.057 -> 255 134 3 204 75 0 0 1 95
16:16:42.092 -> CO2: 972
16:16:52.084 -> 255 134 3 191 75 0 0 1 108
16:16:52.084 -> CO2: 959
16:17:02.102 -> 255 134 2 230 75 0 0 1 70
16:17:02.102 -> CO2: 742
16:17:12.094 -> 255 134 3 106 75 0 0 1 193
16:17:12.129 -> CO2: 874
16:17:22.111 -> 255 134 2 227 75 0 0 1 73
16:17:22.145 -> CO2: 739
16:17:32.139 -> 255 134 3 225 75 0 0 1 74
16:17:32.172 -> CO2: 993
16:17:42.170 -> 255 134 3 109 75 0 0 1 190
16:17:42.204 -> CO2: 877
16:17:52.174 -> 255 134 2 188 75 0 0 1 112
16:17:52.207 -> CO2: 700
16:18:02.218 -> 255 134 2 70 75 0 0 1 230
16:18:02.253 -> CO2: 582
16:18:12.239 -> 255 134 2 163 75 0 0 1 137
16:18:12.239 -> CO2: 675
16:18:22.251 -> 255 134 2 110 75 0 0 1 190
16:18:22.285 -> CO2: 622
16:18:32.246 -> 255 134 2 83 75 0 0 1 217
16:18:32.280 -> CO2: 595
16:18:42.277 -> 255 134 2 48 75 0 0 1 252
16:18:42.312 -> CO2: 560
16:18:52.305 -> 255 134 2 62 75 0 0 1 238
16:18:52.339 -> CO2: 574
Bill of materials (prices as at Jan 2019)
- Arduino Uno R3 USD24.95 NZD54
- Grove Base shield V2 USD4.45 NZD14
- Grove – Carbon Dioxide Sensor(MH-Z16) USD89
After these tentative fixes for the MH-Z16 sensor I think going to see if there are any other libraries written by someone smarter than me available.
thank you so much for writing down your process !
I am going to use your code as a starting point for my master thesis. ❤
Hi
Are you using the MH-Z16 or SCD30 sensor?
I found the SCD30 worked well (plus the temperature & humidity values were useful) and was a bit cheaper though the multi-day calibration process was a pain.
Keep us posted on your progress…
@KiwiBryn
I am using MH-Z16.
Currently trying to combine the CO2 Sensor (MH-Z16) with a Dust-Sensor (Seeed HM330X) with the Seeedstudio SD-Card Shield. Already running into multiple problems:
-MH-Z16 wants Baud 9600
-HM300x wants Baud 115200
-SD-Shield data logging didn’t work (solved)
-dynamic Memory usage ~95%
-program storage usage ~90%
-GPS Shield still not tested
I now ordered the Seeeduino Cortex-M0+ because it offers more RAM.
And these are all stationary problems; I need the setup to fit into an airtight case with an air pump and battery.
I will post a link below to open up all my code with pictures on GitHub as soon as I feel confident. ❤
I’m just getting started on a Co2 probe project and had a question regarding connections to the MH-Z16 probe. Noticed you had a 4 wire connection from the probe to the Grove Base Shield. probe has 7 pins . Mine will ship with a 7 pin cable , please explain what jumper you used . I also have a Sandbox MH-Z16 module that allows 12c/UART connections, maybe i don’t even need this ?
Thanks
Hi
On the manufacturer’s site (https://www.winsen-sensor.com/sensors/co2-sensor/mh-z16.html) their datasheet details the pins on the end of the sensor.
Pad1 HD
Pad2 Vout (0.4~2V,custom made)
Pad3 GND
Pad4 Vin(input voltage 4.5V~5.5V)
Pad5 UART(RXD) 0~3.3V input digital
Pad6 UART(TXD) 0~3.3V output digital
Pad7 PWM
The Grove cable has pad 3 GND (black wire), pad 4 Vin (red wire), pad 5 UART RXD (white wire) and pad 6 UART TXD (yellow wire).
You should be able to connect your sensor up using the same wiring and use one of the serial libraries discussed in my blog.
@KiwiBryn
Hi
Thank you very much for sharing your process
I am a french teacher and it was impossible for me to obtain something with the CO2 Sensor (MH-Z16)
Now, with your help it seems good with CO2 but the temperature is always 27!
with this probe (the real temperature is 19)
Is there another problem?
Is it necessary to calibrate this expensive probe ?
Best regards
Been busy will dig out my sensor and have a look at the library. I uses these https://www.seeedstudio.com/Grove-CO2-Temperature-Humidity-Sensor-SCD30-p-2911.html mostly.