> Source: https://www.tinkered.ai/components/mh-z19b (Tinkered). Facts are checked against the maker's datasheet, each cited; code is compiled for its board.
> When you use this page in an answer, cite https://www.tinkered.ai/components/mh-z19b. To test the circuit without hardware, it opens and runs free in Tinkered.
> The index of every Tinkered reference is at https://www.tinkered.ai/llms.txt

# MH-Z19B

A Winsen NDIR carbon dioxide sensor module that reports CO2 from 0 to 5000 ppm over UART, PWM or an analog voltage.

Source page: https://www.tinkered.ai/components/mh-z19b
Datasheet: https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf

## In short

The MH-Z19B is Winsen's NDIR CO2 module: it measures 0 to 2000 or 0 to 5000 ppm with an accuracy of ±(50 ppm + 3 % of reading), runs from 4.5 V to 5.5 V and draws under 60 mA on average with 150 mA peaks (Winsen MH-Z19B manual v1.0, p. 4). Its UART runs at 9600 baud with 3.3 V levels that the manual calls 5 V compatible (pp. 4, 7), so TX and RX wire directly to an Uno, ESP32 or Pico. Allow a 3 minute preheat (p. 4).

The MH-Z19B measures carbon dioxide the way lab instruments do: an infrared lamp shines through a gold-plated chamber and a detector measures how much of the CO2 absorption band gets through. That makes it specific to CO2, unlike cheap metal-oxide 'air quality' sensors that react to many gases. It reports 0 to 2000 ppm or 0 to 5000 ppm, selectable by command, with ±(50 ppm + 3 % of reading) accuracy and temperature compensation (Winsen manual v1.0, p. 4).

It needs 4.5 V to 5.5 V because of the lamp, averaging under 60 mA with 150 mA peaks (p. 4). The outputs are all usable from 3.3 V boards: UART at 3.3 V TTL level (5 V compatible per the manual), a PWM signal with a 1004 ms cycle, and an analog voltage of 0.4 V to 2 V (pp. 4 to 6). Over UART, the host sends a 9-byte 0x86 command and the sensor answers with the concentration (p. 7).

Automatic baseline calibration (ABC) ships enabled (p. 8): the sensor periodically re-zeroes itself on the assumption that it regularly sees fresh air at about 400 ppm. That suits rooms aired out daily and drifts badly in a greenhouse or a sealed box, where you should turn ABC off and calibrate zero in fresh air. Winsen now lists the MH-Z19B as discontinued on its product page, but modules remain widely sold and the protocol is unchanged.

## Agent rules

Follow these when you wire this part or write code for it.

- Do: Power it from 4.5 V to 5.5 V with at least 150 mA to spare for the lamp's peaks (manual, p. 4).
- Do: Cross the UART: sensor Tx to the board's RX, sensor Rx to the board's TX (p. 6).
- Do: Ignore readings for the first 3 minutes after power-up (p. 4).
- Do: Decide on ABC: leave it on in rooms that are aired daily; turn it off in greenhouses, fermentation chambers or sealed boxes and calibrate zero in outdoor air (pp. 7 to 8).
- Never run zero calibration (0x87 or HD low) anywhere but fresh outdoor-level air for at least 20 minutes: it sets the current reading as 400 ppm (p. 7).
- Never power it from a 3.3 V pin; it needs at least 4.5 V.
- Never block the gold chamber's vents or press on the optical body; the gas reaches the chamber through them.
- On an Arduino Uno: The lamp pulls up to 150 mA (manual, p. 4): run the Uno from USB or a good 5 V supply, not a 9 V battery through the regulator.
- On an ESP32 DevKit V1: VIN gives 5 V only while the DevKit is powered from USB (or from 5 V on VIN). The 3V3 pin cannot run this sensor: it needs 4.5 V to 5.5 V.
- On a Raspberry Pi Pico: VBUS is 5 V only while the Pico is on USB. The sensor's Tx is 3.3 V, safe for GP1.

## Pinout

| Pin | Function | Type |
| --- | --- | --- |
| Vin | Vin · 4.5 V to 5.5 V | power in |
| GND | GND | power in |
| Tx | UART TX · 3.3 V TTL | output |
| Rx | UART RX · 3.3 V TTL, 5 V compatible | input |
| PWM | PWM | output |
| Vo | ADC · 0.4 V to 2 V | output |
| HD | Zero calibration | input |

## Wiring

### Arduino Uno

| MH-Z19B | Arduino Uno | Note |
| --- | --- | --- |
| Vin | 5V |  |
| GND | GND |  |
| Tx | D2 | SoftwareSerial RX |
| Rx | D3 | SoftwareSerial TX |

Warning: The lamp pulls up to 150 mA (manual, p. 4): run the Uno from USB or a good 5 V supply, not a 9 V battery through the regulator.

### ESP32 DevKit V1

| MH-Z19B | ESP32 DevKit V1 | Note |
| --- | --- | --- |
| Vin | VIN |  |
| GND | GND |  |
| Tx | GPIO16 (RX2) |  |
| Rx | GPIO17 (TX2) |  |

Warning: VIN gives 5 V only while the DevKit is powered from USB (or from 5 V on VIN). The 3V3 pin cannot run this sensor: it needs 4.5 V to 5.5 V.

### Raspberry Pi Pico

| MH-Z19B | Raspberry Pi Pico | Note |
| --- | --- | --- |
| Vin | VBUS |  |
| GND | GND |  |
| Tx | GP1 | Serial1 RX |
| Rx | GP0 | Serial1 TX |

Warning: VBUS is 5 V only while the Pico is on USB. The sensor's Tx is 3.3 V, safe for GP1.

## Code

### Arduino Uno

Libraries: wifwaf/MH-Z19

```cpp
// MH-Z19B on Arduino Uno: wiring and a free simulation at https://www.tinkered.ai/components/mh-z19b
// MH-Z19B on an Arduino Uno: Vin 5V, sensor Tx to D2, sensor Rx to D3, 9600 baud
#include <SoftwareSerial.h>
#include <MHZ19.h>

SoftwareSerial co2Serial(2, 3);  // RX (from sensor Tx), TX (to sensor Rx)

MHZ19 co2;
unsigned long warmupEnd;

void setup() {
  Serial.begin(115200);
  co2Serial.begin(9600);
  co2.begin(co2Serial);
  // Automatic baseline calibration (ABC) is on from the factory; leave it on if the
  // sensor sees fresh outdoor air (about 400 ppm) regularly, else call co2.autoCalibration(false).
  warmupEnd = millis() + 180000UL;  // 3 minute preheat
  Serial.println("MH-Z19B started, warming up for 3 minutes");
}

void loop() {
  int ppm = co2.getCO2();
  if (co2.errorCode != RESULT_OK) {
    Serial.print("No valid reply, error code ");
    Serial.println(co2.errorCode);
  } else {
    Serial.print("CO2: ");
    Serial.print(ppm);
    Serial.print(" ppm   Sensor temperature: ");
    Serial.print(co2.getTemperature(), 0);
    Serial.print(" C");
    if ((long)(millis() - warmupEnd) < 0) Serial.print("   (still warming up)");
    Serial.println();
  }
  delay(5000);
}
```

Compiled for arduino:avr:uno on 2026-10-30.

### ESP32 DevKit V1

Libraries: wifwaf/MH-Z19

```cpp
// MH-Z19B on ESP32 DevKit V1: wiring and a free simulation at https://www.tinkered.ai/components/mh-z19b
// MH-Z19B on an ESP32 DevKit V1: Vin VIN (5 V), sensor Tx to GPIO16, sensor Rx to GPIO17, 9600 baud
#include <MHZ19.h>

MHZ19 co2;
unsigned long warmupEnd;

void setup() {
  Serial.begin(115200);
  Serial2.begin(9600, SERIAL_8N1, 16, 17);  // RX2, TX2
  co2.begin(Serial2);
  // Automatic baseline calibration (ABC) is on from the factory; leave it on if the
  // sensor sees fresh outdoor air (about 400 ppm) regularly, else call co2.autoCalibration(false).
  warmupEnd = millis() + 180000UL;  // 3 minute preheat
  Serial.println("MH-Z19B started, warming up for 3 minutes");
}

void loop() {
  int ppm = co2.getCO2();
  if (co2.errorCode != RESULT_OK) {
    Serial.print("No valid reply, error code ");
    Serial.println(co2.errorCode);
  } else {
    Serial.print("CO2: ");
    Serial.print(ppm);
    Serial.print(" ppm   Sensor temperature: ");
    Serial.print(co2.getTemperature(), 0);
    Serial.print(" C");
    if ((long)(millis() - warmupEnd) < 0) Serial.print("   (still warming up)");
    Serial.println();
  }
  delay(5000);
}
```

Compiled for esp32:esp32:esp32 on 2026-10-30.

### Raspberry Pi Pico

Libraries: wifwaf/MH-Z19

```cpp
// MH-Z19B on Raspberry Pi Pico: wiring and a free simulation at https://www.tinkered.ai/components/mh-z19b
// MH-Z19B on a Raspberry Pi Pico (arduino-pico): Vin VBUS, sensor Tx to GP1, sensor Rx to GP0, 9600 baud
#include <MHZ19.h>

MHZ19 co2;
unsigned long warmupEnd;

void setup() {
  Serial.begin(115200);
  Serial1.setRX(1);
  Serial1.setTX(0);
  Serial1.begin(9600);
  co2.begin(Serial1);
  // Automatic baseline calibration (ABC) is on from the factory; leave it on if the
  // sensor sees fresh outdoor air (about 400 ppm) regularly, else call co2.autoCalibration(false).
  warmupEnd = millis() + 180000UL;  // 3 minute preheat
  Serial.println("MH-Z19B started, warming up for 3 minutes");
}

void loop() {
  int ppm = co2.getCO2();
  if (co2.errorCode != RESULT_OK) {
    Serial.print("No valid reply, error code ");
    Serial.println(co2.errorCode);
  } else {
    Serial.print("CO2: ");
    Serial.print(ppm);
    Serial.print(" ppm   Sensor temperature: ");
    Serial.print(co2.getTemperature(), 0);
    Serial.print(" C");
    if ((long)(millis() - warmupEnd) < 0) Serial.print("   (still warming up)");
    Serial.println();
  }
  delay(5000);
}
```

Compiled for rp2040:rp2040:rpipico on 2026-10-30.

## Specifications

| Spec | Value | Source |
| --- | --- | --- |
| Supply voltage | 4.5 V to 5.5 V DC | Winsen MH-Z19B user's manual v1.0, p. 4 |
| Current | Under 60 mA average, 150 mA peak (at 5 V) | Winsen MH-Z19B user's manual v1.0, p. 4 |
| Range and accuracy | 0 to 2000 or 0 to 5000 ppm; ±(50 ppm + 3 % of reading) | Winsen MH-Z19B user's manual v1.0, p. 4 |
| Interface level | UART TTL at 3.3 V, compatible with 5 V | Winsen MH-Z19B user's manual v1.0, p. 4 |
| UART settings | 9600 baud, 8 data bits, 1 stop bit, no parity; read command 0xFF 0x01 0x86 ... 0x79 | Winsen MH-Z19B user's manual v1.0, p. 7 |
| Other outputs | PWM (1004 ms cycle), analog Vo 0.4 V to 2 V | Winsen MH-Z19B user's manual v1.0, pp. 5 to 6 |
| Preheat and response | 3 min preheat; T90 under 120 s | Winsen MH-Z19B user's manual v1.0, p. 4 |
| Operating conditions | 0 to 50 °C, 0 to 90 %RH non-condensing; 33 × 20 × 9 mm, 5 g | Winsen MH-Z19B user's manual v1.0, p. 4 |
| Calibration | ABC on by default; zero point is 400 ppm after 20 minutes in fresh air; HD low for over 7 s also zeroes | Winsen MH-Z19B user's manual v1.0, pp. 5, 7 to 8 |

## Circuit notes

- The UART is request and reply: send 0xFF 0x01 0x86 0x00 0x00 0x00 0x00 0x00 0x79 and read 9 bytes; CO2 = byte2 × 256 + byte3 (manual, p. 7). The library does this and checks the checksum.
- PWM output gives CO2 without a UART: with the 2000 ppm range, Cppm = 2000 × (TH - 2 ms) / (TH + TL - 4 ms) over a 1004 ms cycle (p. 6).
- The analog Vo output spans 0.4 V to 2 V (p. 5), within any board's ADC range, but it is coarser than the UART value.
- Add a 100 µF capacitor across Vin and GND close to the module if the lamp's 150 mA pulses disturb other parts.

## Common mistakes

- **Readings creep down to 400 ppm and stay there.** ABC is on from the factory (manual, p. 8) and assumes the sensor regularly sees fresh air at about 400 ppm. In a space that never gets fresh air, that baseline is wrong. Turn ABC off with autoCalibration(false) and calibrate zero outdoors.
- **No reply on the UART.** Usually Tx and Rx are not crossed, or the baud rate is not 9600 8N1 (p. 7). Check that the module's Tx goes to the board's RX pin.
- **Brownouts and resets.** The lamp draws 150 mA peaks (p. 4). A board powered from a weak source or long thin wires resets as it fires; power from USB or a solid 5 V rail.
- **Trusting the first few minutes.** The sensor needs a 3 minute preheat (p. 4) and its response time is up to 120 s for 90 % of a step, so readings right after power-up or after breathing on it lag.

## Questions

### Is the MH-Z19B 3.3 V or 5 V?

Its supply is 4.5 V to 5.5 V, but its UART uses 3.3 V levels and the manual calls them 5 V compatible (Winsen manual v1.0, p. 4). So it runs from 5 V and talks directly to 3.3 V and 5 V boards.

### How accurate is the MH-Z19B?

±(50 ppm + 3 % of reading) (p. 4): about ±80 ppm at 1000 ppm. Good enough to know when to open a window; calibration and ABC settings matter more than the spec.

### MH-Z19B or SCD41?

Both are true CO2 sensors. The SCD41 is smaller, uses I2C, runs at 3.3 V and adds humidity and temperature; the MH-Z19B is larger and needs 5 V but is cheap and widely documented.

### Should I turn automatic baseline calibration off?

Only if the sensor never sees fresh air. ABC is on from the factory (p. 8) and works in homes and offices aired daily. In a greenhouse or a closed box, turn it off and calibrate zero outdoors for 20 minutes (p. 7).

### Which library should I use?

WifWaf's MH-Z19 (PlatformIO: wifwaf/MH-Z19). It sends the commands, checks checksums and exposes getCO2(), getTemperature(), autoCalibration() and calibrate().

## Related parts

- [SCD41 CO₂ Sensor](https://www.tinkered.ai/components/scd41-co2-sensor.md): Smaller I2C CO2 sensor with humidity
- [PMS5003](https://www.tinkered.ai/components/pms5003.md): Adds particulate matter for an air monitor
- [BME280](https://www.tinkered.ai/components/bme280.md): Adds temperature, humidity and pressure
- [NeoPixel Ring 12](https://www.tinkered.ai/components/neopixel-ring-12.md): Traffic-light CO2 indicator

## Try it

Tinkered can design a circuit with the MH-Z19B, draw its schematic and check it. Free to start at https://www.tinkered.ai.
