From 0d7cb32c58dce7396315c7c6fb7f67749e5ee1b3 Mon Sep 17 00:00:00 2001 From: Leonard Pollak Date: Wed, 28 Apr 2021 16:11:50 +0200 Subject: [PATCH] drivers: sensor: SGP40 Added support This adds support for Sensirion's SGP40 multipixel gas sensor. Signed-off-by: Leonard Pollak --- drivers/sensor/CMakeLists.txt | 1 + drivers/sensor/Kconfig | 2 + drivers/sensor/sgp40/CMakeLists.txt | 5 + drivers/sensor/sgp40/Kconfig | 10 + drivers/sensor/sgp40/sgp40.c | 306 +++++++++++++++++++++++ drivers/sensor/sgp40/sgp40.h | 56 +++++ dts/bindings/sensor/sensirion,sgp40.yaml | 17 ++ include/drivers/sensor/sgp40.h | 33 +++ tests/drivers/build_all/sensor/i2c.dtsi | 7 + tests/drivers/build_all/sensor/prj.conf | 1 + 10 files changed, 438 insertions(+) create mode 100644 drivers/sensor/sgp40/CMakeLists.txt create mode 100644 drivers/sensor/sgp40/Kconfig create mode 100644 drivers/sensor/sgp40/sgp40.c create mode 100644 drivers/sensor/sgp40/sgp40.h create mode 100644 dts/bindings/sensor/sensirion,sgp40.yaml create mode 100644 include/drivers/sensor/sgp40.h diff --git a/drivers/sensor/CMakeLists.txt b/drivers/sensor/CMakeLists.txt index 3d8d076103d..108865d68eb 100644 --- a/drivers/sensor/CMakeLists.txt +++ b/drivers/sensor/CMakeLists.txt @@ -70,6 +70,7 @@ add_subdirectory_ifdef(CONFIG_QDEC_NRFX qdec_nrfx) add_subdirectory_ifdef(CONFIG_QDEC_SAM qdec_sam) add_subdirectory_ifdef(CONFIG_TEMP_NRF5 nrf5) add_subdirectory_ifdef(CONFIG_SBS_GAUGE sbs_gauge) +add_subdirectory_ifdef(CONFIG_SGP40 sgp40) add_subdirectory_ifdef(CONFIG_SHTCX shtcx) add_subdirectory_ifdef(CONFIG_SHT3XD sht3xd) add_subdirectory_ifdef(CONFIG_SI7006 si7006) diff --git a/drivers/sensor/Kconfig b/drivers/sensor/Kconfig index b789adc2d37..b8bd85aff9c 100644 --- a/drivers/sensor/Kconfig +++ b/drivers/sensor/Kconfig @@ -180,6 +180,8 @@ source "drivers/sensor/qdec_sam/Kconfig" source "drivers/sensor/sbs_gauge/Kconfig" +source "drivers/sensor/sgp40/Kconfig" + source "drivers/sensor/shtcx/Kconfig" source "drivers/sensor/sht3xd/Kconfig" diff --git a/drivers/sensor/sgp40/CMakeLists.txt b/drivers/sensor/sgp40/CMakeLists.txt new file mode 100644 index 00000000000..03d2561f3ed --- /dev/null +++ b/drivers/sensor/sgp40/CMakeLists.txt @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() + +zephyr_library_sources(sgp40.c) diff --git a/drivers/sensor/sgp40/Kconfig b/drivers/sensor/sgp40/Kconfig new file mode 100644 index 00000000000..af3921f0cb0 --- /dev/null +++ b/drivers/sensor/sgp40/Kconfig @@ -0,0 +1,10 @@ +# SGP40 multipixel gas sensor configuration options + +# Copyright (c) 2021 Leonard Pollak +# SPDX-License-Identifier: Apache-2.0 + +config SGP40 + bool "SGP40 Multipixel Gas Sensor" + depends on I2C + help + Enable driver for SGP40 Multipixel Gas Sensor. diff --git a/drivers/sensor/sgp40/sgp40.c b/drivers/sensor/sgp40/sgp40.c new file mode 100644 index 00000000000..017ba7fffec --- /dev/null +++ b/drivers/sensor/sgp40/sgp40.c @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2021 Leonard Pollak + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT sensirion_sgp40 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include "sgp40.h" + +LOG_MODULE_REGISTER(SGP40, CONFIG_SENSOR_LOG_LEVEL); + +static uint8_t sgp40_compute_crc(uint16_t value) +{ + uint8_t buf[2]; + + sys_put_be16(value, buf); + + return crc8(buf, 2, SGP40_CRC_POLY, SGP40_CRC_INIT, false); +} + +static int sgp40_write_command(const struct device *dev, uint16_t cmd) +{ + + const struct sgp40_config *cfg = dev->config; + uint8_t tx_buf[2]; + + sys_put_be16(cmd, tx_buf); + + return i2c_write(cfg->bus, tx_buf, sizeof(tx_buf), + cfg->i2c_addr); +} + +static int sgp40_start_measurement(const struct device *dev) +{ + const struct sgp40_config *cfg = dev->config; + struct sgp40_data *data = dev->data; + uint8_t tx_buf[8]; + + sys_put_be16(SGP40_CMD_MEASURE_RAW, tx_buf); + sys_put_be24(sys_get_be24(data->rh_param), &tx_buf[2]); + sys_put_be24(sys_get_be24(data->t_param), &tx_buf[5]); + + return i2c_write(cfg->bus, tx_buf, sizeof(tx_buf), + cfg->i2c_addr); +} + +static int sgp40_attr_set(const struct device *dev, + enum sensor_channel chan, + enum sensor_attribute attr, + const struct sensor_value *val) +{ + struct sgp40_data *data = dev->data; + + /* + * Temperature and RH conversion to ticks as explained in datasheet + * in section "I2C commands" + */ + + switch ((enum sensor_attribute_sgp40)attr) { + case SENSOR_ATTR_SGP40_TEMPERATURE: + { + int16_t t_ticks; + int32_t tmp; + + tmp = CLAMP(val->val1, SGP40_COMP_MIN_T, SGP40_COMP_MAX_T); + t_ticks = ((tmp + 45U) * 0xFFFF / 175U) + 0.5; + sys_put_be16(t_ticks, data->t_param); + data->t_param[2] = sgp40_compute_crc(t_ticks); + } + break; + case SENSOR_ATTR_SGP40_HUMIDITY: + { + int16_t rh_ticks; + int32_t tmp; + + tmp = CLAMP(val->val1, SGP40_COMP_MIN_RH, SGP40_COMP_MAX_RH); + rh_ticks = (tmp * 0xFFFF / 100U) + 0.5; + sys_put_be16(rh_ticks, data->rh_param); + data->rh_param[2] = sgp40_compute_crc(rh_ticks); + } + break; + default: + return -ENOTSUP; + } + return 0; +} + +static int sgp40_selftest(const struct device *dev) +{ + const struct sgp40_config *cfg = dev->config; + uint8_t rx_buf[3]; + uint16_t raw_sample; + int rc; + + rc = sgp40_write_command(dev, SGP40_CMD_MEASURE_TEST); + if (rc < 0) { + LOG_ERR("Failed to start selftest!"); + return rc; + } + + k_sleep(K_MSEC(SGP40_TEST_WAIT_MS)); + + rc = i2c_read(cfg->bus, rx_buf, sizeof(rx_buf), cfg->i2c_addr); + if (rc < 0) { + LOG_ERR("Failed to read data sample."); + return rc; + } + + raw_sample = sys_get_be16(rx_buf); + if (sgp40_compute_crc(raw_sample) != rx_buf[2]) { + LOG_ERR("Received invalid CRC from selftest."); + return -EIO; + } + + if (raw_sample != SGP40_TEST_OK) { + LOG_ERR("Selftest failed."); + return -EIO; + } + + return 0; +} + +static int sgp40_sample_fetch(const struct device *dev, + enum sensor_channel chan) +{ + struct sgp40_data *data = dev->data; + const struct sgp40_config *cfg = dev->config; + uint8_t rx_buf[3]; + uint16_t raw_sample; + int rc; + + if (chan != SENSOR_CHAN_GAS_RES && chan != SENSOR_CHAN_ALL) { + return -ENOTSUP; + } + + rc = sgp40_start_measurement(dev); + if (rc < 0) { + LOG_ERR("Failed to start measurement."); + return rc; + } + + k_sleep(K_MSEC(SGP40_MEASURE_WAIT_MS)); + + rc = i2c_read(cfg->bus, rx_buf, sizeof(rx_buf), cfg->i2c_addr); + if (rc < 0) { + LOG_ERR("Failed to read data sample."); + return rc; + } + + raw_sample = sys_get_be16(rx_buf); + if (sgp40_compute_crc(raw_sample) != rx_buf[2]) { + LOG_ERR("Invalid CRC8 for data sample."); + return -EIO; + } + + data->raw_sample = raw_sample; + + return 0; +} + +static int sgp40_channel_get(const struct device *dev, + enum sensor_channel chan, + struct sensor_value *val) +{ + const struct sgp40_data *data = dev->data; + + if (chan != SENSOR_CHAN_GAS_RES) { + return -ENOTSUP; + } + + val->val1 = data->raw_sample; + val->val2 = 0; + + return 0; +} + + +#ifdef CONFIG_PM_DEVICE +static int sgp40_set_power_state(const struct device *dev, + enum pm_device_state power_state) +{ + struct sgp40_data *data = dev->data; + uint16_t cmd; + int rc; + + if (data->pm_state == power_state) { + LOG_DBG("Device already in requested PM_STATE."); + return 0; + } + + if (power_state == PM_DEVICE_STATE_ACTIVE) { + /* activate the hotplate by sending a measure command */ + cmd = SGP40_CMD_MEASURE_RAW; + } else if (power_state == PM_DEVICE_STATE_SUSPEND) { + cmd = SGP40_CMD_HEATER_OFF; + } else { + LOG_DBG("Power state not implemented."); + return -ENOTSUP; + } + + rc = sgp40_write_command(dev, cmd); + if (rc < 0) { + LOG_ERR("Failed to set power state."); + return rc; + } + + data->pm_state = power_state; + + return 0; +} + +static uint32_t sgp40_get_power_state(const struct device *dev, + enum pm_device_state *state) +{ + struct sgp40_data *data = dev->data; + + *state = data->pm_state; + + return 0; +} + +static int sgp40_pm_ctrl(const struct device *dev, + uint32_t ctrl_command, + enum pm_device_state *state) +{ + int rc = 0; + + if (ctrl_command == PM_DEVICE_STATE_SET) { + rc = sgp40_set_power_state(dev, *state); + } else if (ctrl_command == PM_DEVICE_STATE_GET) { + rc = sgp40_get_power_state(dev, state); + } + + return rc; +} +#endif /* CONFIG_PM_DEVICE */ + +static int sgp40_init(const struct device *dev) +{ + const struct sgp40_config *cfg = dev->config; + struct sensor_value comp_data; + + if (!device_is_ready(cfg->bus)) { + LOG_ERR("Device not ready."); + return -ENODEV; + } + + if (cfg->selftest) { + int rc = sgp40_selftest(dev); + + if (rc < 0) { + LOG_ERR("Selftest failed!"); + return rc; + } + LOG_DBG("Selftest succeded!"); + } + + comp_data.val1 = SGP40_COMP_DEFAULT_T; + sensor_attr_set(dev, + SENSOR_CHAN_GAS_RES, + (enum sensor_attribute) SENSOR_ATTR_SGP40_TEMPERATURE, + &comp_data); + comp_data.val1 = SGP40_COMP_DEFAULT_RH; + sensor_attr_set(dev, + SENSOR_CHAN_GAS_RES, + (enum sensor_attribute) SENSOR_ATTR_SGP40_HUMIDITY, + &comp_data); + + return 0; +} + +static const struct sensor_driver_api sgp40_api = { + .sample_fetch = sgp40_sample_fetch, + .channel_get = sgp40_channel_get, + .attr_set = sgp40_attr_set, +}; + +#define SGP40_INIT(n) \ + static struct sgp40_data sgp40_data_##n; \ + \ + static const struct sgp40_config sgp40_config_##n = { \ + .bus = DEVICE_DT_GET(DT_INST_BUS(n)), \ + .i2c_addr = DT_INST_REG_ADDR(n), \ + .selftest = DT_INST_PROP(n, enable_selftest), \ + }; \ + \ + DEVICE_DT_INST_DEFINE(n, \ + sgp40_init, \ + sgp40_pm_ctrl,\ + &sgp40_data_##n, \ + &sgp40_config_##n, \ + POST_KERNEL, \ + CONFIG_SENSOR_INIT_PRIORITY, \ + &sgp40_api); + +DT_INST_FOREACH_STATUS_OKAY(SGP40_INIT) diff --git a/drivers/sensor/sgp40/sgp40.h b/drivers/sensor/sgp40/sgp40.h new file mode 100644 index 00000000000..856af8c33fe --- /dev/null +++ b/drivers/sensor/sgp40/sgp40.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021 Leonard Pollak + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_SENSOR_SGP40_SGP40_H_ +#define ZEPHYR_DRIVERS_SENSOR_SGP40_SGP40_H_ + +#include + +#define SGP40_CMD_MEASURE_RAW 0x260F +#define SGP40_CMD_MEASURE_TEST 0x280E +#define SGP40_CMD_HEATER_OFF 0x3615 + +#define SGP40_TEST_OK 0xD400 +#define SGP40_TEST_FAIL 0x4B00 + +#define SGP40_RESET_WAIT_MS 10 +#define SGP40_MEASURE_WAIT_MS 30 +#define SGP40_TEST_WAIT_MS 250 + +/* + * CRC parameters were taken from the + * "Checksum Calculation" section of the datasheet. + */ +#define SGP40_CRC_POLY 0x31 +#define SGP40_CRC_INIT 0xFF + +/* + * Value range of compensation data parameters + */ +#define SGP40_COMP_MIN_RH 0 +#define SGP40_COMP_MAX_RH 100 +#define SGP40_COMP_MIN_T -45 +#define SGP40_COMP_MAX_T 130 +#define SGP40_COMP_DEFAULT_T 25 +#define SGP40_COMP_DEFAULT_RH 50 + +struct sgp40_config { + const struct device *bus; + uint8_t i2c_addr; + bool selftest; +}; + +struct sgp40_data { + uint16_t raw_sample; + int8_t rh_param[3]; + int8_t t_param[3]; + +#ifdef CONFIG_PM_DEVICE + enum pm_device_state pm_state; +#endif +}; + +#endif /* ZEPHYR_DRIVERS_SENSOR_SGP40_SGP40_H_ */ diff --git a/dts/bindings/sensor/sensirion,sgp40.yaml b/dts/bindings/sensor/sensirion,sgp40.yaml new file mode 100644 index 00000000000..7c0a2685573 --- /dev/null +++ b/dts/bindings/sensor/sensirion,sgp40.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2021, Leonard Pollak +# +# SPDX-License-Identifier: Apache-2.0 + +description: Sensirion SGP40 Multipixel Gas Sensor + +compatible: "sensirion,sgp40" + +include: i2c-device.yaml + +properties: + enable-selftest: + type: boolean + description: | + Enabling this will run a selftest when the driver initializes. + The selftest takes ~250ms. diff --git a/include/drivers/sensor/sgp40.h b/include/drivers/sensor/sgp40.h new file mode 100644 index 00000000000..0045988da4d --- /dev/null +++ b/include/drivers/sensor/sgp40.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021, Leonard Pollak + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Extended public API for Sensirion's SGP40 gas sensor + * + * This exposes two attributes for the SGP40 which can be used for + * setting the on-chip Temperature and Humidity compensation parameters. + */ + +#ifndef ZEPHYR_INCLUDE_DRIVERS_SENSOR_SGP40_H_ +#define ZEPHYR_INCLUDE_DRIVERS_SENSOR_SGP40_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +enum sensor_attribute_sgp40 { + /* Temperature in degC; only the integer part is used */ + SENSOR_ATTR_SGP40_TEMPERATURE = SENSOR_ATTR_PRIV_START, + /* Relative Humidity in %; only the integer part is used */ + SENSOR_ATTR_SGP40_HUMIDITY +}; + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_DRIVERS_SENSOR_SGP40_H_ */ diff --git a/tests/drivers/build_all/sensor/i2c.dtsi b/tests/drivers/build_all/sensor/i2c.dtsi index c62102a08fb..33d54256a06 100644 --- a/tests/drivers/build_all/sensor/i2c.dtsi +++ b/tests/drivers/build_all/sensor/i2c.dtsi @@ -230,6 +230,13 @@ test_i2c_sx9500: sx9500@22 { int-gpios = <&test_gpio 0 0>; }; +test_i2c_sgp40: sgp40@59 { + compatible = "sensirion,sgp40"; + label = "SGP40"; + reg = <0x59>; + enable-selftest; +}; + test_i2c_sht3xd: sht3xd@23 { compatible = "sensirion,sht3xd"; label = "SHT3XD"; diff --git a/tests/drivers/build_all/sensor/prj.conf b/tests/drivers/build_all/sensor/prj.conf index f47ee4fa3bc..64895bc762c 100644 --- a/tests/drivers/build_all/sensor/prj.conf +++ b/tests/drivers/build_all/sensor/prj.conf @@ -73,6 +73,7 @@ CONFIG_MS5607=y CONFIG_MS5837=y CONFIG_OPT3001=y CONFIG_SBS_GAUGE=y +CONFIG_SGP40=y CONFIG_SHT3XD=y CONFIG_SHTCX=y CONFIG_SI7006=y