From 2efc8598e39f6b4f78605221223c051e316e481f Mon Sep 17 00:00:00 2001 From: Jan Faeh Date: Wed, 28 Aug 2024 10:21:04 +0200 Subject: [PATCH] drivers: sensor: SCD4x Add driver This adds support for Sensirion's SCD4x co2 sensor. Signed-off-by: Jan Faeh --- drivers/sensor/sensirion/CMakeLists.txt | 1 + drivers/sensor/sensirion/Kconfig | 1 + drivers/sensor/sensirion/scd4x/CMakeLists.txt | 5 + drivers/sensor/sensirion/scd4x/Kconfig | 13 + drivers/sensor/sensirion/scd4x/scd4x.c | 908 ++++++++++++++++++ drivers/sensor/sensirion/scd4x/scd4x.h | 88 ++ dts/bindings/sensor/sensirion,scd40.yaml | 8 + dts/bindings/sensor/sensirion,scd41.yaml | 21 + include/zephyr/drivers/sensor/scd4x.h | 92 ++ tests/drivers/build_all/sensor/i2c.dtsi | 6 + 10 files changed, 1143 insertions(+) create mode 100644 drivers/sensor/sensirion/scd4x/CMakeLists.txt create mode 100644 drivers/sensor/sensirion/scd4x/Kconfig create mode 100644 drivers/sensor/sensirion/scd4x/scd4x.c create mode 100644 drivers/sensor/sensirion/scd4x/scd4x.h create mode 100644 dts/bindings/sensor/sensirion,scd40.yaml create mode 100644 dts/bindings/sensor/sensirion,scd41.yaml create mode 100644 include/zephyr/drivers/sensor/scd4x.h diff --git a/drivers/sensor/sensirion/CMakeLists.txt b/drivers/sensor/sensirion/CMakeLists.txt index 2ef59c627dc0a6..08d20a61ed87a4 100644 --- a/drivers/sensor/sensirion/CMakeLists.txt +++ b/drivers/sensor/sensirion/CMakeLists.txt @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # zephyr-keep-sorted-start +add_subdirectory_ifdef(CONFIG_SCD4X scd4x) add_subdirectory_ifdef(CONFIG_SGP40 sgp40) add_subdirectory_ifdef(CONFIG_SHT3XD sht3xd) add_subdirectory_ifdef(CONFIG_SHT4X sht4x) diff --git a/drivers/sensor/sensirion/Kconfig b/drivers/sensor/sensirion/Kconfig index 6d1f48dbe959e0..2b70b2e731d8a5 100644 --- a/drivers/sensor/sensirion/Kconfig +++ b/drivers/sensor/sensirion/Kconfig @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # zephyr-keep-sorted-start +source "drivers/sensor/sensirion/scd4x/Kconfig" source "drivers/sensor/sensirion/sgp40/Kconfig" source "drivers/sensor/sensirion/sht3xd/Kconfig" source "drivers/sensor/sensirion/sht4x/Kconfig" diff --git a/drivers/sensor/sensirion/scd4x/CMakeLists.txt b/drivers/sensor/sensirion/scd4x/CMakeLists.txt new file mode 100644 index 00000000000000..69f348fc202fa6 --- /dev/null +++ b/drivers/sensor/sensirion/scd4x/CMakeLists.txt @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() + +zephyr_library_sources(scd4x.c) diff --git a/drivers/sensor/sensirion/scd4x/Kconfig b/drivers/sensor/sensirion/scd4x/Kconfig new file mode 100644 index 00000000000000..09012fe6f5fc7d --- /dev/null +++ b/drivers/sensor/sensirion/scd4x/Kconfig @@ -0,0 +1,13 @@ +# Drivers configuration options for Sensirion SCD4x + +# Copyright (c) 2024 Jan Fäh +# SPDX-License-Identifier: Apache-2.0 + +config SCD4X + bool "SCD4x Carbon Dioxide Sensor" + default y + depends on DT_HAS_SENSIRION_SCD41_ENABLED || DT_HAS_SENSIRION_SCD40_ENABLED + select I2C + select CRC + help + Enable driver for the Sensirion SCD4x carbon dioxide sensors. diff --git a/drivers/sensor/sensirion/scd4x/scd4x.c b/drivers/sensor/sensirion/scd4x/scd4x.c new file mode 100644 index 00000000000000..0d1281af12a8e0 --- /dev/null +++ b/drivers/sensor/sensirion/scd4x/scd4x.c @@ -0,0 +1,908 @@ +/* + * Copyright (c) 2024 Jan Fäh + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "scd4x.h" + +LOG_MODULE_REGISTER(SCD4X, CONFIG_SENSOR_LOG_LEVEL); + +static uint8_t scd4x_calc_crc(uint16_t value) +{ + uint8_t buf[2]; + + sys_put_be16(value, buf); + + return crc8(buf, 2, SCD4X_CRC_POLY, SCD4X_CRC_INIT, false); +} + +static int scd4x_write_command(const struct device *dev, uint8_t cmd) +{ + const struct scd4x_config *cfg = dev->config; + uint8_t tx_buf[2]; + int ret; + + sys_put_be16(scd4x_cmds[cmd].cmd, tx_buf); + + ret = i2c_write_dt(&cfg->bus, tx_buf, sizeof(tx_buf)); + + if (scd4x_cmds[cmd].cmd_duration_ms) { + k_msleep(scd4x_cmds[cmd].cmd_duration_ms); + } + + return ret; +} + +static int scd4x_read_reg(const struct device *dev, uint8_t *rx_buf, uint8_t rx_buf_size) +{ + const struct scd4x_config *cfg = dev->config; + int ret; + + ret = i2c_read_dt(&cfg->bus, rx_buf, rx_buf_size); + if (ret < 0) { + LOG_ERR("Failed to read i2c data."); + return ret; + } + + for (uint8_t i = 0; i < (rx_buf_size / 3); i++) { + ret = scd4x_calc_crc(sys_get_be16(&rx_buf[i * 3])); + if (ret != rx_buf[(i * 3) + 2]) { + LOG_ERR("Invalid CRC."); + return -EIO; + } + } + + return 0; +} + +static int scd4x_write_reg(const struct device *dev, uint8_t cmd, uint16_t *data, uint8_t data_size) +{ + const struct scd4x_config *cfg = dev->config; + int ret; + uint8_t tx_buf[((data_size * 3) + 2)]; + + sys_put_be16(scd4x_cmds[cmd].cmd, tx_buf); + + uint8_t tx_buf_pos = 2; + + for (uint8_t i = 0; i < data_size; i++) { + sys_put_be16(data[i], &tx_buf[tx_buf_pos]); + tx_buf_pos += 2; + tx_buf[tx_buf_pos++] = scd4x_calc_crc(data[i]); + } + + ret = i2c_write_dt(&cfg->bus, tx_buf, sizeof(tx_buf)); + if (ret < 0) { + LOG_ERR("Failed to write i2c data."); + return ret; + } + + if (scd4x_cmds[cmd].cmd_duration_ms) { + k_msleep(scd4x_cmds[cmd].cmd_duration_ms); + } + return 0; +} + +static int scd4x_data_ready(const struct device *dev, bool *is_data_ready) +{ + uint8_t rx_data[3]; + int ret; + *is_data_ready = false; + + ret = scd4x_write_command(dev, SCD4X_CMD_GET_DATA_READY_STATUS); + if (ret < 0) { + LOG_ERR("Failed to write get_data_ready_status command."); + return ret; + } + + ret = scd4x_read_reg(dev, rx_data, sizeof(rx_data)); + if (ret < 0) { + LOG_ERR("Failed to read get_data_ready_status register."); + return ret; + } + + uint16_t word = sys_get_be16(rx_data); + /* Least significant 11 bits = 0 --> data not ready */ + if ((word & 0x07FF) > 0) { + *is_data_ready = true; + } + + return 0; +} + +static int scd4x_read_sample(const struct device *dev) +{ + struct scd4x_data *data = dev->data; + uint8_t rx_data[9]; + int ret; + + ret = scd4x_write_command(dev, SCD4X_CMD_READ_MEASUREMENT); + if (ret < 0) { + LOG_ERR("Failed to write read_measurement command."); + return ret; + } + + ret = scd4x_read_reg(dev, rx_data, sizeof(rx_data)); + if (ret < 0) { + LOG_ERR("Failed to read read_measurement register."); + return ret; + } + + data->co2_sample = sys_get_be16(rx_data); + data->temp_sample = sys_get_be16(&rx_data[3]); + data->humi_sample = sys_get_be16(&rx_data[6]); + + return 0; +} + +static int scd4x_setup_measurement(const struct device *dev) +{ + const struct scd4x_config *cfg = dev->config; + int ret; + + switch ((enum scd4x_mode_t)cfg->mode) { + case SCD4X_MODE_NORMAL: + ret = scd4x_write_command(dev, SCD4X_CMD_START_PERIODIC_MEASUREMENT); + if (ret < 0) { + LOG_ERR("Failed to write start_periodic_measurement command."); + return ret; + } + break; + case SCD4X_MODE_LOW_POWER: + ret = scd4x_write_command(dev, SCD4X_CMD_LOW_POWER_PERIODIC_MEASUREMENT); + if (ret < 0) { + LOG_ERR("Failed to write start_low_power_periodic_measurement command."); + return ret; + } + break; + case SCD4X_MODE_SINGLE_SHOT: + ret = scd4x_write_command(dev, SCD4X_CMD_POWER_DOWN); + if (ret < 0) { + LOG_ERR("Failed to write power_down command."); + return ret; + } + break; + default: + return -EINVAL; + } + return 0; +} + +static int scd4x_set_idle_mode(const struct device *dev) +{ + const struct scd4x_config *cfg = dev->config; + int ret; + + if (cfg->mode == SCD4X_MODE_SINGLE_SHOT) { + /*send wake up command twice because of an expected nack return in power down mode*/ + scd4x_write_command(dev, SCD4X_CMD_WAKE_UP); + ret = scd4x_write_command(dev, SCD4X_CMD_WAKE_UP); + if (ret < 0) { + LOG_ERR("Failed write wake_up command."); + return ret; + } + } else { + ret = scd4x_write_command(dev, SCD4X_CMD_STOP_PERIODIC_MEASUREMENT); + if (ret < 0) { + LOG_ERR("Failed to write stop_periodic_measurement command."); + return ret; + } + } + + return 0; +} + +static int scd4x_set_temperature_offset(const struct device *dev, const struct sensor_value *val) +{ + int ret; + /*Calculation from Datasheet*/ + uint16_t offset_temp = + (float)(val->val1 + (val->val2 / 1000000.0)) * 0xFFFF / SCD4X_MAX_TEMP; + + ret = scd4x_write_reg(dev, SCD4X_CMD_SET_TEMPERATURE_OFFSET, &offset_temp, 1); + if (ret < 0) { + LOG_ERR("Failed to write set_temperature_offset register."); + return ret; + } + + return 0; +} + +static int scd4x_set_sensor_altitude(const struct device *dev, const struct sensor_value *val) +{ + int ret; + uint16_t altitude = val->val1; + + ret = scd4x_write_reg(dev, SCD4X_CMD_SET_SENSOR_ALTITUDE, &altitude, 1); + if (ret < 0) { + LOG_ERR("Failed to write set_sensor_altitude register."); + return ret; + } + return 0; +} + +static int scd4x_set_ambient_pressure(const struct device *dev, const struct sensor_value *val) +{ + int ret; + + uint16_t ambient_pressure = val->val1; + + ret = scd4x_write_reg(dev, SCD4X_CMD_SET_AMBIENT_PRESSURE, &ambient_pressure, 1); + if (ret < 0) { + LOG_ERR("Failed to write set_ambient_pressure register."); + return ret; + } + + return 0; +} + +static int scd4x_set_automatic_calib_enable(const struct device *dev, + const struct sensor_value *val) +{ + int ret; + uint16_t automatic_calib_enable = val->val1; + + ret = scd4x_write_reg(dev, SCD4X_CMD_SET_AUTOMATIC_CALIB_ENABLE, &automatic_calib_enable, + 1); + if (ret < 0) { + LOG_ERR("Failed to write set_automatic_calibration_enable register."); + return ret; + } + + return 0; +} + +static int scd4x_set_self_calib_initial_period(const struct device *dev, + const struct sensor_value *val) +{ + int ret; + uint16_t initial_period = val->val1; + + ret = scd4x_write_reg(dev, SCD4X_CMD_SET_SELF_CALIB_INITIAL_PERIOD, &initial_period, 1); + if (ret < 0) { + LOG_ERR("Failed to write set_automatic_self_calibration_initial_period register."); + return ret; + } + + return 0; +} + +static int scd4x_set_self_calib_standard_period(const struct device *dev, + const struct sensor_value *val) +{ + int ret; + uint16_t standard_period = val->val1; + + ret = scd4x_write_reg(dev, SCD4X_CMD_SET_SELF_CALIB_STANDARD_PERIOD, &standard_period, 1); + if (ret < 0) { + LOG_ERR("Failed to write set_automatic_self_calibration_standard_period register."); + return ret; + } + + return 0; +} + +static int scd4x_get_temperature_offset(const struct device *dev, struct sensor_value *val) +{ + int ret; + uint8_t rx_buf[3]; + + ret = scd4x_write_command(dev, SCD4X_CMD_GET_TEMPERATURE_OFFSET); + if (ret < 0) { + LOG_ERR("Failed to write get_temperature_offset command."); + return ret; + } + + ret = scd4x_read_reg(dev, rx_buf, sizeof(rx_buf)); + if (ret < 0) { + LOG_ERR("Failed to read get_temperature_offset register."); + return ret; + } + + int32_t temp; + + /*Calculation from Datasheet*/ + temp = sys_get_be16(rx_buf) * SCD4X_MAX_TEMP; + val->val1 = (int32_t)(temp / 0xFFFF); + val->val2 = ((temp % 0xFFFF) * 1000000) / 0xFFFF; + + return 0; +} + +static int scd4x_get_sensor_altitude(const struct device *dev, struct sensor_value *val) +{ + int ret; + uint8_t rx_buf[3]; + + ret = scd4x_write_command(dev, SCD4X_CMD_GET_SENSOR_ALTITUDE); + if (ret < 0) { + LOG_ERR("Failed to write get_sensor_altitude command."); + return ret; + } + + ret = scd4x_read_reg(dev, rx_buf, sizeof(rx_buf)); + if (ret < 0) { + LOG_ERR("Failed to read get_sensor_altitude register."); + return ret; + } + + val->val1 = sys_get_be16(rx_buf); + val->val2 = 0; + + return 0; +} + +static int scd4x_get_ambient_pressure(const struct device *dev, struct sensor_value *val) +{ + int ret; + uint8_t rx_buf[3]; + + ret = scd4x_write_command(dev, SCD4X_CMD_GET_AMBIENT_PRESSURE); + if (ret < 0) { + LOG_ERR("Failed to write get_ambient_pressure command."); + return ret; + } + + ret = scd4x_read_reg(dev, rx_buf, sizeof(rx_buf)); + if (ret < 0) { + LOG_ERR("Failed to read get_ambient_pressure register."); + return ret; + } + + val->val1 = sys_get_be16(rx_buf); + val->val2 = 0; + + return 0; +} + +static int scd4x_get_automatic_calib_enable(const struct device *dev, struct sensor_value *val) +{ + int ret; + uint8_t rx_buf[3]; + + ret = scd4x_write_command(dev, SCD4X_CMD_GET_AUTOMATIC_CALIB_ENABLE); + if (ret < 0) { + LOG_ERR("Failed to write get_automatic_calibration_enable command."); + return ret; + } + + ret = scd4x_read_reg(dev, rx_buf, sizeof(rx_buf)); + if (ret < 0) { + LOG_ERR("Failed to read get_automatic_calibration_enabled register."); + return ret; + } + + val->val1 = sys_get_be16(rx_buf); + val->val2 = 0; + + return 0; +} + +static int scd4x_get_self_calib_initial_period(const struct device *dev, struct sensor_value *val) +{ + int ret; + uint8_t rx_buf[3]; + + ret = scd4x_write_command(dev, SCD4X_CMD_GET_SELF_CALIB_INITIAL_PERIOD); + if (ret < 0) { + LOG_ERR("Failed to write get_automati_calibration_initial_period command."); + return ret; + } + + ret = scd4x_read_reg(dev, rx_buf, sizeof(rx_buf)); + if (ret < 0) { + LOG_ERR("Failed to read get_automatic_calibration_initial_period register."); + return ret; + } + + val->val1 = sys_get_be16(rx_buf); + val->val2 = 0; + + return 0; +} + +static int scd4x_get_self_calib_standard_period(const struct device *dev, struct sensor_value *val) +{ + int ret; + uint8_t rx_buf[3]; + + ret = scd4x_write_command(dev, SCD4X_CMD_GET_SELF_CALIB_STANDARD_PERIOD); + if (ret < 0) { + LOG_ERR("Failed to write get_automatic_self_calibration_standard_period command."); + return ret; + } + + ret = scd4x_read_reg(dev, rx_buf, sizeof(rx_buf)); + if (ret < 0) { + LOG_ERR("Failed to read get_automatic_self_calibration_standard_period register."); + return ret; + } + + val->val1 = sys_get_be16(rx_buf); + val->val2 = 0; + + return 0; +} + +int scd4x_forced_recalibration(const struct device *dev, uint16_t target_concentration, + uint16_t *frc_correction) +{ + uint8_t rx_buf[3]; + int ret; + + ret = scd4x_set_idle_mode(dev); + if (ret < 0) { + LOG_ERR("Failed to set idle mode."); + return ret; + } + + ret = scd4x_write_reg(dev, SCD4X_CMD_FORCED_RECALIB, &target_concentration, 1); + if (ret < 0) { + LOG_ERR("Failed to write perform_forced_recalibration register."); + return ret; + } + + ret = scd4x_read_reg(dev, rx_buf, sizeof(rx_buf)); + if (ret < 0) { + LOG_ERR("Failed to read perform_forced_recalibration register."); + return ret; + } + + *frc_correction = sys_get_be16(rx_buf); + + /*from datasheet*/ + if (*frc_correction == 0xFFFF) { + LOG_ERR("FRC failed. Returned 0xFFFF."); + return -EIO; + } + + *frc_correction -= 0x8000; + + ret = scd4x_setup_measurement(dev); + if (ret < 0) { + LOG_ERR("Failed to setup measurement."); + return ret; + } + + return 0; +} + +int scd4x_self_test(const struct device *dev) +{ + int ret; + uint8_t rx_buf[3]; + + ret = scd4x_set_idle_mode(dev); + if (ret < 0) { + LOG_ERR("Failed to set idle mode."); + return ret; + } + + ret = scd4x_write_command(dev, SCD4X_CMD_SELF_TEST); + if (ret < 0) { + LOG_ERR("Failed to write perform_self_test command."); + return ret; + } + + ret = scd4x_read_reg(dev, rx_buf, sizeof(rx_buf)); + if (ret < 0) { + LOG_ERR("Failed to read perform_self_test register."); + return ret; + } + + uint16_t is_malfunction = sys_get_be16(rx_buf); + + if (is_malfunction) { + LOG_ERR("malfunction detected."); + return -EIO; + } + + ret = scd4x_setup_measurement(dev); + if (ret < 0) { + LOG_ERR("Failed to setup measurement."); + return ret; + } + + return 0; +} + +int scd4x_persist_settings(const struct device *dev) +{ + int ret; + + ret = scd4x_set_idle_mode(dev); + if (ret < 0) { + LOG_ERR("Failed to set idle mode."); + return ret; + } + + ret = scd4x_write_command(dev, SCD4X_CMD_PERSIST_SETTINGS); + if (ret < 0) { + LOG_ERR("Failed to write persist_settings command."); + return ret; + } + + ret = scd4x_setup_measurement(dev); + if (ret < 0) { + LOG_ERR("Failed to setup measurement."); + return ret; + } + + return 0; +} + +int scd4x_factory_reset(const struct device *dev) +{ + int ret; + + ret = scd4x_set_idle_mode(dev); + if (ret < 0) { + LOG_ERR("Failed to set idle mode."); + return ret; + } + + ret = scd4x_write_command(dev, SCD4X_CMD_FACTORY_RESET); + if (ret < 0) { + LOG_ERR("Failed to write perfom_factory_reset command."); + return ret; + } + + ret = scd4x_setup_measurement(dev); + if (ret < 0) { + LOG_ERR("Failed to setup measurement."); + return ret; + } + + return 0; +} + +static int scd4x_sample_fetch(const struct device *dev, enum sensor_channel chan) +{ + const struct scd4x_config *cfg = dev->config; + bool is_data_ready; + int ret; + + if (chan != SENSOR_CHAN_ALL && chan != SENSOR_CHAN_AMBIENT_TEMP && + chan != SENSOR_CHAN_HUMIDITY && chan != SENSOR_CHAN_CO2) { + return -ENOTSUP; + } + + if (cfg->mode == SCD4X_MODE_SINGLE_SHOT) { + ret = scd4x_set_idle_mode(dev); + if (ret < 0) { + LOG_ERR("Failed to set idle mode."); + return ret; + } + + if (chan == SENSOR_CHAN_HUMIDITY || chan == SENSOR_CHAN_AMBIENT_TEMP) { + ret = scd4x_write_command(dev, SCD4X_CMD_MEASURE_SINGLE_SHOT_RHT); + if (ret < 0) { + LOG_ERR("Failed to write measure_single_shot_rht_only command."); + return ret; + } + } else { + ret = scd4x_write_command(dev, SCD4X_CMD_MEASURE_SINGLE_SHOT); + if (ret < 0) { + LOG_ERR("Failed to write measure_single_shot command."); + return ret; + } + } + } else { + ret = scd4x_data_ready(dev, &is_data_ready); + if (ret < 0) { + LOG_ERR("Failed to check data ready."); + return ret; + } + if (!is_data_ready) { + return 0; + } + } + + ret = scd4x_read_sample(dev); + if (ret < 0) { + LOG_ERR("Failed to get sample data."); + return ret; + } + + if (cfg->mode == SCD4X_MODE_SINGLE_SHOT) { + ret = scd4x_setup_measurement(dev); + if (ret < 0) { + LOG_ERR("Failed to setup measurement."); + return ret; + } + } + return 0; +} + +static int scd4x_channel_get(const struct device *dev, enum sensor_channel chan, + struct sensor_value *val) +{ + const struct scd4x_data *data = dev->data; + int32_t tmp_val; + + switch ((enum sensor_channel)chan) { + case SENSOR_CHAN_AMBIENT_TEMP: + /*Calculation from Datasheet*/ + tmp_val = data->temp_sample * SCD4X_MAX_TEMP; + val->val1 = (int32_t)(tmp_val / 0xFFFF) + SCD4X_MIN_TEMP; + val->val2 = ((tmp_val % 0xFFFF) * 1000000) / 0xFFFF; + break; + case SENSOR_CHAN_HUMIDITY: + /*Calculation from Datasheet*/ + tmp_val = data->humi_sample * 100; + val->val1 = (int32_t)(tmp_val / 0xFFFF); + val->val2 = ((tmp_val % 0xFFFF) * 1000000) / 0xFFFF; + break; + case SENSOR_CHAN_CO2: + val->val1 = data->co2_sample; + val->val2 = 0; + break; + default: + return -ENOTSUP; + } + return 0; +} + +int scd4x_attr_set(const struct device *dev, enum sensor_channel chan, enum sensor_attribute attr, + const struct sensor_value *val) +{ + const struct scd4x_config *cfg = dev->config; + int ret; + + if (chan != SENSOR_CHAN_ALL && chan != SENSOR_CHAN_AMBIENT_TEMP && + chan != SENSOR_CHAN_HUMIDITY && chan != SENSOR_CHAN_CO2) { + return -ENOTSUP; + } + + if ((enum sensor_attribute_scd4x)attr != SENSOR_ATTR_SCD4X_AMBIENT_PRESSURE) { + ret = scd4x_set_idle_mode(dev); + if (ret < 0) { + LOG_ERR("Failed to set idle mode."); + return ret; + } + } + + if (val->val1 < 0 || val->val2 < 0) { + return -EINVAL; + } + + switch ((enum sensor_attribute_scd4x)attr) { + case SENSOR_ATTR_SCD4X_TEMPERATURE_OFFSET: + if (val->val1 > SCD4X_TEMPERATURE_OFFSET_IDX_MAX) { + return -EINVAL; + } + ret = scd4x_set_temperature_offset(dev, val); + if (ret < 0) { + LOG_ERR("Failed to set temperature offset."); + return ret; + } + break; + case SENSOR_ATTR_SCD4X_SENSOR_ALTITUDE: + if (val->val1 > SCD4X_SENSOR_ALTITUDE_IDX_MAX) { + return -EINVAL; + } + ret = scd4x_set_sensor_altitude(dev, val); + if (ret < 0) { + LOG_ERR("Failed to set sensor altitude."); + return ret; + } + break; + case SENSOR_ATTR_SCD4X_AMBIENT_PRESSURE: + if (val->val1 > SCD4X_AMBIENT_PRESSURE_IDX_MAX || val->val1 < 700) { + return -EINVAL; + } + ret = scd4x_set_ambient_pressure(dev, val); + if (ret < 0) { + LOG_ERR("Failed to set ambient pressure."); + return ret; + } + /* return 0 to not call scd4x_start_measurement */ + return 0; + case SENSOR_ATTR_SCD4X_AUTOMATIC_CALIB_ENABLE: + if (val->val1 > SCD4X_BOOL_IDX_MAX) { + return -EINVAL; + } + ret = scd4x_set_automatic_calib_enable(dev, val); + if (ret < 0) { + LOG_ERR("Failed to set automatic calib enable."); + return ret; + } + break; + case SENSOR_ATTR_SCD4X_SELF_CALIB_INITIAL_PERIOD: + if (val->val1 % 4) { + return -EINVAL; + } + if (cfg->model == SCD4X_MODEL_SCD40) { + LOG_ERR("SELF_CALIB_INITIAL_PERIOD not available for SCD40."); + return -ENOTSUP; + } + ret = scd4x_set_self_calib_initial_period(dev, val); + if (ret < 0) { + LOG_ERR("Failed to set self calib initial period."); + return ret; + } + break; + case SENSOR_ATTR_SCD4X_SELF_CALIB_STANDARD_PERIOD: + if (val->val1 % 4) { + return -EINVAL; + } + if (cfg->model == SCD4X_MODEL_SCD40) { + LOG_ERR("SELF_CALIB_STANDARD_PERIOD not available for SCD40."); + return -ENOTSUP; + } + ret = scd4x_set_self_calib_standard_period(dev, val); + if (ret < 0) { + LOG_ERR("Failed to set self calib standard period."); + return ret; + } + break; + default: + return -ENOTSUP; + } + + ret = scd4x_setup_measurement(dev); + if (ret < 0) { + LOG_ERR("Failed to setup measurement."); + return ret; + } + + return 0; +} + +static int scd4x_attr_get(const struct device *dev, enum sensor_channel chan, + enum sensor_attribute attr, struct sensor_value *val) +{ + const struct scd4x_config *cfg = dev->config; + int ret; + + if (chan != SENSOR_CHAN_ALL && chan != SENSOR_CHAN_AMBIENT_TEMP && + chan != SENSOR_CHAN_HUMIDITY && chan != SENSOR_CHAN_CO2) { + return -ENOTSUP; + } + + if ((enum sensor_attribute_scd4x)attr != SENSOR_ATTR_SCD4X_AMBIENT_PRESSURE || + cfg->mode == SCD4X_MODE_SINGLE_SHOT) { + ret = scd4x_set_idle_mode(dev); + if (ret < 0) { + LOG_ERR("Failed to set idle mode."); + return ret; + } + } + + switch ((enum sensor_attribute_scd4x)attr) { + case SENSOR_ATTR_SCD4X_TEMPERATURE_OFFSET: + ret = scd4x_get_temperature_offset(dev, val); + if (ret < 0) { + LOG_ERR("Failed to get temperature offset."); + return ret; + } + break; + case SENSOR_ATTR_SCD4X_SENSOR_ALTITUDE: + ret = scd4x_get_sensor_altitude(dev, val); + if (ret < 0) { + LOG_ERR("Failed to get sensor altitude."); + return ret; + } + break; + case SENSOR_ATTR_SCD4X_AMBIENT_PRESSURE: + ret = scd4x_get_ambient_pressure(dev, val); + if (ret < 0) { + LOG_ERR("Failed to get ambient pressure."); + return ret; + } + /* return 0 to not call scd4x_setup_measurement */ + return 0; + case SENSOR_ATTR_SCD4X_AUTOMATIC_CALIB_ENABLE: + ret = scd4x_get_automatic_calib_enable(dev, val); + if (ret < 0) { + LOG_ERR("Failed to get automatic calib."); + return ret; + } + break; + case SENSOR_ATTR_SCD4X_SELF_CALIB_INITIAL_PERIOD: + if (cfg->model == SCD4X_MODEL_SCD40) { + LOG_ERR("SELF_CALIB_INITIAL_PERIOD not available for SCD40."); + return -ENOTSUP; + } + ret = scd4x_get_self_calib_initial_period(dev, val); + if (ret < 0) { + LOG_ERR("Failed to set get self calib initial period."); + return ret; + } + break; + case SENSOR_ATTR_SCD4X_SELF_CALIB_STANDARD_PERIOD: + if (cfg->model == SCD4X_MODEL_SCD40) { + LOG_ERR("SELF_CALIB_STANDARD_PERIOD not available for SCD40."); + return -ENOTSUP; + } + ret = scd4x_get_self_calib_standard_period(dev, val); + if (ret < 0) { + LOG_ERR("Failed to set get self calib standard period."); + return ret; + } + break; + default: + return -ENOTSUP; + } + + ret = scd4x_setup_measurement(dev); + if (ret < 0) { + LOG_ERR("Failed to setup measurement."); + return ret; + } + + return 0; +} + +static int scd4x_init(const struct device *dev) +{ + const struct scd4x_config *cfg = dev->config; + int ret; + + if (!i2c_is_ready_dt(&cfg->bus)) { + LOG_ERR("Device not ready."); + return -ENODEV; + } + + ret = scd4x_write_command(dev, SCD4X_CMD_STOP_PERIODIC_MEASUREMENT); + if (ret < 0) { + /*send wake up command twice because of an expected nack return in power down mode*/ + scd4x_write_command(dev, SCD4X_CMD_WAKE_UP); + ret = scd4x_write_command(dev, SCD4X_CMD_WAKE_UP); + if (ret < 0) { + LOG_ERR("Failed to put the device in idle mode."); + return ret; + } + } + + ret = scd4x_write_command(dev, SCD4X_CMD_REINIT); + if (ret < 0) { + LOG_ERR("Failed to reset the device."); + return ret; + } + + ret = scd4x_setup_measurement(dev); + if (ret < 0) { + LOG_ERR("Failed to setup measurement."); + return ret; + } + return 0; +} + +static const struct sensor_driver_api scd4x_api_funcs = { + .sample_fetch = scd4x_sample_fetch, + .channel_get = scd4x_channel_get, + .attr_set = scd4x_attr_set, + .attr_get = scd4x_attr_get, +}; + +#define SCD4X_INIT(inst, scd4x_model) \ + static struct scd4x_data scd4x_data_##inst; \ + static const struct scd4x_config scd4x_config_##inst = { \ + .bus = I2C_DT_SPEC_INST_GET(inst), \ + .model = scd4x_model, \ + .mode = DT_INST_ENUM_IDX_OR(inst, mode, SCD4X_MODE_NORMAL), \ + }; \ + SENSOR_DEVICE_DT_INST_DEFINE(inst, scd4x_init, NULL, &scd4x_data_##inst, \ + &scd4x_config_##inst, POST_KERNEL, \ + CONFIG_SENSOR_INIT_PRIORITY, &scd4x_api_funcs); + +#define DT_DRV_COMPAT sensirion_scd40 +DT_INST_FOREACH_STATUS_OKAY_VARGS(SCD4X_INIT, SCD4X_MODEL_SCD40) +#undef DT_DRV_COMPAT + +#define DT_DRV_COMPAT sensirion_scd41 +DT_INST_FOREACH_STATUS_OKAY_VARGS(SCD4X_INIT, SCD4X_MODEL_SCD41) +#undef DT_DRV_COMPAT diff --git a/drivers/sensor/sensirion/scd4x/scd4x.h b/drivers/sensor/sensirion/scd4x/scd4x.h new file mode 100644 index 00000000000000..3955d2021990fb --- /dev/null +++ b/drivers/sensor/sensirion/scd4x/scd4x.h @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 Jan Fäh + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_SENSOR_SCD4X_SCD4X_H_ +#define ZEPHYR_DRIVERS_SENSOR_SCD4X_SCD4X_H_ + +#include + +#define SCD4X_CMD_REINIT 0 +#define SCD4X_CMD_START_PERIODIC_MEASUREMENT 1 +#define SCD4X_CMD_STOP_PERIODIC_MEASUREMENT 2 +#define SCD4X_CMD_READ_MEASUREMENT 3 +#define SCD4X_CMD_SET_TEMPERATURE_OFFSET 4 +#define SCD4X_CMD_GET_TEMPERATURE_OFFSET 5 +#define SCD4X_CMD_SET_SENSOR_ALTITUDE 6 +#define SCD4X_CMD_GET_SENSOR_ALTITUDE 7 +#define SCD4X_CMD_SET_AMBIENT_PRESSURE 8 +#define SCD4X_CMD_GET_AMBIENT_PRESSURE 9 +#define SCD4X_CMD_FORCED_RECALIB 10 +#define SCD4X_CMD_SET_AUTOMATIC_CALIB_ENABLE 11 +#define SCD4X_CMD_GET_AUTOMATIC_CALIB_ENABLE 12 +#define SCD4X_CMD_LOW_POWER_PERIODIC_MEASUREMENT 13 +#define SCD4X_CMD_GET_DATA_READY_STATUS 14 +#define SCD4X_CMD_PERSIST_SETTINGS 15 +#define SCD4X_CMD_SELF_TEST 16 +#define SCD4X_CMD_FACTORY_RESET 17 +#define SCD4X_CMD_MEASURE_SINGLE_SHOT 18 +#define SCD4X_CMD_MEASURE_SINGLE_SHOT_RHT 19 +#define SCD4X_CMD_POWER_DOWN 10 +#define SCD4X_CMD_WAKE_UP 21 +#define SCD4X_CMD_SET_SELF_CALIB_INITIAL_PERIOD 22 +#define SCD4X_CMD_GET_SELF_CALIB_INITIAL_PERIOD 23 +#define SCD4X_CMD_SET_SELF_CALIB_STANDARD_PERIOD 24 +#define SCD4X_CMD_GET_SELF_CALIB_STANDARD_PERIOD 25 + +#define SCD4X_CRC_POLY 0x31 +#define SCD4X_CRC_INIT 0xFF + +#define SCD4X_STARTUP_TIME_MS 30 + +#define SCD4X_TEMPERATURE_OFFSET_IDX_MAX 20 +#define SCD4X_SENSOR_ALTITUDE_IDX_MAX 3000 +#define SCD4X_AMBIENT_PRESSURE_IDX_MAX 1200 +#define SCD4X_BOOL_IDX_MAX 1 + +#define SCD4X_MAX_TEMP 175 +#define SCD4X_MIN_TEMP -45 + +enum scd4x_model_t { + SCD4X_MODEL_SCD40, + SCD4X_MODEL_SCD41, +}; + +enum scd4x_mode_t { + SCD4X_MODE_NORMAL, + SCD4X_MODE_LOW_POWER, + SCD4X_MODE_SINGLE_SHOT, +}; + +struct scd4x_config { + struct i2c_dt_spec bus; + enum scd4x_model_t model; + enum scd4x_mode_t mode; +}; + +struct scd4x_data { + uint16_t temp_sample; + uint16_t humi_sample; + uint16_t co2_sample; +}; + +struct cmds_t { + uint16_t cmd; + uint16_t cmd_duration_ms; +}; + +const struct cmds_t scd4x_cmds[] = { + {0x3646, 30}, {0x21B1, 0}, {0x3F86, 500}, {0xEC05, 1}, {0x241D, 1}, {0x2318, 1}, + {0x2427, 1}, {0x2322, 1}, {0xE000, 1}, {0xE000, 1}, {0x362F, 400}, {0x2416, 1}, + {0x2313, 1}, {0x21AC, 0}, {0xE4B8, 1}, {0x3615, 800}, {0x3639, 10000}, {0x3632, 1200}, + {0x219D, 5000}, {0x2196, 50}, {0x36E0, 1}, {0x36F6, 30}, {0x2445, 1}, {0x2340, 1}, + {0x244E, 1}, {0x234B, 1}, +}; + +#endif /* ZEPHYR_DRIVERS_SENSOR_SCD4X_SCD4X_H_ */ diff --git a/dts/bindings/sensor/sensirion,scd40.yaml b/dts/bindings/sensor/sensirion,scd40.yaml new file mode 100644 index 00000000000000..7f6bcaaff5d2fd --- /dev/null +++ b/dts/bindings/sensor/sensirion,scd40.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2024, Jan Fäh +# SPDX-License-Identifier: Apache-2.0 + +description: Sensirion SCD4x temperature sensor + +compatible: "sensirion,scd40" + +include: [sensor-device.yaml, i2c-device.yaml] diff --git a/dts/bindings/sensor/sensirion,scd41.yaml b/dts/bindings/sensor/sensirion,scd41.yaml new file mode 100644 index 00000000000000..e5a955d196b6fd --- /dev/null +++ b/dts/bindings/sensor/sensirion,scd41.yaml @@ -0,0 +1,21 @@ +# Copyright (c) 2024, Jan Fäh +# SPDX-License-Identifier: Apache-2.0 + +description: Sensirion SCD4x temperature sensor + +compatible: "sensirion,scd41" + +include: [sensor-device.yaml, i2c-device.yaml] + +properties: + mode: + type: int + required: true + description: | + - 0: Normal periodic measurement. Default interval of 5sec + - 1: Low power periodic measurement. Interval of 30sec + - 2: Singleshot measurement for low power usage. + enum: + - 0 + - 1 + - 2 diff --git a/include/zephyr/drivers/sensor/scd4x.h b/include/zephyr/drivers/sensor/scd4x.h new file mode 100644 index 00000000000000..edae9693d7ef5b --- /dev/null +++ b/include/zephyr/drivers/sensor/scd4x.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024 Jan Fäh + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_DRIVERS_SENSOR_SCD4X_H_ +#define ZEPHYR_INCLUDE_DRIVERS_SENSOR_SCD4X_H_ + +#include + +enum sensor_attribute_scd4x { + /* Offset temperature: Toffset_actual = Tscd4x – Treference + Toffset_previous + * 0 - 20°C + */ + SENSOR_ATTR_SCD4X_TEMPERATURE_OFFSET = SENSOR_ATTR_PRIV_START, + /* Altidude of the sensor; + * 0 - 3000m + */ + SENSOR_ATTR_SCD4X_SENSOR_ALTITUDE, + /* Ambient pressure in hPa + * 700 - 1200hPa + */ + SENSOR_ATTR_SCD4X_AMBIENT_PRESSURE, + /* Set the current state (enabled: 1 / disabled: 0). + * Default: enabled. + */ + SENSOR_ATTR_SCD4X_AUTOMATIC_CALIB_ENABLE, + /* Set the initial period for automatic self calibration correction in hours. Allowed values + * are integer multiples of 4 hours. + * Default: 44 + */ + SENSOR_ATTR_SCD4X_SELF_CALIB_INITIAL_PERIOD, + /* Set the standard period for automatic self calibration correction in hours. Allowed + * values are integer multiples of 4 hours. Default: 156 + */ + SENSOR_ATTR_SCD4X_SELF_CALIB_STANDARD_PERIOD, +}; + +/** + * @brief Performs a forced recalibration. + * + * Operate the SCD4x in the operation mode for at least 3 minutes in an environment with a + * homogeneous and constant CO2 concentration. Otherwise the recalibratioin will fail. The sensor + * must be operated at the voltage desired for the application when performing the FRC sequence. + * + * @param dev Pointer to the sensor device + * @param target_concentration Reference CO2 concentration. + * @param frc_correction Previous differences from the target concentration + * + * @return 0 if successful, negative errno code if failure. + */ +int scd4x_forced_recalibration(const struct device *dev, uint16_t target_concentration, + uint16_t *frc_correction); + +/** + * @brief Performs a self test. + * + * The self_test command can be used as an end-of-line test to check the sensor functionality + * + * @param dev Pointer to the sensor device + * + * @return 0 if successful, negative errno code if failure. + */ +int scd4x_self_test(const struct device *dev); + +/** + * @brief Performs a self test. + * + * The persist_settings command can be used to save the actual configuration. This command + * should only be sent when persistence is required and if actual changes to the configuration have + * been made. The EEPROM is guaranteed to withstand at least 2000 write cycles + * + * @param dev Pointer to the sensor device + * + * @return 0 if successful, negative errno code if failure. + */ +int scd4x_persist_settings(const struct device *dev); + +/** + * @brief Performs a factory reset. + * + * The perform_factory_reset command resets all configuration settings stored in the EEPROM and + * erases the FRC and ASC algorithm history. + * + * @param dev Pointer to the sensor device + * + * @return 0 if successful, negative errno code if failure. + */ +int scd4x_factory_reset(const struct device *dev); + +#endif /* ZEPHYR_INCLUDE_DRIVERS_SENSOR_SCD4X_H_ */ diff --git a/tests/drivers/build_all/sensor/i2c.dtsi b/tests/drivers/build_all/sensor/i2c.dtsi index 9b1a5cd1f95a28..7b5ef3e9294bf8 100644 --- a/tests/drivers/build_all/sensor/i2c.dtsi +++ b/tests/drivers/build_all/sensor/i2c.dtsi @@ -1131,3 +1131,9 @@ test_i2c_sts4x: sts4x@9d { reg = <0x99>; repeatability = <2>; }; + +test_i2c_scd4x: scd4x@9e { + compatible = "sensirion,scd41"; + reg = <0x9e>; + mode = <0>; +};