# Sensirion SCD4x Component (ESP-IDF)
ESP-IDF component for the Sensirion SCD4x family (SCD40 / SCD41 / SCD43) CO2, temperature, and humidity sensors over I2C.
## Sensor variants
All three variants share the same I2C address (`0x62`), pinout, command set and CO2 output range (0–40 000 ppm). They differ in the range over which Sensirion specifies the accuracy, the accuracy itself, and which measurement modes are supported:
| Variant | Specified accuracy range | CO2 accuracy | Single-shot mode |
|---------|--------------------------|--------------|------------------|
| SCD40 | 400–2 000 ppm | ±(50 ppm + 5 % of reading) | **no** |
| SCD41 | 400–5 000 ppm | ±(50 ppm + 2.5 %) @ 400–1 000 ppm; ±(50 ppm + 3 %) @ 1 001–2 000 ppm; ±(40 ppm + 5 %) @ 2 001–5 000 ppm | yes |
| SCD43 | 400–5 000 ppm | ±(30 ppm + 3 % of reading) | yes |
Periodic and low-power periodic modes are supported by all three variants.
Note: the sensor will report values across the full 0–40 000 ppm output range on all variants, but readings outside the specified accuracy range above are not guaranteed by Sensirion.
All variants also share:
- Temperature: −10 °C to 60 °C, ±0.8 °C (15–35 °C)
- Humidity: 0–100 %RH reporting range, ±6 %RH (15–35 °C, 20–65 %RH)
- Supply: 2.4–5.5 V
- Automatic Self-Calibration (ASC) and Forced Recalibration (FRC)
> **Operating humidity limit:** the sensor is specified for **0–95 %RH, non-condensing** only. Prolonged exposure above 95 %RH, or any condensation (100 %RH / liquid water on the sensor), can permanently damage the device. Make sure the enclosure prevents condensation, especially if the sensor is used outdoors or in humid environments.
Single-shot commands (`scd4x_measure_single_shot()`, `scd4x_measure_single_shot_rht_only()`, `scd4x_power_down()`, `scd4x_wake_up()`, and the getters and setters of the ASC initial/standard period) are **only supported by SCD41 and SCD43**. The driver reads the variant during `scd4x_init()`, and on an SCD40 these calls return `ESP_ERR_NOT_SUPPORTED` without touching the bus.
Use `scd4x_get_sensor_variant()` to find out which part is connected.
## Features
- Periodic and low-power periodic measurement modes
- Single-shot measurement (SCD41 / SCD43 only)
- Temperature offset and altitude compensation
- Forced recalibration and automatic self-calibration (ASC), incl. target / initial / standard period tuning
- Power-down / wake-up for low-power applications (SCD41 / SCD43)
- Sensor variant detection (SCD40 / SCD41 / SCD43)
- Built-in CRC verification, I2C error recovery and restart of a stalled measurement
## Requirements
- ESP-IDF >= 5.3.0
- I2C master driver (`esp_driver_i2c`)
## Task model
This driver is not thread-safe. Give one FreeRTOS task exclusive ownership of
each `scd4x_t` instance and perform all sensor I/O from that task. Other tasks
should send requests to the owner through a queue or another message-passing
mechanism and receive measurements through the same mechanism. Do not call the
driver concurrently or share an instance between tasks without external
serialization.
The driver tracks the sensor state and rejects commands that are invalid in the
current state with `ESP_ERR_INVALID_STATE`. Most configuration and utility
commands require idle mode. During periodic measurement, only measurement
readout, data-ready polling, stop, and ambient-pressure get/set are accepted.
While powered down, only `scd4x_wake_up()` is accepted. A NULL pointer argument
or a value out of range returns `ESP_ERR_INVALID_ARG`.
During periodic measurement the driver watches for new data. After three signal
update intervals without any (15 s, or 90 s in low-power mode) the sensor is not
measuring, for example because it was power-cycled and came back idle.
`scd4x_data_ready()` and `scd4x_read_measurement()` then restart the measurement,
which blocks for about 1.5 s, and return `ESP_ERR_TIMEOUT` once. Settings that were
not saved with `scd4x_persist_settings()` may have been lost and should be applied
again.
## Installation
The component is published in the [ESP Component Registry](https://components.espressif.com/components/jef-sure/scd4x). From your project directory, add it with the [IDF Component Manager](https://docs.espressif.com/projects/idf-component-manager/en/latest/use/how_to_add_dependency.html):
```bash
idf.py add-dependency "jef-sure/scd4x^0.3.0"
```
To track the Git repository instead of a registry release, declare it in your project's `main/idf_component.yml`:
```yaml
dependencies:
jef-sure/scd4x:
git: https://github.com/jef-sure/esp32-component-scd4x.git
```
## Wiring
| SCD4x Pin | Connection |
|-----------|------------|
| SDA | I2C data line (any GPIO) |
| SCL | I2C clock line (any GPIO) |
| VDD | 3.3V |
| VDDH | Same supply as VDD, connected close to the sensor |
| GND | GND |
The I2C address is fixed at `0x62`. Connect SDA/SCL to whichever GPIOs you configure for your I2C master bus.
The datasheet recommends external pull-up resistors on SDA and SCL (e.g. 10 kΩ) and a low-noise supply able to deliver the peak current (up to 205 mA at 3.3 V). Breakout boards usually include the pull-ups and the VDD/VDDH connection.
## Quick Start
```c
#include "scd4x.h"
// After initializing the I2C bus and adding device at address SCD4X_I2C_ADDR:
scd4x_t *scd4x = scd4x_init(dev_handle);
scd4x_start_periodic_measurement(scd4x);
while (true) {
scd4x_measurement_t m = {0};
esp_err_t ret = scd4x_read_measurement(scd4x, &m);
if (ret == ESP_OK) {
printf("CO2=%.0f ppm T=%.2f °C RH=%.2f %%\n",
m.co2, m.temperature, m.humidity);
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
```
See [examples/simple_main](examples/simple_main) for a complete project. Build it from its directory with `idf.py set-target esp32 build`, or create a copy with `idf.py create-project-from-example "jef-sure/scd4x:simple_main"`. Pins 25 and 26 are set at the top of [simple_main.c](examples/simple_main/main/simple_main.c).
## API Reference
### Types
```c
typedef enum __attribute__((packed)) {
SCD4X_MODE_IDLE = 0,
SCD4X_MODE_PERIODIC = 1,
SCD4X_MODE_LOW_POWER_PERIODIC = 2,
SCD4X_MODE_SLEEP = 3,
} scd4x_mode_t;
typedef struct {
i2c_master_dev_handle_t i2c_dev;
scd4x_mode_t mode;
scd4x_variant_t variant;
TickType_t last_data_tick;
} scd4x_t;
typedef struct {
float co2; // CO2 in ppm
float temperature; // Temperature in °C
float humidity; // Relative humidity in %RH
} scd4x_measurement_t;
```
### Initialization
| Function | Description |
|----------|-------------|
| `scd4x_init()` | Initialize sensor, stop any active measurement, read the variant, allocate device handle |
| `scd4x_deinit()` | Stop active periodic measurement, release the driver context, and clear the caller's pointer; does not remove the caller-owned I2C device |
### Measurement
| Function | Description |
|----------|-------------|
| `scd4x_start_periodic_measurement()` | Start periodic measurement (5 s interval) |
| `scd4x_start_low_power_periodic_measurement()` | Start low-power periodic measurement (~30 s interval) |
| `scd4x_stop_periodic_measurement()` | Stop periodic measurement, return to idle |
| `scd4x_read_measurement()` | Read CO2, temperature, humidity. Returns `ESP_ERR_NOT_FINISHED` if not ready, `ESP_ERR_TIMEOUT` if a stalled measurement was restarted |
| `scd4x_data_ready()` | Check if new data is available. Returns `ESP_ERR_TIMEOUT` if a stalled measurement was restarted |
| `scd4x_measure_single_shot()` | Single-shot CO2 + T + RH measurement (SCD41 / SCD43, blocks 5 s) |
| `scd4x_measure_single_shot_rht_only()` | Single-shot T + RH only (SCD41 / SCD43, blocks 50 ms) |
### Configuration
| Function | Description |
|----------|-------------|
| `scd4x_set_temperature_offset()` | Set temperature offset for compensation (0–175 °C, recommended 0–20 °C) |
| `scd4x_get_temperature_offset()` | Read current temperature offset |
| `scd4x_set_sensor_altitude()` | Set altitude for pressure compensation (0..3000 m) |
| `scd4x_get_sensor_altitude()` | Read configured altitude |
| `scd4x_set_ambient_pressure()` | Set ambient pressure in hPa (700..1200, can be sent during measurement) |
| `scd4x_get_ambient_pressure()` | Read configured ambient pressure |
| `scd4x_persist_settings()` | Save configuration to EEPROM (limited write cycles) |
### Calibration
| Function | Description |
|----------|-------------|
| `scd4x_perform_forced_recalibration()` | Forced recalibration to known CO2 reference |
| `scd4x_set_automatic_self_calibration()` | Enable/disable ASC |
| `scd4x_get_automatic_self_calibration()` | Read ASC state |
| `scd4x_set_automatic_self_calibration_target()` | Set ASC target CO2 concentration in ppm (default 400) |
| `scd4x_get_automatic_self_calibration_target()` | Read ASC target CO2 concentration |
| `scd4x_set_automatic_self_calibration_initial_period()` | Set ASC initial learning period (hours, multiple of 4) |
| `scd4x_get_automatic_self_calibration_initial_period()` | Read ASC initial learning period |
| `scd4x_set_automatic_self_calibration_standard_period()` | Set ASC standard learning period (hours, multiple of 4) |
| `scd4x_get_automatic_self_calibration_standard_period()` | Read ASC standard learning period |
### Utility
| Function | Description |
|----------|-------------|
| `scd4x_get_serial_number()` | Read 48-bit sensor serial number |
| `scd4x_perform_self_test()` | Run built-in self-test |
| `scd4x_perform_factory_reset()` | Reset all settings to factory defaults |
| `scd4x_reinit()` | Reload settings from EEPROM |
| `scd4x_get_sensor_variant()` | Read sensor variant (SCD40 / SCD41 / SCD43) |
| `scd4x_power_down()` | Enter sleep mode (ASC is not available in power-cycled single-shot operation) |
| `scd4x_wake_up()` | Wake from sleep mode and verify the sensor responds |
## Testing
The host tests run the driver against a model of the sensor built from the datasheet. They need no hardware and no ESP-IDF build, only the Unity sources:
```bash
make -C host_test test IDF_PATH=~/esp/esp-idf
```
## License
MIT — see [LICENSE](LICENSE).
idf.py add-dependency "jef-sure/scd4x^0.3.0"