From: Gustavo Silva <gustavograzs@gmail.com>
To: jic23@kernel.org
Cc: robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
lars@metafoo.de, christophe.jaillet@wanadoo.fr,
gerald.loacker@wolfvision.net, devicetree@vger.kernel.org,
linux-iio@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH 3/6] iio: chemical: add driver for ENS160 sensor
Date: Sun, 12 May 2024 18:04:39 -0300 [thread overview]
Message-ID: <20240512210444.30824-4-gustavograzs@gmail.com> (raw)
In-Reply-To: <20240512210444.30824-1-gustavograzs@gmail.com>
ScioSense ENS160 is a digital metal oxide multi-gas sensor, designed
for indoor air quality monitoring. The driver supports readings of
CO2 and VOC, and can be accessed via both SPI and I2C.
Signed-off-by: Gustavo Silva <gustavograzs@gmail.com>
---
drivers/iio/chemical/Kconfig | 22 +++
drivers/iio/chemical/Makefile | 3 +
drivers/iio/chemical/ens160.h | 9 ++
drivers/iio/chemical/ens160_core.c | 227 +++++++++++++++++++++++++++++
drivers/iio/chemical/ens160_i2c.c | 68 +++++++++
drivers/iio/chemical/ens160_spi.c | 69 +++++++++
6 files changed, 398 insertions(+)
create mode 100644 drivers/iio/chemical/ens160.h
create mode 100644 drivers/iio/chemical/ens160_core.c
create mode 100644 drivers/iio/chemical/ens160_i2c.c
create mode 100644 drivers/iio/chemical/ens160_spi.c
diff --git a/drivers/iio/chemical/Kconfig b/drivers/iio/chemical/Kconfig
index 02649ab81..e407afab8 100644
--- a/drivers/iio/chemical/Kconfig
+++ b/drivers/iio/chemical/Kconfig
@@ -76,6 +76,28 @@ config CCS811
Say Y here to build I2C interface support for the AMS
CCS811 VOC (Volatile Organic Compounds) sensor
+config ENS160
+ tristate "ScioSense ENS160 sensor driver"
+ depends on (I2C || SPI)
+ select REGMAP
+ select ENS160_I2C if I2C
+ select ENS160_SPI if SPI
+ help
+ Say yes here to build support for ScioSense ENS160 multi-gas sensor.
+
+ This driver can also be built as a module. If so, the module for I2C
+ would be called ens160_i2c and ens160_spi for SPI support.
+
+config ENS160_I2C
+ tristate
+ depends on I2C && ENS160
+ select REGMAP_I2C
+
+config ENS160_SPI
+ tristate
+ depends on SPI && ENS160
+ select REGMAP_SPI
+
config IAQCORE
tristate "AMS iAQ-Core VOC sensors"
depends on I2C
diff --git a/drivers/iio/chemical/Makefile b/drivers/iio/chemical/Makefile
index 2f3dee8bb..4866db06b 100644
--- a/drivers/iio/chemical/Makefile
+++ b/drivers/iio/chemical/Makefile
@@ -11,6 +11,9 @@ obj-$(CONFIG_BME680) += bme680_core.o
obj-$(CONFIG_BME680_I2C) += bme680_i2c.o
obj-$(CONFIG_BME680_SPI) += bme680_spi.o
obj-$(CONFIG_CCS811) += ccs811.o
+obj-$(CONFIG_ENS160) += ens160_core.o
+obj-$(CONFIG_ENS160_I2C) += ens160_i2c.o
+obj-$(CONFIG_ENS160_SPI) += ens160_spi.o
obj-$(CONFIG_IAQCORE) += ams-iaq-core.o
obj-$(CONFIG_PMS7003) += pms7003.o
obj-$(CONFIG_SCD30_CORE) += scd30_core.o
diff --git a/drivers/iio/chemical/ens160.h b/drivers/iio/chemical/ens160.h
new file mode 100644
index 000000000..3fd079bc4
--- /dev/null
+++ b/drivers/iio/chemical/ens160.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef ENS160_H_
+#define ENS160_H_
+
+int ens160_core_probe(struct device *dev, struct regmap *regmap,
+ const char *name);
+void ens160_core_remove(struct device *dev);
+
+#endif
diff --git a/drivers/iio/chemical/ens160_core.c b/drivers/iio/chemical/ens160_core.c
new file mode 100644
index 000000000..25593420d
--- /dev/null
+++ b/drivers/iio/chemical/ens160_core.c
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ScioSense ENS160 multi-gas sensor driver
+ *
+ * Copyright (c) 2024 Gustavo Silva <gustavograzs@gmail.com>
+ *
+ * Data sheet:
+ * https://www.sciosense.com/wp-content/uploads/2023/12/ENS160-Datasheet.pdf
+ *
+ */
+
+#include <linux/bitfield.h>
+#include <linux/iio/iio.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+
+#include "ens160.h"
+
+#define ENS160_PART_ID 0x160
+
+#define ENS160_BOOTING_TIME_MS 10U
+
+#define ENS160_REG_PART_ID 0x00
+
+#define ENS160_REG_OPMODE 0x10
+
+#define ENS160_REG_MODE_DEEP_SLEEP 0x00
+#define ENS160_REG_MODE_IDLE 0x01
+#define ENS160_REG_MODE_STANDARD 0x02
+#define ENS160_REG_MODE_RESET 0xF0
+
+#define ENS160_REG_COMMAND 0x12
+#define ENS160_REG_COMMAND_GET_APPVER 0x0E
+#define ENS160_REG_COMMAND_CLRGPR 0xCC
+
+#define ENS160_REG_TEMP_IN 0x13
+#define ENS160_REG_RH_IN 0x15
+#define ENS160_REG_DEVICE_STATUS 0x20
+#define ENS160_REG_DATA_TVOC 0x22
+#define ENS160_REG_DATA_ECO2 0x24
+#define ENS160_REG_DATA_T 0x30
+#define ENS160_REG_DATA_RH 0x32
+#define ENS160_REG_GPR_READ4 0x4C
+
+#define ENS160_STATUS_VALIDITY_FLAG GENMASK(3, 2)
+
+#define ENS160_STATUS_NORMAL 0x00
+
+struct ens160_data {
+ struct regmap *regmap;
+};
+
+static const struct iio_chan_spec ens160_channels[] = {
+ {
+ .type = IIO_CONCENTRATION,
+ .channel2 = IIO_MOD_VOC,
+ .modified = 1,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE),
+ .address = ENS160_REG_DATA_TVOC,
+ },
+ {
+ .type = IIO_CONCENTRATION,
+ .channel2 = IIO_MOD_CO2,
+ .modified = 1,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE),
+ .address = ENS160_REG_DATA_ECO2,
+ },
+};
+
+static int ens160_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int *val, int *val2, long mask)
+{
+ struct ens160_data *data = iio_priv(indio_dev);
+ __le16 buf;
+ int ret;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_RAW:
+ ret = regmap_bulk_read(data->regmap, chan->address,
+ &buf, sizeof(buf));
+ if (ret)
+ return ret;
+ *val = le16_to_cpu(buf);
+ return IIO_VAL_INT;
+
+ case IIO_CHAN_INFO_SCALE:
+ switch (chan->channel2) {
+ case IIO_MOD_CO2:
+ /* The sensor reads CO2 data as ppm */
+ *val = 0;
+ *val2 = 100;
+ return IIO_VAL_INT_PLUS_MICRO;
+ case IIO_MOD_VOC:
+ /* The sensor reads VOC data as ppb */
+ *val = 0;
+ *val2 = 100;
+ return IIO_VAL_INT_PLUS_NANO;
+ }
+ }
+
+ return -EINVAL;
+}
+
+static int ens160_set_mode(struct ens160_data *data, u8 mode)
+{
+ int ret;
+
+ ret = regmap_write(data->regmap, ENS160_REG_OPMODE, mode);
+ if (ret)
+ return ret;
+
+ msleep(ENS160_BOOTING_TIME_MS);
+
+ return 0;
+}
+
+static int ens160_chip_init(struct ens160_data *data)
+{
+ struct device *dev = regmap_get_device(data->regmap);
+ u8 fw_version[3];
+ __le16 part_id;
+ unsigned int status;
+ int ret;
+
+ ret = ens160_set_mode(data, ENS160_REG_MODE_RESET);
+ if (ret)
+ return ret;
+
+ ret = regmap_bulk_read(data->regmap, ENS160_REG_PART_ID, &part_id,
+ sizeof(part_id));
+ if (ret)
+ return ret;
+
+ if (le16_to_cpu(part_id) != ENS160_PART_ID)
+ return -ENODEV;
+
+ ret = ens160_set_mode(data, ENS160_REG_MODE_IDLE);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(data->regmap, ENS160_REG_COMMAND,
+ ENS160_REG_COMMAND_CLRGPR);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(data->regmap, ENS160_REG_COMMAND,
+ ENS160_REG_COMMAND_GET_APPVER);
+ if (ret)
+ return ret;
+
+ msleep(ENS160_BOOTING_TIME_MS);
+
+ ret = regmap_bulk_read(data->regmap, ENS160_REG_GPR_READ4,
+ fw_version, sizeof(fw_version));
+ if (ret)
+ return ret;
+
+ msleep(ENS160_BOOTING_TIME_MS);
+
+ dev_info(dev, "firmware version: %u.%u.%u\n", fw_version[2],
+ fw_version[1], fw_version[0]);
+
+ ret = ens160_set_mode(data, ENS160_REG_MODE_STANDARD);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(data->regmap, ENS160_REG_DEVICE_STATUS, &status);
+ if (ret)
+ return ret;
+
+ if (FIELD_GET(ENS160_STATUS_VALIDITY_FLAG, status)
+ != ENS160_STATUS_NORMAL)
+ return -EINVAL;
+
+ return 0;
+}
+
+static const struct iio_info ens160_info = {
+ .read_raw = ens160_read_raw,
+};
+
+int ens160_core_probe(struct device *dev, struct regmap *regmap,
+ const char *name)
+{
+ struct ens160_data *data;
+ struct iio_dev *indio_dev;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(dev, sizeof(*data));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ data = iio_priv(indio_dev);
+ dev_set_drvdata(dev, indio_dev);
+ data->regmap = regmap;
+
+ indio_dev->name = name;
+ indio_dev->info = &ens160_info;
+ indio_dev->modes = INDIO_DIRECT_MODE;
+ indio_dev->channels = ens160_channels;
+ indio_dev->num_channels = ARRAY_SIZE(ens160_channels);
+
+ ret = ens160_chip_init(data);
+ if (ret) {
+ dev_err_probe(dev, ret, "chip initialization failed\n");
+ return ret;
+ }
+
+ return devm_iio_device_register(dev, indio_dev);
+}
+EXPORT_SYMBOL_NS(ens160_core_probe, IIO_ENS160);
+
+void ens160_core_remove(struct device *dev)
+{
+ struct iio_dev *indio_dev = dev_get_drvdata(dev);
+ struct ens160_data *data = iio_priv(indio_dev);
+
+ ens160_set_mode(data, ENS160_REG_MODE_IDLE);
+}
+EXPORT_SYMBOL_NS(ens160_core_remove, IIO_ENS160);
+
+MODULE_AUTHOR("Gustavo Silva <gustavograzs@gmail.com>");
+MODULE_DESCRIPTION("ScioSense ENS160 driver");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/iio/chemical/ens160_i2c.c b/drivers/iio/chemical/ens160_i2c.c
new file mode 100644
index 000000000..ee2b44184
--- /dev/null
+++ b/drivers/iio/chemical/ens160_i2c.c
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ScioSense ENS160 multi-gas sensor I2C driver
+ *
+ * Copyright (c) 2024 Gustavo Silva <gustavograzs@gmail.com>
+ *
+ * 7-Bit I2C slave address is:
+ * - 0x52 if ADDR pin LOW
+ * - 0x53 if ADDR pin HIGH
+ */
+
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+
+#include "ens160.h"
+
+static const struct regmap_config ens160_regmap_i2c_conf = {
+ .reg_bits = 8,
+ .val_bits = 8,
+};
+
+static int ens160_i2c_probe(struct i2c_client *client)
+{
+ struct regmap *regmap;
+
+ regmap = devm_regmap_init_i2c(client, &ens160_regmap_i2c_conf);
+ if (IS_ERR(regmap)) {
+ dev_err(&client->dev, "Failed to register i2c regmap %ld\n",
+ PTR_ERR(regmap));
+ return PTR_ERR(regmap);
+ }
+
+ return ens160_core_probe(&client->dev, regmap, client->name);
+}
+
+static void ens160_i2c_remove(struct i2c_client *client)
+{
+ ens160_core_remove(&client->dev);
+}
+
+static const struct i2c_device_id ens160_i2c_id[] = {
+ { "ens160", 0 },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, ens160_i2c_id);
+
+static const struct of_device_id ens160_of_i2c_match[] = {
+ { .compatible = "sciosense,ens160" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ens160_of_i2c_match);
+
+static struct i2c_driver ens160_i2c_driver = {
+ .driver = {
+ .name = "ens160_i2c",
+ .of_match_table = ens160_of_i2c_match,
+ },
+ .probe = ens160_i2c_probe,
+ .remove = ens160_i2c_remove,
+ .id_table = ens160_i2c_id,
+};
+module_i2c_driver(ens160_i2c_driver);
+
+MODULE_AUTHOR("Gustavo Silva <gustavograzs@gmail.com>");
+MODULE_DESCRIPTION("ScioSense ENS160 I2C driver");
+MODULE_LICENSE("GPL v2");
+MODULE_IMPORT_NS(IIO_ENS160);
diff --git a/drivers/iio/chemical/ens160_spi.c b/drivers/iio/chemical/ens160_spi.c
new file mode 100644
index 000000000..effc4acee
--- /dev/null
+++ b/drivers/iio/chemical/ens160_spi.c
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ScioSense ENS160 multi-gas sensor SPI driver
+ *
+ * Copyright (c) 2024 Gustavo Silva <gustavograzs@gmail.com>
+ */
+
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/spi/spi.h>
+
+#include "ens160.h"
+
+#define ENS160_SPI_READ BIT(0)
+
+static const struct regmap_config ens160_regmap_spi_conf = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .reg_shift = -1,
+ .read_flag_mask = ENS160_SPI_READ,
+};
+
+static int ens160_spi_probe(struct spi_device *spi)
+{
+ struct regmap *regmap;
+ const struct spi_device_id *id = spi_get_device_id(spi);
+
+ regmap = devm_regmap_init_spi(spi, &ens160_regmap_spi_conf);
+ if (IS_ERR(regmap)) {
+ dev_err(&spi->dev, "Failed to register spi regmap: %pe\n",
+ regmap);
+ return PTR_ERR(regmap);
+ }
+
+ return ens160_core_probe(&spi->dev, regmap, id->name);
+}
+
+static void ens160_spi_remove(struct spi_device *spi)
+{
+ ens160_core_remove(&spi->dev);
+}
+
+static const struct of_device_id ens160_spi_of_match[] = {
+ { .compatible = "sciosense,ens160" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ens160_spi_of_match);
+
+static const struct spi_device_id ens160_spi_id[] = {
+ { "ens160", 0 },
+ { }
+};
+MODULE_DEVICE_TABLE(spi, ens160_spi_id);
+
+static struct spi_driver ens160_spi_driver = {
+ .driver = {
+ .name = "ens160_spi",
+ .of_match_table = ens160_spi_of_match,
+ },
+ .probe = ens160_spi_probe,
+ .remove = ens160_spi_remove,
+ .id_table = ens160_spi_id,
+};
+module_spi_driver(ens160_spi_driver);
+
+MODULE_AUTHOR("Gustavo Silva <gustavograzs@gmail.com>");
+MODULE_DESCRIPTION("ScioSense ENS160 SPI driver");
+MODULE_LICENSE("GPL v2");
+MODULE_IMPORT_NS(IIO_ENS160);
--
2.45.0
next prev parent reply other threads:[~2024-05-12 21:05 UTC|newest]
Thread overview: 22+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-05-12 21:04 [PATCH 0/6] Add driver for ENS160 sensor Gustavo Silva
2024-05-12 21:04 ` [PATCH 1/6] dt-bindings: vendor-prefixes: add ScioSense Gustavo Silva
2024-05-13 16:02 ` Conor Dooley
2024-05-12 21:04 ` [PATCH 2/6] dt-bindings: Add ENS160 as trivial device Gustavo Silva
2024-05-13 16:09 ` Conor Dooley
2024-05-19 12:33 ` Jonathan Cameron
2024-05-12 21:04 ` Gustavo Silva [this message]
2024-05-13 19:12 ` [PATCH 3/6] iio: chemical: add driver for ENS160 sensor Christophe JAILLET
2024-05-19 13:24 ` Jonathan Cameron
2024-05-22 23:41 ` Gustavo Silva
2024-05-19 14:01 ` Jonathan Cameron
2024-05-26 0:29 ` Gustavo Silva
2024-05-26 12:20 ` Jonathan Cameron
2024-05-12 21:04 ` [PATCH 4/6] iio: chemical: ens160: add triggered buffer support Gustavo Silva
2024-05-13 19:13 ` Christophe JAILLET
2024-05-19 14:03 ` Jonathan Cameron
2024-05-20 6:50 ` Christophe JAILLET
2024-05-13 23:50 ` kernel test robot
2024-05-19 14:18 ` Jonathan Cameron
2024-05-12 21:04 ` [PATCH 5/6] iio: chemical: ens160: add power management support Gustavo Silva
2024-05-19 14:18 ` Jonathan Cameron
2024-05-12 21:04 ` [PATCH 6/6] MAINTAINERS: Add ScioSense ENS160 Gustavo Silva
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20240512210444.30824-4-gustavograzs@gmail.com \
--to=gustavograzs@gmail.com \
--cc=christophe.jaillet@wanadoo.fr \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=gerald.loacker@wolfvision.net \
--cc=jic23@kernel.org \
--cc=krzk+dt@kernel.org \
--cc=lars@metafoo.de \
--cc=linux-iio@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=robh@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).