Friday, March 9, 2018

Review and Test of the MH-Z14A NDIR CO2 Sensor

The MH-Z14A measures CO2 concentrations using absorption of infra-red light. These devices are available on eBay, Amazon and other places from about $20.

Ignore the capacitor across the power pins - this was a failed attempt by me to improve the reliability of the analog outputs.

My initial impressions of the module were pretty favourable. It looks pretty space-age with its gold coated chamber and 4 LEDs in the corner of the fabric window that allows the air to enter. These LEDs blink once a second as the readings are taken.

You can find a datasheet for the module here.

The device is available configured for different ranges of CO2 concentration. This module uses the most common range of 0 to 5000ppm.


The module actually has three interfaces - described in the datasheet.

  • Analog output - proportional to the ppm of CO2 range of 0.4 to 2V for full range.
  • PWM output - very slow PWM over the period of a second, with the HIGH duration being proportional to the ppm of CO2.
  • 9600 baud 3V/5V TTL UART interface. You send a message saying you want a reading, it responds with a reading.


The module has lots of pins on it, most of which are duplicates. I just stuck to the main 0.1inch pitch 2 x 6 connector on the right. Note that the pin numbering on the board reflects the evolution of various connectors added through versions of the boards, and is therefore a bit odd.

First Use

My first experiment was just to power-up the board (5V to pin1, GND to pin 2) with a multimeter on the analog output (pin 4).

As soon as it powered up it was obvious that every second or so, the LEDs behind the fabric would light up and the current would surge to around 100mA. This was very short in duration so I expect the average power consumption of this unit is actually pretty low.

The analog output on pin 4 indicated 0.85V using the datasheet the ppm is calculated as:

ppm = (Vout - 0.4) * (5000 / 1.6)

In this case a ppm of 1406. This was wrong, I was in a big room with the door open and not breathing on the sensor, so I would expect a reading of around 400ppm.

It looked like the device was not calibrated. Or, calibrated in a somewhat unhealthy factory.


You can calibrate by sending a serial message (see later) or by connecting pin 8 to GND for at least 7 seconds. Using the pin 8 methods, the new voltage reading was a much more believable 0.52V indicating a plausible 375 ppm.

After huffing on the sensor for a bit, it was easy to get the readings to go up and then work their way back down to ambient levels after about 5 minutes.

Its looking good!


The next step was to hook the device up to an Arduino Uno so that I could compare the reading methods and also do a bit of logging.

Here is the test script that displays the readings from all three output methods in the Arduino Serial Monitor once every 10 seconds.

#include <SoftwareSerial.h>

const int analogPin = A0;
const int pwmPin = 9;

const long samplePeriod = 10000L;

SoftwareSerial sensor(10, 11); // RX, TX

const byte requestReading[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
byte result[9];
long lastSampleTime = 0;

void setup() {
  pinMode(pwmPin, INPUT_PULLUP);

void loop() {
  long now = millis();
  if (now > lastSampleTime + samplePeriod) {
    lastSampleTime = now;
    int ppmV = readPPMV();
    int ppmS = readPPMSerial();
    int ppmPWM = readPPMPWM();

int readPPMV() {
  float v = analogRead(analogPin) * 5.0 / 1023.0;
  int ppm = int((v - 0.4) * 3125.0);
  return ppm;

int readPPMSerial() {
  for (int i = 0; i < 9; i++) {
  //Serial.println("sent request");
  while (sensor.available() < 9) {}; // wait for response
  for (int i = 0; i < 9; i++) {
    result[i] = sensor.read(); 
  int high = result[2];
  int low = result[3];
    //Serial.print(high); Serial.print(" ");Serial.println(low);
  return high * 256 + low;

int readPPMPWM() {
  while (digitalRead(pwmPin) == LOW) {}; // wait for pulse to go high
  long t0 = millis();
  while (digitalRead(pwmPin) == HIGH) {}; // wait for pulse to go low
  long t1 = millis();
  while (digitalRead(pwmPin) == LOW) {}; // wait for pulse to go high again
  long t2 = millis();
  long th = t1-t0;
  long tl = t2-t1;
  long ppm = 5000L * (th - 2) / (th + tl - 4);
  while (digitalRead(pwmPin) == HIGH) {}; // wait for pulse to go low
  delay(10); // allow output to settle.
  return int(ppm);

Here is a sample of the results from the Arduino Serial Monitor.

The first column is the analog reading, the middle column from the PWM interface and the right column from the serial interface.

The analog readings are all over the place. I don't think this interface is useable.

UPDATE: The analog output actually appears to be unbuffered. Using my DMM which has a Zin of about 10MΩ the voltage readings were within a couple of % of the serial readings. It was the relatively low Zin of the Arduino ADCs sample and hold messing things up. So, a unity gain op-amp buffer should do the trick.

The PWM interface is pretty consistent with the UART interface, although every so often there will be a bad reading.

The Serial interface is clearly the winner and if you implement the checksum algorithm specified in the datasheet I would expect 100% reliable readings.


To give the module a long term test, I set it running overnight in out bedroom with the following somewhat worrying results.

The y-axis is ppm of CO2 and the x-axis time (1 sample per minute).

I used the PWM measurement method and you can see the frequent misreadings (perhaps 1 in 50). But the trend is pretty good, showing the ppm rising well above 4000ppm, before the bedroom door was opened in the morning and the readings fell back to ambient values of 400ppm or so. Yes, we were all still alive!

The high overnight readings seem worryingly high for me, but at the moment, I do not have a calibrated sensor to compare with.


The sensor module's Serial interface works well and the PWM interface acceptably but the analog interface is only useful if buffered.

A serial interface that it both 5V and 3V compatible makes the module suitable for use with a Raspberry Pi or Arduino without the need for level conversion.

I cannot comment on the accuracy of the sensor without comparison to a trusted device. When I get one, I will come back here for an update.

No comments: