ESP32 MicroPython SCD40 CO2 Sensor

This tutorial instructs you how to use the SCD40 CO2 sensor with the ESP32 and MicroPython. In detail, we will learn:

ESP32 MicroPython and SCD40 CO2 sensor module

Overview of SCD40 CO2 Sensor

When people stay in a closed room, the CO2 level climbs. High CO2 is a clear sign that the room needs fresh air. The SCD40 from Sensirion measures this gas for real. It does not guess from other gases, like the "eCO2" value of an MQ135 or a CCS811. How does it work? A tiny infrared light pulses inside a small chamber. CO2 takes in part of that light and makes a very small pressure wave, a bit like a sound. A tiny microphone picks it up. The more CO2 there is, the stronger the signal. This method is called photoacoustic NDIR.

Item SCD40
Supply voltage 2.4V to 5.5V
Interface I2C, address 0x62
CO2 output range 0 to 40,000 ppm
CO2 accuracy ±(50 ppm + 5% of reading), from 400 to 2,000 ppm
New reading every 5 seconds (low-power mode every 30 seconds)
Temperature accuracy ±0.8°C (15°C to 35°C)
Humidity accuracy ±6 %RH (15°C to 35°C, 20 to 65 %RH)
Average current about 15 mA at 3.3V

Two things are good to know before you start:

  • The first few readings after power-up can be less accurate. Give the sensor a little time.
  • Automatic self-calibration (ASC) is ON by default. It thinks that the sensor sees fresh, outdoor-level air (about 400 ppm) at least once a week, best for an hour or more. So, now and then, open a window or put the sensor near one. If it never sees fresh air, its readings can slowly drift.

Pinout

SCD40 CO2 sensor module pinout

The SCD40 module has four pins:

  • VCC pin (also printed VIN): connect to 3.3V of the ESP32 (2.4 volts to 5.5 volts are accepted).
  • GND pin: connect to GND (0 volts).
  • SDA pin: the I2C data line. Connect to GPIO21 of the ESP32.
  • SCL pin: the I2C clock line. Connect to GPIO22 of the ESP32.

Most SCD40 modules already have I2C pull-up resistors on board, so you do not need to add any.

What Does the CO2 Number Mean?

CO2 is shown in ppm (parts per million). Use this guide to read the value:

CO2 level Air quality What to do
about 400 ppm Outdoor air Nothing, this is fresh air
400 to 1000 ppm Good indoor air Nothing
1000 to 2000 ppm Stuffy Open a window
above 2000 ppm Poor Get fresh air now, you may feel sleepy or get a headache

Wiring Diagram

The ESP32 runs at 3.3 volts, and the SCD40 works fine at 3.3 volts too. We power it from the 3.3V pin, so the I2C lines also stay at 3.3 volts.

SCD40 Pin ESP32 Pin
VCC 3.3V
GND GND
SDA GPIO21
SCL GPIO22
  • How to connect ESP32 and SCD40 CO2 sensor using breadboard
The wiring diagram between ESP32 MicroPython SCD40 CO2 sensor

This image is created using Fritzing. Click to enlarge image

How to connect ESP32 and SCD40 CO2 sensor

GPIO21 and GPIO22 are the usual I2C pins of the ESP32. You can move them to other GPIOs. If you do, change SDA_PIN and SCL_PIN at the top of the code.

ESP32 MicroPython Code - SCD40 CO2 Sensor

There is no SCD40 driver built into MicroPython, and we do not want to depend on a random file from the internet. The good news: the sensor needs only a few short commands, so the script talks to it directly with machine.I2C.

What the script does:

  • It sends "stop periodic measurement" first. If an older script left the sensor running, this gives a clean start. Then it waits 500 ms.
  • It sends "start periodic measurement". From now on, the sensor makes a new reading every 5 seconds.
  • Every second, it asks "is new data ready?". If not, it waits again.
  • When data is ready, it reads 9 bytes: CO2, temperature and humidity. Each value has 2 data bytes and 1 check byte (CRC). If a check byte is wrong, the reading is skipped.
  • It prints the CO2 value with a short air-quality word: Good, Moderate or Poor.
""" This ESP32 MicroPython code was developed by newbiely.com This ESP32 MicroPython code is made available for public use without any restriction For comprehensive instructions and wiring diagrams, please visit: https://newbiely.com/tutorials/esp32-micropython/esp32-micropython-scd40-co2-sensor """ from machine import I2C, Pin import time SDA_PIN = 21 # ESP32 GPIO21 connected to the SDA pin of the SCD40 SCL_PIN = 22 # ESP32 GPIO22 connected to the SCL pin of the SCD40 SCD40_ADDR = 0x62 # I2C address of the SCD40 # SCD40 commands (16-bit, sent MSB first) CMD_START_PERIODIC = 0x21B1 CMD_STOP_PERIODIC = 0x3F86 CMD_DATA_READY = 0xE4B8 CMD_READ_MEASUREMENT = 0xEC05 i2c = I2C(0, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN), freq=100000) def send_command(cmd): i2c.writeto(SCD40_ADDR, bytes([cmd >> 8, cmd & 0xFF])) def crc8(two_bytes): # Sensirion CRC-8: polynomial 0x31, start value 0xFF crc = 0xFF for byte in two_bytes: crc ^= byte for _ in range(8): if crc & 0x80: crc = ((crc << 1) ^ 0x31) & 0xFF else: crc = (crc << 1) & 0xFF return crc def read_words(cmd, count): # Send a read command, then read 'count' words (2 data bytes + 1 CRC byte each) send_command(cmd) time.sleep_ms(1) raw = i2c.readfrom(SCD40_ADDR, count * 3) words = [] for i in range(0, count * 3, 3): if crc8(raw[i:i + 2]) != raw[i + 2]: return None # CRC error: skip this reading words.append((raw[i] << 8) | raw[i + 1]) return words def air_quality(ppm): if ppm < 1000: return "Good" elif ppm < 2000: return "Moderate - open a window" return "Poor - get fresh air now" # Clean start: stop a measurement that may still run from an earlier script send_command(CMD_STOP_PERIODIC) time.sleep_ms(500) send_command(CMD_START_PERIODIC) print("SCD40 started. First CO2 reading comes in about 5 seconds...") while True: time.sleep(1) status = read_words(CMD_DATA_READY, 1) if status is None or (status[0] & 0x07FF) == 0: continue # no new reading yet data = read_words(CMD_READ_MEASUREMENT, 3) if data is None: print("CRC error, reading skipped") continue co2 = data[0] # CO2 in ppm print("CO2: {} ppm -> {}".format(co2, air_quality(co2)))

Detailed Instructions

Here's instructions on how to set up and run your MicroPython code on the ESP32 using Thonny IDE:

  • Make sure Thonny IDE is installed on your computer.
  • Confirm that MicroPython firmware is loaded on your ESP32 board.
  • If you have never used MicroPython on the ESP32 before, follow the ESP32 MicroPython Getting Started guide.
  • Connect the SCD40 module to the ESP32 board according to the provided diagram.
  • Connect the ESP32 board to your computer with a USB cable.
  • Open Thonny IDE on your computer.
  • In Thonny, go to Tools Options, open the Interpreter tab, and choose MicroPython (ESP32) from the dropdown.
  • Copy the provided MicroPython code and paste it into Thonny's editor.
  • Save the code to your ESP32 by:
    • Clicking the Save button or pressing Ctrl+S.
    • Choosing MicroPython device in the save dialog.
    • Typing main.py as the file name.
  • Click the green Run button (or press F5) to execute the script.
  • Wait about 5 seconds for the first value. Then breathe gently toward the sensor a few times and watch the number go up.
  • Check out the message in the Shell at the bottom of Thonny.
Shell x
>>> %Run -c $EDITOR_CONTENT
MPY: soft reboot SCD40 started. First CO2 reading comes in about 5 seconds... CO2: 587 ppm -> Good CO2: 594 ppm -> Good CO2: 601 ppm -> Good CO2: 873 ppm -> Good CO2: 1146 ppm -> Moderate - open a window CO2: 1062 ppm -> Moderate - open a window CO2: 918 ppm -> Good CO2: 763 ppm -> Good
MicroPython (ESP32) • CP2102 USB To UART Bridge Controller @ COM12 ≡

The value falls back slowly after you stop breathing on it, because the CO2 must leave the small chamber first.

Got OSError: [Errno 116] ETIMEDOUT or [Errno 19] ENODEV? The ESP32 cannot find the sensor. Check the four wires, and make sure SDA goes to GPIO21 and SCL goes to GPIO22, not the other way round.

Reading Temperature and Humidity from the SCD40

The SCD40 also has a temperature and humidity sensor inside. It uses them to correct the CO2 value, but you can read them too. In fact, the first script already receives them. They are the second and third values in the same 9 bytes. It just does not print them.

To turn the raw numbers into real units, use these formulas:

  • Temperature (°C) = -45 + 175 × raw value ÷ 65535
  • Humidity (%) = 100 × raw value ÷ 65535

The script below is the same as before, but it adds these two formulas and prints all three values on one line.

ESP32 MicroPython Code - CO2, Temperature and Humidity

""" This ESP32 MicroPython code was developed by newbiely.com This ESP32 MicroPython code is made available for public use without any restriction For comprehensive instructions and wiring diagrams, please visit: https://newbiely.com/tutorials/esp32-micropython/esp32-micropython-scd40-co2-sensor """ from machine import I2C, Pin import time SDA_PIN = 21 # ESP32 GPIO21 connected to the SDA pin of the SCD40 SCL_PIN = 22 # ESP32 GPIO22 connected to the SCL pin of the SCD40 SCD40_ADDR = 0x62 # I2C address of the SCD40 # SCD40 commands (16-bit, sent MSB first) CMD_START_PERIODIC = 0x21B1 CMD_STOP_PERIODIC = 0x3F86 CMD_DATA_READY = 0xE4B8 CMD_READ_MEASUREMENT = 0xEC05 i2c = I2C(0, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN), freq=100000) def send_command(cmd): i2c.writeto(SCD40_ADDR, bytes([cmd >> 8, cmd & 0xFF])) def crc8(two_bytes): # Sensirion CRC-8: polynomial 0x31, start value 0xFF crc = 0xFF for byte in two_bytes: crc ^= byte for _ in range(8): if crc & 0x80: crc = ((crc << 1) ^ 0x31) & 0xFF else: crc = (crc << 1) & 0xFF return crc def read_words(cmd, count): # Send a read command, then read 'count' words (2 data bytes + 1 CRC byte each) send_command(cmd) time.sleep_ms(1) raw = i2c.readfrom(SCD40_ADDR, count * 3) words = [] for i in range(0, count * 3, 3): if crc8(raw[i:i + 2]) != raw[i + 2]: return None # CRC error: skip this reading words.append((raw[i] << 8) | raw[i + 1]) return words # Clean start: stop a measurement that may still run from an earlier script send_command(CMD_STOP_PERIODIC) time.sleep_ms(500) send_command(CMD_START_PERIODIC) print("SCD40 started. Waiting for the first reading...") while True: time.sleep(1) status = read_words(CMD_DATA_READY, 1) if status is None or (status[0] & 0x07FF) == 0: continue # no new reading yet data = read_words(CMD_READ_MEASUREMENT, 3) if data is None: print("CRC error, reading skipped") continue co2 = data[0] # CO2 in ppm temperature = -45 + 175 * data[1] / 65535 # temperature in °C humidity = 100 * data[2] / 65535 # relative humidity in % print("CO2: {} ppm | Temperature: {:.1f} °C | Humidity: {:.1f} %".format( co2, temperature, humidity))

Detailed Instructions

  • Copy the code above and paste it into Thonny's editor.
  • Save it to your ESP32 as main.py.
  • Click the green Run button (or press F5).
  • Check out the message in the Shell at the bottom of Thonny.
Shell x
>>> %Run -c $EDITOR_CONTENT
MPY: soft reboot SCD40 started. Waiting for the first reading... CO2: 642 ppm | Temperature: 25.3 °C | Humidity: 51.7 % CO2: 648 ppm | Temperature: 25.3 °C | Humidity: 51.5 % CO2: 655 ppm | Temperature: 25.2 °C | Humidity: 51.6 % CO2: 661 ppm | Temperature: 25.2 °C | Humidity: 51.4 %
MicroPython (ESP32) • CP2102 USB To UART Bridge Controller @ COM12 ≡

If you name your script main.py and save it to the MicroPython device, it will run automatically every time the ESP32 is powered on.

The temperature reads a bit higher than your room thermometer? The sensor warms itself a little while it runs, and so does a nearby ESP32. Keep a few centimeters of space between the two boards for a better value.

Video Tutorial

※ OUR MESSAGES

  • As freelancers, We are AVAILABLE for HIRE. See how to outsource your project to us
  • Please feel free to share the link of this tutorial. However, Please do not use our content on any other websites. We invested a lot of effort and time to create the content, please respect our work!