Raspberry Pi - SCD40 CO2 Sensor

When many people sit in a closed room, the carbon dioxide (CO2) level slowly rises. You do not smell it, but you may feel tired or get a headache. The Sensirion SCD40 measures this gas directly and sends the result as a number in ppm (parts per million). This tutorial instructs you how to use Raspberry Pi to read the CO2 level from the SCD40 sensor. In detail, we will learn:

Raspberry Pi SCD40 CO2 Sensor

We suggest:

Hardware Preparation

1×Raspberry Pi 5
1×SCD40 CO2 Sensor Module
1×Breadboard
1×Jumper Wires
1×Recommended: Screw Terminal Block Shield for Raspberry Pi
1×Recommended: Raspberry Pi Prototyping Base Plate & Breadboard Kit

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 true CO2 sensor. Many cheap "air quality" sensors (like the CCS811 or MQ135) only guess the CO2 value from other gases. The SCD40 measures the CO2 itself with photoacoustic NDIR sensing: a tiny infrared light pulses inside a small chamber, the CO2 in the air absorbs that light and makes a very weak pressure "sound", and a tiny microphone picks it up. More CO2 gives a stronger signal. The chip also has a temperature and humidity sensor inside, which it uses to correct the CO2 reading.

SCD40 CO2 sensor pinout

The SCD40 module has four pins:

  • VCC (or VIN): This pin must be connected to the voltage supply (2.4V to 5.5V). On Raspberry Pi, use 3.3V
  • GND: This pin must be connected to ground (0V)
  • SDA: This pin is the I2C data line and is connected to the SDA pin of Raspberry Pi
  • SCL: This pin is the I2C clock line and is connected to the SCL pin of Raspberry Pi

Most SCD40 modules already have pull-up resistors on the I2C lines, so no extra resistor is needed.

※ NOTE THAT:

The arrangement of pins on a module can differ between manufacturers. It is essential to always use the labels printed on the module when working with it. Be sure to take a close look!

SCD40 Specifications

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
Update rate 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

What Do the CO2 Numbers Mean?

CO2 level (ppm) Air quality
around 400 Fresh outdoor air
400 to 1000 Good indoor air
1000 to 2000 Stuffy, time to open a window
above 2000 Poor, headache and sleepiness are possible

The SCD40 has automatic self-calibration (ASC) turned on by default. It expects the sensor to see fresh air (about 400 ppm) at least once a week, ideally for an hour or more. If your room is never aired out, the readings can drift over time. Also, the first few readings after power-on can be a little less accurate while the sensor warms up.

Wiring Diagram

The wiring diagram between Raspberry Pi and SCD40 CO2 sensor

This image is created using Fritzing. Click to enlarge image

To simplify and organize your wiring setup, we recommend using a Screw Terminal Block Shield for Raspberry Pi. This shield ensures more secure and manageable connections, as shown below:

Raspberry Pi Screw Terminal Block Shield

Connect the SCD40 module to the Raspberry Pi as follows:

  • SCD40 VCC pin to Raspberry Pi 3.3V (pin 1)
  • SCD40 GND pin to Raspberry Pi GND (pin 6 or any GND pin)
  • SCD40 SDA pin to Raspberry Pi GPIO 2 (pin 3), the SDA pin of I2C bus 1
  • SCD40 SCL pin to Raspberry Pi GPIO 3 (pin 5), the SCL pin of I2C bus 1

Powering the module from 3.3V keeps the I2C pull-up resistors at 3.3V too, which is safe for the GPIO pins of Raspberry Pi.

How To Program for Raspberry Pi to read CO2 from SCD40 Sensor

There is no need for a big driver library. The SCD40 is controlled with a few 16-bit I2C commands, and the script sends them directly with the smbus2 library:

  • 0x21B1: start periodic measurement (a new reading every 5 seconds)
  • 0xE4B8: ask if a new reading is ready
  • 0xEC05: read the measurement (CO2, temperature and humidity)
  • 0x3F86: stop periodic measurement

Every value the sensor sends back comes as two data bytes plus one CRC byte. The script checks the CRC and skips the reading if it does not match, so you never print a broken value.

Detailed Instructions

  • Make sure you have Raspbian or any other Raspberry Pi compatible operating system installed on your Pi.
  • Make sure your Raspberry Pi is connected to the same local network as your PC.
  • Make sure your Raspberry Pi is connected to the internet if you need to install some libraries.
  • If this is the first time you use Raspberry Pi, See how to set up the Raspberry Pi
  • Connect your PC to the Raspberry Pi via SSH using the built-in SSH client on Linux and macOS or PuTTY on Windows. See to how connect your PC to Raspberry Pi via SSH.
  • Run the following command to open the Raspberry Pi configuration tool:
sudo raspi-config
sudo reboot
  • Reconnect your PC to the Raspberry Pi via SSH
  • Install the smbus2 library and the I2C tools by running the following commands:
sudo apt-get update sudo apt-get install python3-smbus2 i2c-tools
  • Check that Raspberry Pi can see the sensor:
i2cdetect -y 1

You should see the number 62 in the table. If the table is empty, check the SDA and SCL wires again.

  • Create a Python script file scd40_co2.py and add the following code:
""" This Raspberry Pi code was developed by newbiely.com This Raspberry Pi code is made available for public use without any restriction For comprehensive instructions and wiring diagrams, please visit: https://newbiely.com/tutorials/raspberry-pi/raspberry-pi-scd40-co2-sensor """ import time from smbus2 import SMBus, i2c_msg I2C_BUS = 1 # /dev/i2c-1 = GPIO2 (SDA, pin 3) and GPIO3 (SCL, pin 5) 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 def crc8(data): # Sensirion CRC-8: polynomial 0x31, init 0xFF crc = 0xFF for byte in data: crc ^= byte for _ in range(8): if crc & 0x80: crc = ((crc << 1) ^ 0x31) & 0xFF else: crc = (crc << 1) & 0xFF return crc def send_command(bus, cmd): bus.i2c_rdwr(i2c_msg.write(SCD40_ADDR, [cmd >> 8, cmd & 0xFF])) def read_words(bus, cmd, count): # send a command, then read "count" words (2 data bytes + 1 CRC byte each) send_command(bus, cmd) time.sleep(0.001) msg = i2c_msg.read(SCD40_ADDR, count * 3) bus.i2c_rdwr(msg) raw = list(msg) 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 "Stuffy, open a window" return "Poor" with SMBus(I2C_BUS) as bus: # clean start: stop a measurement that may still run from an earlier script try: send_command(bus, CMD_STOP_PERIODIC) except OSError: pass time.sleep(0.5) send_command(bus, CMD_START_PERIODIC) print("SCD40 started, first reading in about 5 seconds...") try: while True: time.sleep(1) status = read_words(bus, CMD_DATA_READY, 1) if status is None or (status[0] & 0x07FF) == 0: continue # no new data yet data = read_words(bus, 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))) except KeyboardInterrupt: pass finally: send_command(bus, CMD_STOP_PERIODIC)
  • Save the file and run the Python script by executing the following command in the terminal:
python3 scd40_co2.py
  • Breathe gently toward the sensor for a few seconds and watch the CO2 value go up, then wait for it to come back down.
PuTTY - Raspberry Pi
SCD40 started, first reading in about 5 seconds... CO2: 584 ppm -> Good CO2: 591 ppm -> Good CO2: 603 ppm -> Good CO2: 1187 ppm -> Stuffy, open a window CO2: 1462 ppm -> Stuffy, open a window CO2: 1095 ppm -> Stuffy, open a window CO2: 836 ppm -> Good CO2: 702 ppm -> Good

The script checks the sensor once every second (as specified in the time.sleep(1) line), but the SCD40 only makes a new reading every 5 seconds, so you will see a new line about every 5 seconds until you stop the script by pressing Ctrl + C keys in the terminal. When you stop it, the script tells the sensor to stop measuring.

Reading Temperature and Humidity from the SCD40

The same 0xEC05 read command already gives us three values, not just one. We simply ignored the last two in the first script. The second and third values are raw numbers that are turned into real units like this:

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

The new script below prints all three values on one line.

Detailed Instructions

  • Create a Python script file scd40_co2_temp_humidity.py and add the following code:
""" This Raspberry Pi code was developed by newbiely.com This Raspberry Pi code is made available for public use without any restriction For comprehensive instructions and wiring diagrams, please visit: https://newbiely.com/tutorials/raspberry-pi/raspberry-pi-scd40-co2-sensor """ import time from smbus2 import SMBus, i2c_msg I2C_BUS = 1 # /dev/i2c-1 = GPIO2 (SDA, pin 3) and GPIO3 (SCL, pin 5) 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 def crc8(data): # Sensirion CRC-8: polynomial 0x31, init 0xFF crc = 0xFF for byte in data: crc ^= byte for _ in range(8): if crc & 0x80: crc = ((crc << 1) ^ 0x31) & 0xFF else: crc = (crc << 1) & 0xFF return crc def send_command(bus, cmd): bus.i2c_rdwr(i2c_msg.write(SCD40_ADDR, [cmd >> 8, cmd & 0xFF])) def read_words(bus, cmd, count): # send a command, then read "count" words (2 data bytes + 1 CRC byte each) send_command(bus, cmd) time.sleep(0.001) msg = i2c_msg.read(SCD40_ADDR, count * 3) bus.i2c_rdwr(msg) raw = list(msg) 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 with SMBus(I2C_BUS) as bus: # clean start: stop a measurement that may still run from an earlier script try: send_command(bus, CMD_STOP_PERIODIC) except OSError: pass time.sleep(0.5) send_command(bus, CMD_START_PERIODIC) print("SCD40 started, waiting for data...") try: while True: time.sleep(1) status = read_words(bus, CMD_DATA_READY, 1) if status is None or (status[0] & 0x07FF) == 0: continue # no new data yet data = read_words(bus, CMD_READ_MEASUREMENT, 3) if data is None: print("CRC error, reading skipped") continue co2 = data[0] # ppm temperature = -45 + 175 * data[1] / 65535 # degrees Celsius humidity = 100 * data[2] / 65535 # % relative humidity print("CO2: {} ppm | Temperature: {:.1f}°C | Humidity: {:.1f}%".format( co2, temperature, humidity)) except KeyboardInterrupt: pass finally: send_command(bus, CMD_STOP_PERIODIC)
  • Save the file and run the Python script by executing the following command in the terminal:
python3 scd40_co2_temp_humidity.py
  • Check the terminal output:
PuTTY - Raspberry Pi
SCD40 started, waiting for data... CO2: 647 ppm | Temperature: 23.4°C | Humidity: 51.2% CO2: 652 ppm | Temperature: 23.5°C | Humidity: 51.0% CO2: 649 ppm | Temperature: 23.5°C | Humidity: 50.8% CO2: 658 ppm | Temperature: 23.6°C | Humidity: 50.9%

If the temperature looks a bit higher than your room thermometer, move the sensor away from the Raspberry Pi. The Pi's processor gets warm and can heat the air around the sensor.

Additional Knowledge

Let's compare the SCD40 with the MQ2 gas sensor from the Raspberry Pi - Gas Sensor tutorial:

SCD40 MQ2
What it detects CO2 only LPG, smoke, alcohol, propane, hydrogen, methane, carbon monoxide
Output CO2 in ppm (I2C) HIGH/LOW signal or analog voltage
Measuring method photoacoustic NDIR heated sensing element
Needs an ADC on Raspberry Pi No Yes, for the analog value
Also measures temperature and humidity Yes No

Evidently, the SCD40 is the right choice for checking indoor air in a room or office, while the MQ2 is meant for detecting gas leaks and smoke.

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!