bitcreed

Embedded & Sensing

SCD40 Below-Sea-Level Altitude Compensation

· updated

In June 2024 a user asked on Sensirion’s Arduino driver repository whether the SCD40 CO₂ sensor can be altitude-compensated below sea level. The obvious function, setSensorAltitude(), only takes positive values. The sensor has a second way in, though: setAmbientPressure(), meant for use with an absolute pressure sensor like the BME280. With a little physics you can use that interface to express any altitude, including negative ones. Calculate the ambient pressure at the desired altitude and send that to the sensor instead.

I posted the answer the same day, and Sensirion closed the issue a month later with “everything should be clear now”. This note is the longer version, checked against the datasheet and both major versions of the library.

I didn’t come to this question cold. From 2015 to 2021 I worked at Sensirion. For the last three of those years I led the team that maintained its open-source drivers, including embedded-scd for the SCD30, the SCD4x’s predecessor.

Why the CO₂ reading needs pressure at all

The SCD4x compensates its CO₂ output on-chip for temperature and humidity. For pressure it needs outside help: the datasheet says you can provide “externally obtained pressure or altitude values to enable on-board compensation of the CO2 output signal for pressure variations”. Its accuracy figures are specified at an ambient pressure of 1013 mbar. If you tell the sensor nothing, it assumes sea level.

Two commands, one compensation

The SCD4x datasheet (version 1.7, April 2025) describes both commands:

set_sensor_altitude set_ambient_pressure
I²C command 0x2427 0xe000
Unit on the wire metres Pa ÷ 100 (i.e. hPa)
Valid range 0 – 3,000 m 70,000 – 120,000 Pa
Default 0 m 101,300 Pa
When idle mode only also during periodic measurement
Kept across power cycles with persist_settings not listed under persist_settings

The two are alternatives, not layers. Setting an ambient pressure “overrides any pressure compensation based on a previously set sensor altitude”, and the reverse is also true. Whichever you call last is the one the sensor uses.

That is why the workaround is clean rather than a hack: the altitude command is only a convenience for converting a fixed height into a pressure. The pressure command accepts values well above sea-level pressure, so it covers everything down to the lowest dry land on earth.

Below sea level: calculate the pressure

Pressure rises by roughly 1 hPa for every 10 m you go below sea level. Starting from sea-level pressure (1013.25 hPa), the Dead Sea shore at 430 m below sea level works out to:

430 m / 10 m per hPa = 43 hPa
1013.25 hPa + 43 hPa = 1056.25 hPa

So send about 1056 hPa to the sensor, then go enjoy floating in the warm water.

The 1 hPa per 10 m rule is only an approximation. If you have a barometer on the board, use its reading instead: set_ambient_pressure can be sent during periodic measurement, and the datasheet highly recommends it for “applications experiencing significant ambient pressure changes”.

Don’t try to squeeze a negative number through setSensorAltitude(). Its parameter is a uint16_t, so −430 wraps to 65,106 m, far outside the 0–3,000 m the datasheet allows.

Mind the library version

This is where a two-year-old answer can bite. The call in my original answer, setAmbientPressure(1056), is correct for the library as it was then (0.4.0), where the argument is in hPa. Version 1.0.0 (January 2025) changed it: its changelog lists “get/setAmbientPressure(uint32_t& aAmbientPressure); now takes the ambient pressure in Pa as uint32_t” as a breaking change. The hPa variant lives on as setAmbientPressureRaw().

With the current library, pick one of these:

int16_t localError = 0;

// Pa (library 1.x). The driver rounds to whole hPa before sending.
localError = sensor.setAmbientPressure(105625);

// hPa, the value that goes over the wire (library 1.x)
localError = sensor.setAmbientPressureRaw(1056);

Passing 1056 to setAmbientPressure() in 1.x compiles fine and sends 11 hPa, far outside the valid range. If you copy code from an older forum post, check which version it was written for.

Checklist

  • Fixed installation below sea level: calculate the pressure once and send it with setAmbientPressure() (Pa) or setAmbientPressureRaw() (hPa) on library 1.x.
  • persist_settings covers the altitude, but the datasheet doesn’t list ambient pressure among the settings it stores. Treat it as volatile and send it after every power-up.
  • Don’t mix the two commands: the last one called wins.
  • Doing a forced recalibration (FRC)? The datasheet says to provide the altitude or pressure reference before it.

Sources: Sensirion/arduino-i2c-scd4x issue #35, the library’s header and changelog, and the 0.4.0 header.

Embedded projects, device drivers and sensor integration are what I like working on most. If you’re stuck on something similar, get in touch.

Aside: Raspberry Pi 5 and Pico 2 (August 2024)

Years ago the Raspberry Pi was one of those disruptive developments on the embedded front, bridging the gap between Arduinos and low-cost computers. It’s great for prototyping while powerful enough that you don’t have to worry about prematurely optimizing code. It is still a major contender in the same field, and each generation delivers better efficiency and features. Two releases from August 2024:

Raspberry Pi Pico 2

The Pico 2 is a worthy upgrade to the original Pico. Its RP2350 chip is also sold on its own, and the RP2354 variants include 2 MB of stacked-in-package QSPI flash. Check out the many partner boards at the bottom of the announcement for ready-to-use solutions. The RP2350 also carries RISC-V (Hazard3) cores that can be selected at boot instead of the Arm Cortex-M33 pair.

The extra processing power has already found a particularly cool application: real-time ML noise suppression on live audio.

Raspberry Pi 5 with 2 GB

The Raspberry Pi 5 now comes in a 2 GB RAM model with a quad-core Arm Cortex-A76 running at 2.4 GHz. I don’t recommend the 2 GB variant for development or anything display-driven, but it’s a great budget saver when you have a finished application that you know will not need more.

If you’re upgrading from a Pi 4, reconsider your existing 5 V / 3 A (15 W) power supply, especially if your use case draws power off USB: with such a supply the Pi 5 limits USB peripherals to 600 mA (power supply documentation). A 5 V / 5 A supply avoids the limit.

For long-running, write-heavy applications, an M.2 HAT lets you use more reliable M.2 SSDs instead of the typical SD card.

My main wish at this point is a fanless, enclosed option: the Pi 4 and 5 quickly exhaust their thermal reserves and throttle accordingly.