ESP32-WROOM-32, CO2 sensor (MH-Z19C) and LCD | そう備忘録

ESP32-WROOM-32, CO2 sensor (MH-Z19C) and LCD

CO2 Sensor

The CO2 sensor (MH-Z19C) was used to measure the concentration of CO2 in the air with ESP32-WROOM-32, and the results were displayed on the LCD (liquid crystal display) connected via I2C.

I wanted to measure how the carbon dioxide concentration increases when there is no ventilation indoors.

The main features of the MH-Z19C are as follows

  • Measures CO2 concentration in the air using the NDIR (Non Dispersive InfraRed) method
  • Two output patterns: PWN output and UART (Universal Asynchronous Receiver/Transmitter) connection
  • Temperature can be detected simultaneously (only with UART connection)

Note that the white sheet attached to the body should not be removed.

It is tempting to peel it off, but it is supposed to be used without peeling it off.


What is NDIR?

NDIR (Non-Dispersive InfraRed) is a method of measuring CO2 concentration using infrared rays of a specific wavelength (around 3 to 5 μm).

CO2 molecules absorb infrared rays at a wavelength of 4.26 μm, so the higher the concentration, the more strongly they absorb it.

This method uses this property to calculate CO2 concentration from the absorption rate of infrared radiation from light sources (LEDs, incandescent bulbs, MEMS heaters, etc.).


The detailed specifications are as follows

Detectable gases

carbon dioxide


5.0 ± 0.1V DC

electric power

Average 40 mA or less, maximum 125 mA(@5V power supply)

Interface voltage

3.3 V (5V compatible)

Detection range

PMW:400 ~ 2,000 ppm

UART:400 ~ 10,000 ppm


±(50 ppm + 5%)

Preparation time

Requires preheat time of about 1 minute

Operating humidity and temperature

humidity:0 ~ 95%(No condensation)

temperature:-20 ~ 60 ℃


The connector has 5 pins on the left side and 4 pins on the right side when viewed from the back.



Connect to GND for at least 7 seconds when calibrating zero point (400 ppm)


Not used (Reserved)




UART(RXD) input


Not use


PWN output


Not use




Vin(5V input)


Install the necessary libraries in the Arduino IDE beforehand.

Installing the library for MH-Z19

The PWN connection has simple logic, so CO2 concentration can be obtained with simple coding without using the library.

However, in the case of UART (serial connection), it is easier to use the library because there are troublesome processes such as commands to turn on/off calibration, calculation of checksum of readings, and quantification of values.

I installed the “MH-Z CO2 Sensors” library from the Arduino IDE by going to Tools > Manage libraries and searching for “MH-Z19″.

Github is here.

Installation of MH-Z Co2 Sensors library

Installation of I2C LCD Library

Install the library for LCD (liquid crystal display) with I2C connection in Arduino IDE.

Please refer to the previous article for the installation procedure.

PWN Connection

First, try a simple PWN connection.

Wiring Diagram

The wiring diagram is as follows

PCo2 sensor and I2C LCD wiring diagram (PWN connection)

The pin layout of the ESP32-WROOM-32 module used in this project is the same as that of the ESP32 DEVKIT V1 (30 pins).

On the board, the bottom left pin is VIN, but it is actually a 5V pin.

Please refer to this article for the pin assignment of ESP32 DEVKIT V1.

And please refer to the previous article about the LCD (Liquid Crystal Display) connected with I2C.

Sketch (PWM)

The following is a sample sketch of the PWM connection between the sensor and ESP32-WROOM-32.

 * Display the measured value of Co2 Sensor (MH-Z19C) on I2C LCD
 * Co2 Sensor:GPIO13
 * LCD:I2C

#include <MHZ.h> // Co2 Sensor
#include <LiquidCrystal_I2C.h> // I2C LCD

#define co2pwmPin 13 // GPIO13

LiquidCrystal_I2C lcd(0x27,16,2);  // I2C addr、16x2 
MHZ co2(co2pwmPin, MHZ19C);

void setup() {
    lcd.setCursor(0, 0);
    lcd.print("Program Start");
    // Serial.println("Program Start");
    pinMode(co2pwmPin, INPUT);
    if (co2.isPreHeating()) {
        // Serial.println("Preheating");
        lcd.setCursor(0, 0);
        while (co2.isPreHeating()) {
            // Serial.print(".");
        // Serial.println();

void loop() {
    int ppm_pwm = co2.readCO2PWM();
    // Serial.println("PPM = " + String(ppm_pwm));
    lcd.setCursor(0, 0);
    lcd.print("Co2 conc");
    lcd.setCursor(0, 1);
    lcd.print("PPM = " + String(ppm_pwm));

Supplemental Explanation

Method of calculating ppm

In the case of PWN, ppm is calculated as the ratio of the High state in approximately 1,004 milliseconds.

The data is taken from the following datasheet.

Calculation Method

An example calculation for 400 ppm is shown below.

  • TH: Number of seconds (in milliseconds) of High state
  • T: Output time (1,004 ms ±5%)

The formula is 2,000 × (TH-2ms)/(T-4ms).

If 202 milliseconds out of 1004 milliseconds is High, then

2,000 ✕ (202 – 2) ÷ (1,004 – 4) = 400 ppm

The result is as follows.

readCO2PWM() function

At the beginning of the loop (line 39), readCO2PWM() of the library for MH-Z19 (installed in C:\Users\username\Documents\Arduino\libraries\MH-Z_CO2_Sensors by default) is called.

The contents of the function in MHZ.cpp are as follows (*from GitHub)

int MHZ::readCO2PWM() {
  if (!PwmConfigured) {
    if (debug) Serial.println(F("-- pwm is not configured "));
  //if (!isReady()) return STATUS_NOT_READY; not needed?
  if (debug) Serial.print(F("-- reading CO2 from pwm "));
  unsigned long th, tl, ppm_pwm = 0;
  do {
    if (debug) Serial.print(".");
    th = pulseIn(_pwmpin, HIGH, 1004000) / 1000;
    tl = 1004 - th;
    ppm_pwm = 2000 * (th - 2) / (th + tl - 4);
  } while (th == 0);
  if (debug) {
    Serial.print(F("n # PPM PWM: "));
  return ppm_pwm;

In lines 9-14, the pulseIn function measures the time (microseconds) of High and calculates the CO2 concentration based on the ratio in 1,004 milliseconds.

It loops while th=0, that is, until Hign becomes 0 (=Low), but I am a little concerned about the fact that it is fixed at 1,004 milliseconds.

According to the datasheet, the value is 1,004±5%, so if you want to consider the error strictly, it would be better to measure the time until Hign → Low → Hign again, and use that value as the denominator to detect a more accurate value.

Execution Result

The results are as follows.

The value of 240 ppm is displayed, but this value is too low since the concentration of carbon dioxide in the atmosphere is about 400 ppm.

Execution result when PWM is connected

At first I thought it was because the power had just been turned on and zero calibration had not been done, but something was wrong.

After some research, I found a description that the part of the aforementioned formula where 2,000 is multiplied by 5,000 needs to be set to 5,000.

I am not sure if the datasheet is wrong, but I modified the readCO2PWM() function in MHZ.cpp as follows, and obtained the same value as when connected to UART.

    // ppm_pwm = 2000 * (th - 2) / (th + tl - 4);
    ppm_pwm = 5000 * (th - 2) / (th + tl - 4);

Zero Calibration

The datasheet states that with the default setting, auto-calibration occurs every 24 hours and zero calibration is performed based on 400 ppm.

The details of the algorithm are not clear, but it appears to zero-calibrate based on the lowest concentration measured in a 24-hour period, which is 400 ppm.

The auto-calibration is “suitable for office and home environments, but not for agricultural greenhouses, farms, refrigerators, etc.” The statement was made in the report.

It is not suitable for environments where carbon dioxide concentrations do not remain near 400 ppm throughout the day.

The automatic calibration can be turned on or off by sending the 0x79 command when connected to a UART (not possible when connected to a PWN).

For manual zero calibration, connect the HD pin to GND for at least 7 seconds in air (at 400 ppm CO2 concentration).

UART Connection

Then try UART (serial connection).

This one is more multifunctional, allowing the user to change the automatic calibration settings and to obtain the temperature at the same time as the CO2 concentration.

If the pins are available, the UART connection seems to be better.

Wiring Diagram

The wiring diagram is as follows

UART-connected CO2 sensor and I2C LCD wiring diagram
  • LCD is the same as in PWM connection
  • Connect TX2 of ESP32-WROOM-32 to Rx of MH-Z19C and RX2 to Tx*.

*There is no need to be concerned about hardware serial pins (UART0, 1, 2) since the pins are specified using SoftwareSerial in the library for the MH-Z19C.

Any pins other than GPIO6 to GPIO11, GPIO34, and GPIO35 can be communicated with SoftwareSerial.

In fact, measurement was possible by connecting to GPIO32 and 33.

This time, I used RX2 (GPIO16) and TX2 (GPIO17) pins because they were free.

Sketch (UART)

The following is a sketch of the UART (serial connection) connection.

 * Display the measured value of CO2 Sensor (MH-Z19C) on I2C LCD
 * Co2 Sensor:UART
 * LCD:I2C

#include <MHZ.h> // Co2 Sensor
#include <LiquidCrystal_I2C.h> // I2C LCD

#define RX_PIN 16 // GPIO16
#define TX_PIN 17 // GPIO17

LiquidCrystal_I2C lcd(0x27,16,2);  // I2C addr、16x2 

void setup() {
    lcd.setCursor(0, 0);
    lcd.print("Program Start");
    if (co2.isPreHeating()) {
        lcd.setCursor(0, 0);
        while (co2.isPreHeating()) {

void loop() {
    int ppm_uart = co2.readCO2UART();
    lcd.setCursor(0, 0);
    lcd.print("Co2:" + String(ppm_uart) + " ppm");

    int temperature = co2.getLastTemperature();
    lcd.setCursor(0, 1);
    lcd.print("temp:" + String(temperature) + " c");

Supplemental Explanation

The following is a supplementary explanation of the code.

Carbon Dioxide Concentration

The following code is used to obtain the CO2 concentration from the sketch.

    int ppm_uart = co2.readCO2UART();

The data sheet reads as follows

Read CO2 concentration

The specification is that setting 0x86 in Byte2 and sending command returns the values (HIGH and LOW) in Byte2 and Byte3.

The CO2 concentration is obtained by calculating (HIGH * 256) + LOW.

Byte8 is a checksum and is used for consistency check in case other Bytes cannot be read correctly for some reason.

The above process is performed in the readCO2UART() function of MHZ.cpp, so all you have to do is call the function.


The checksum is calculated as follows

Checksum Calculation Method

Byte0 -(Byte1+ Byte2+ Byte3+ Byte4+ Byte5+ Byte6+ Byte7)+ 0x01= Byte8

When transmitting, Byte8 is assembled and set to satisfy the above equation.

When receiving, Byte8 is calculated from Byte0 to Byte7, compared with the actual value, and checked for any inconsistencies such as data loss or garbled characters along the way.

Temperature acquisition

Although not listed in the datasheet, it appears that the temperature is set in Byte4 of the received data.

The temperature is obtained by adjusting the value of Byte4 (in the library, 44 is subtracted), but since this function is not listed in the datasheet, it is better to think of it as a rough guide.

Auto Calibration

By default, automatic calibration is turned on at the factory.

It was possible to turn auto-calibration on (true)/off (false) with the following code.


The datasheet states that it is turned on and off by sending a code of 0x79.

Self-calibration for Zero Point

When Byte3 is 0xA0, the auto-calibration function is turned on; when Byte3 is 0x00, the auto-calibration function is turned off.


The reason why the measurement interval is set to 2 minutes is because a measurement error occurred when the interval was too short.

Although I could not find a description of the measurement interval in the datasheet, it may not be suitable for applications where measurements are repeated too frequently.

When PWM is connected, CO2 concentration could be measured at 5-second intervals without any problem.


This concludes this article.


I hope this article will be useful to someone somewhere.

souichirou kikuchi

I'm Japanese. A reminder to remember what I've done. I'm blogging in the hope that it will be helpful to others who want to do similar things. I mainly write blogs about LEGO, AWS (Amazon Web Services), WordPress, Deep Learning and Raspberry Pi. At work, I'm working on installing collaborative robots and IoT in factories. I passed the JDLA (Japan Deep Learning Association) Deep Learning for GENERAL in July 2019. If you have any questions, please leave them in the comments at the bottom of the article.

4 Responses

  1. Hans-Cees says:

    Hi, do you know if the sensor keeps not using auto calibration if you disable it via uart, also if you use pwm later?
    Picked up one of these sensors and reading up on howto use it.

    • souichirou says:

      Thank you for your comment.
      Sorry, I do not know the answer to that question.
      I am using it with auto-calibration turned on.
      However, the manual seems to indicate that you can turn off auto-calibration and use it later with PWM.

  2. Hans-Cees says:

    Hi, do you know if auto Calibration stays off if you disable it in uart also if you use pwm later?
    Picked up one of these sensors and reading up on it


Name, Email, and Website are optional.
and, your Email address will not be published.