Raspberry Pi Pico - SCD40 CO2 Sensor

In this guide, we will learn to set up and use the SCD40 CO2 sensor with the Raspberry Pi Pico. A room full of people slowly fills with carbon dioxide, and you cannot smell it - but you can feel it as tired eyes and a heavy head. The SCD40 measures that CO2 for real and sends the number in ppm (parts per million) over I2C. We will cover the following details:

Raspberry Pi Pico and SCD40 CO2 sensor module

Hardware Preparation

1×Raspberry Pi Pico W
1×Raspberry Pi Pico Alternatively,
1×Micro USB Cable
1×SCD40 CO2 Sensor Module
1×Breadboard
1×Jumper Wires
1×Recommended: Screw Terminal Expansion Board for Raspberry Pi Pico

Or you can buy the following kits:

1×DIYables Sensor Kit (18 sensors/displays)
Disclosure: Some of the links provided in this section are Amazon affiliate links. We may receive a commission for any purchases made through these links at no additional cost to you.
Additionally, some of these links are for products from our own brand, DIYables .

Overview of SCD40 CO2 Sensor

The SCD40 is a small CO2 sensor made by Sensirion. Many cheap "air quality" sensors, like the MQ135 or the CCS811, only guess the CO2 value from other gases. The SCD40 measures true CO2 with a method called photoacoustic NDIR: a tiny infrared light blinks inside the sensor, the CO2 gas takes in some of that light and makes a very small pressure "sound", and a tiny microphone hears it. More CO2 gives a louder signal. The sensor also has a temperature and humidity sensor inside, which it uses to correct the CO2 value.

SCD40
Supply Voltage 2.4 volts to 5.5 volts
Interface I2C, address 0x62
Output Range 0 to 40,000 ppm
Accuracy ±(50 ppm + 5% of reading), from 400 to 2,000 ppm
Update Time one new result every 5 seconds (30 seconds in low power mode)
Temperature Accuracy ±0.8°C (15 to 35°C)
Humidity Accuracy ±6 %RH (15 to 35°C, 20 to 65 %RH)
Average Current about 15 mA at 3.3 volts

The sensor has automatic self-calibration (ASC), and it is on by default. It thinks that the lowest value it sees in a week is fresh outdoor air (about 400 ppm). So let it breathe fresh air at least once a week, for example near an open window for an hour. Also, the first few readings after power-on can be less exact, so give it a short warm-up.

Pinout

SCD40 CO2 sensor pinout

The SCD40 module has four pins:

  • VCC pin (some boards print VIN): connect to 3.3 volts on the Pico.
  • GND pin: connect to GND (0 volts).
  • SDA pin: the I2C data line, connect to an SDA pin of the Pico.
  • SCL pin: the I2C clock line, connect to an SCL pin of the Pico.

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

What Do the CO2 Numbers Mean?

CO2 Level Meaning
about 400 ppm Outdoor air
400 to 1000 ppm Good indoor air
1000 to 2000 ppm Stuffy air - time to open a window
more than 2000 ppm Poor air - headache and sleepiness are possible

Wiring Diagram

The Pico has two I2C blocks, I2C0 and I2C1. Here we use I2C0 on GP0 and GP1, the same pins used by other I2C tutorials for the Pico on this site. Power the sensor from 3V3(OUT), so the I2C lines stay at 3.3 volts, which is safe for the Pico.

The wiring diagram between Raspberry Pi and Pico SCD40 CO2 sensor

This image is created using Fritzing. Click to enlarge image

SCD40 Module Raspberry Pi Pico
VCC 3V3(OUT) (pin 36)
GND GND (pin 38)
SDA GP0 (pin 1)
SCL GP1 (pin 2)

Raspberry Pi Pico Code - SCD40 CO2

MicroPython has no built-in SCD40 driver, but the sensor only needs four commands, so the code sends them by itself:

  • Stop any old measurement (0x3F86), then start a new one (0x21B1).
  • Every second, ask "is new data ready?" (0xE4B8).
  • When it is ready, read the result (0xEC05). Each value comes with a CRC byte, and the code skips the reading if the CRC does not match.
  • Print the CO2 value with the word Good, Moderate or Poor.
""" This Raspberry Pi Pico MicroPython code was developed by newbiely.com This Raspberry Pi Pico code is made available for public use without any restriction For comprehensive instructions and wiring diagrams, please visit: https://newbiely.com/tutorials/raspberry-pico/raspberry-pi-pico-scd40-co2-sensor """ # Raspberry Pi Pico + SCD40: read the CO2 level (ppm) # No library needed: the few SCD40 commands are sent with machine.I2C from machine import I2C, Pin import time SDA_PIN = 0 # The Raspberry Pi Pico pin GP0 (I2C0 SDA) connected to the SDA pin of the SCD40 SCL_PIN = 1 # The Raspberry Pi Pico pin GP1 (I2C0 SCL) connected to the SCL pin of the SCD40 SCD40_ADDR = 0x62 # Fixed 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, sda=Pin(SDA_PIN), scl=Pin(SCL_PIN), freq=100000) 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 send_command(cmd): i2c.writeto(SCD40_ADDR, bytes([cmd >> 8, cmd & 0xFF])) 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(count): data = raw[i * 3:i * 3 + 2] if crc8(data) != raw[i * 3 + 2]: return None # CRC is wrong: skip this reading words.append((data[0] << 8) | data[1]) return words def data_ready(): words = read_words(CMD_DATA_READY, 1) return words is not None and (words[0] & 0x07FF) != 0 def air_quality(ppm): if ppm < 1000: return "Good" elif ppm < 2000: return "Moderate - open a window" return "Poor" if SCD40_ADDR not in i2c.scan(): print("SCD40 not found - check the SDA/SCL wiring") # Clean start: stop any old measurement, then start a new one (one result every 5 s) send_command(CMD_STOP_PERIODIC) time.sleep_ms(500) send_command(CMD_START_PERIODIC) print("SCD40 started. First result in about 5 seconds...") while True: time.sleep(1) if not data_ready(): continue # no new result yet words = read_words(CMD_READ_MEASUREMENT, 3) if words is None: print("CRC error, reading skipped") continue co2 = words[0] # CO2 in ppm print("CO2: {} ppm ({})".format(co2, air_quality(co2)))

Detailed Instructions

Please follow these instructions step by step:

  • Ensure that Thonny IDE is installed on your computer.
  • Ensure that MicroPython firmware is installed on your Raspberry Pi Pico.
  • If you have never used a Pico before, start with the Raspberry Pi Pico - Getting Started tutorial.
  • Connect the SCD40 module to the Pico as shown in the wiring table.
  • Plug the Raspberry Pi Pico into your computer with a USB cable.
  • Start Thonny IDE and open Tools Options.
  • On the Interpreter tab, select MicroPython (Raspberry Pi Pico) and the correct port (Thonny usually finds it by itself, for example COM4 on Windows or /dev/ttyACM0 on Linux).
  • Paste the code above into the Thonny editor.
  • Save it to the Pico:
    • Click the Save button or press Ctrl+S.
    • In the dialog, pick Raspberry Pi Pico (not This computer).
    • Type main.py as the file name.
  • Press the green Run button (or F5).
  • Wait about 5 seconds for the first value. Then breathe gently toward the sensor and watch the number go up.
Shell x
>>> %Run -c $EDITOR_CONTENT
MPY: soft reboot SCD40 started. First result in about 5 seconds... CO2: 584 ppm (Good) CO2: 579 ppm (Good) CO2: 591 ppm (Good) CO2: 863 ppm (Good) CO2: 1347 ppm (Moderate - open a window) CO2: 1412 ppm (Moderate - open a window) CO2: 1126 ppm (Moderate - open a window) CO2: 902 ppm (Good) CO2: 736 ppm (Good)
MicroPython (Raspberry Pi Pico) • Board CDC @ COM29 ≡

A new line comes about every 5 seconds, because that is how often the SCD40 finishes one measurement. If you see "SCD40 not found - check the SDA/SCL wiring", check that SDA goes to GP0 and SCL goes to GP1, not the other way around.

Reading Temperature and Humidity from the SCD40

The CO2 result of the SCD40 is not alone. Each read of command 0xEC05 returns three values: CO2, temperature and humidity. The first script just ignores the last two. To use them, the code turns the raw numbers into real units:

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

Keep in mind that the sensor warms itself a little while it works, so the temperature can read slightly higher than the room.

Raspberry Pi Pico Code - SCD40 CO2, Temperature and Humidity

""" This Raspberry Pi Pico MicroPython code was developed by newbiely.com This Raspberry Pi Pico code is made available for public use without any restriction For comprehensive instructions and wiring diagrams, please visit: https://newbiely.com/tutorials/raspberry-pico/raspberry-pi-pico-scd40-co2-sensor """ # Raspberry Pi Pico + SCD40: read CO2 (ppm), temperature (C) and humidity (%RH) # No library needed: the few SCD40 commands are sent with machine.I2C from machine import I2C, Pin import time SDA_PIN = 0 # The Raspberry Pi Pico pin GP0 (I2C0 SDA) connected to the SDA pin of the SCD40 SCL_PIN = 1 # The Raspberry Pi Pico pin GP1 (I2C0 SCL) connected to the SCL pin of the SCD40 SCD40_ADDR = 0x62 # Fixed 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, sda=Pin(SDA_PIN), scl=Pin(SCL_PIN), freq=100000) 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 send_command(cmd): i2c.writeto(SCD40_ADDR, bytes([cmd >> 8, cmd & 0xFF])) 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(count): data = raw[i * 3:i * 3 + 2] if crc8(data) != raw[i * 3 + 2]: return None # CRC is wrong: skip this reading words.append((data[0] << 8) | data[1]) return words def data_ready(): words = read_words(CMD_DATA_READY, 1) return words is not None and (words[0] & 0x07FF) != 0 def air_quality(ppm): if ppm < 1000: return "Good" elif ppm < 2000: return "Moderate - open a window" return "Poor" if SCD40_ADDR not in i2c.scan(): print("SCD40 not found - check the SDA/SCL wiring") # Clean start: stop any old measurement, then start a new one (one result every 5 s) send_command(CMD_STOP_PERIODIC) time.sleep_ms(500) send_command(CMD_START_PERIODIC) print("SCD40 started. First result in about 5 seconds...") while True: time.sleep(1) if not data_ready(): continue # no new result yet words = read_words(CMD_READ_MEASUREMENT, 3) if words is None: print("CRC error, reading skipped") continue co2 = words[0] # CO2 in ppm temperature = -45 + 175 * words[1] / 65535 # temperature in degrees C humidity = 100 * words[2] / 65535 # relative humidity in % print("CO2: {} ppm ({}) | Temperature: {:.1f} C | Humidity: {:.1f} %".format( co2, air_quality(co2), temperature, humidity))

Detailed Instructions

  • Keep the same wiring.
  • Paste this code into Thonny, save it to the Pico as main.py, and click Run.
  • Look at the Shell:
Shell x
>>> %Run -c $EDITOR_CONTENT
MPY: soft reboot SCD40 started. First result in about 5 seconds... CO2: 621 ppm (Good) | Temperature: 24.3 C | Humidity: 46.8 % CO2: 628 ppm (Good) | Temperature: 24.3 C | Humidity: 46.9 % CO2: 640 ppm (Good) | Temperature: 24.4 C | Humidity: 47.3 % CO2: 655 ppm (Good) | Temperature: 24.4 C | Humidity: 47.1 %
MicroPython (Raspberry Pi Pico) • Board CDC @ COM29 ≡

Because the file is named main.py and saved in the root of the Raspberry Pi Pico, the CO2 monitor starts every time the Pico is powered on or reset - which is handy later when you add a display and power it without a computer.

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!