All of lore.kernel.org
 help / color / mirror / Atom feed
From: Akshat Chandra <notmissinge@gmail.com>
To: linux-iio@vger.kernel.org
Cc: "Jonathan Cameron" <jic23@kernel.org>,
	"David Lechner" <dlechner@baylibre.com>,
	"Nuno Sá" <nuno.sa@analog.com>,
	"Andy Shevchenko" <andy@kernel.org>,
	linux-kernel@vger.kernel.org,
	"Akshat Chandra" <notmissinge@gmail.com>
Subject: [PATCH] iio: chemical: add SGP41 driver
Date: Mon, 31 Aug 2026 20:42:35 +0530	[thread overview]
Message-ID: <20260831151235.64076-1-notmissinge@gmail.com> (raw)

Signed-off-by: Akshat Chandra <notmissinge@gmail.com>
---
 .../devicetree/bindings/trivial-devices.yaml  |   2 +
 drivers/iio/chemical/Kconfig                  |  11 +
 drivers/iio/chemical/Makefile                 |   1 +
 drivers/iio/chemical/sgp41.c                  | 384 ++++++++++++++++++
 4 files changed, 398 insertions(+)
 create mode 100644 drivers/iio/chemical/sgp41.c

diff --git a/Documentation/devicetree/bindings/trivial-devices.yaml b/Documentation/devicetree/bindings/trivial-devices.yaml
index 2de8eb09c..c3b51d8ee 100644
--- a/Documentation/devicetree/bindings/trivial-devices.yaml
+++ b/Documentation/devicetree/bindings/trivial-devices.yaml
@@ -420,6 +420,8 @@ properties:
           - sensirion,sgp30
             # Sensirion gas sensor with I2C interface
           - sensirion,sgp40
+            # Sensirion gas sensor with I2C interface
+          - sensirion,sgp41
             # Sensirion low power multi-pixel gas sensor with I2C interface
           - sensirion,sgpc3
             # Sensirion temperature & humidity sensor with I2C interface
diff --git a/drivers/iio/chemical/Kconfig b/drivers/iio/chemical/Kconfig
index b22afa1f6..cbb6a51b1 100644
--- a/drivers/iio/chemical/Kconfig
+++ b/drivers/iio/chemical/Kconfig
@@ -210,6 +210,17 @@ config SENSIRION_SGP40
 	  To compile this driver as module, choose M here: the
 	  module will be called sgp40.
 
+config SENSIRION_SGP41
+	tristate "Sensirion SGP41 gas sensor"
+	depends on I2C
+	select CRC8
+	help
+	  Say Y here to build I2C interface to support Sensirion SGP41 gas
+	  sensor
+
+	  To compile this driver as module, choose M here: the
+	  module will be called sgp41.
+
 config SPS30
 	tristate
 	select IIO_BUFFER
diff --git a/drivers/iio/chemical/Makefile b/drivers/iio/chemical/Makefile
index 2287a00a6..90041aaa4 100644
--- a/drivers/iio/chemical/Makefile
+++ b/drivers/iio/chemical/Makefile
@@ -25,6 +25,7 @@ obj-$(CONFIG_SEN0322)	+= sen0322.o
 obj-$(CONFIG_SENSEAIR_SUNRISE_CO2) += sunrise_co2.o
 obj-$(CONFIG_SENSIRION_SGP30)	+= sgp30.o
 obj-$(CONFIG_SENSIRION_SGP40)	+= sgp40.o
+obj-$(CONFIG_SENSIRION_SGP41)	+= sgp41.o
 obj-$(CONFIG_SPS30) += sps30.o
 obj-$(CONFIG_SPS30_I2C) += sps30_i2c.o
 obj-$(CONFIG_SPS30_SERIAL) += sps30_serial.o
diff --git a/drivers/iio/chemical/sgp41.c b/drivers/iio/chemical/sgp41.c
new file mode 100644
index 000000000..717113e95
--- /dev/null
+++ b/drivers/iio/chemical/sgp41.c
@@ -0,0 +1,384 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <linux/crc8.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/iio/iio.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/types.h>
+
+#define SGP41_CALC_POWER		14
+
+#define SGP41_CRC8_POLYNOMIAL		0x31
+#define SGP41_CRC8_INIT		0xff
+
+#define SGP41_COND_ITERATIONS		10
+
+DECLARE_CRC8_TABLE(sgp41_crc8_table);
+
+struct sgp41_data {
+	struct device *dev;
+	struct i2c_client *client;
+
+	int rht;
+	int temp;
+
+	int vos_res_calibbias;
+	int nox_res_calibbias;
+
+	struct mutex lock;/* Protects sensor state and I2C transaction.*/
+	bool conditioned;
+};
+
+struct sgp41_tg_measure {
+	u8 command[2];
+	__be16 rht_ticks;
+	u8 rht_crc;
+	__be16 temp_ticks;
+	u8 temp_crc;
+} __packed;
+
+struct sgp41_tg_result {
+	__be16 voc_res_ticks;
+	u8 voc_res_crc;
+	__be16 nox_res_ticks;
+	u8 nox_res_crc;
+} __packed;
+
+static const struct iio_chan_spec sgp41_channels[] = {
+	{
+		.type = IIO_RESISTANCE,
+		.channel = 0,
+		.modified = 1,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
+	},
+	{
+		.type = IIO_RESISTANCE,
+		.channel = 1,
+		.indexed = 1,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
+	},
+	{
+		.type = IIO_TEMP,
+		.output = 1,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
+	},
+	{
+		.type = IIO_HUMIDITYRELATIVE,
+		.output = 1,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
+	},
+};
+
+static void temp_rht_payload(struct sgp41_data *data,
+			     struct sgp41_tg_measure *tg)
+{
+	s64 ticks;
+	u16 ticks16;
+
+	ticks = div_s64((s64)data->rht * 65535, 100000);
+	ticks16 = (u16)clamp_t(s64, ticks, 0, 65535);
+	tg->rht_ticks = cpu_to_be16(ticks16);
+	tg->rht_crc = crc8(sgp41_crc8_table, (u8 *)&tg->rht_ticks, 2,
+			   SGP41_CRC8_INIT);
+
+	ticks = div_s64((s64)(data->temp + 45000) * 65535, 175000);
+	ticks16 = (u16)clamp_t(s64, ticks, 0, 65535);
+	tg->temp_ticks = cpu_to_be16(ticks16);
+	tg->temp_crc = crc8(sgp41_crc8_table, (u8 *)&tg->temp_ticks, 2,
+			    SGP41_CRC8_INIT);
+}
+
+static int sgp41_run_conditioning(struct sgp41_data *data)
+{
+	struct sgp41_tg_measure tg = {
+		.command = { 0x26, 0x12 },
+	};
+	int i;
+	int ret;
+
+	for (i = 0; i < SGP41_COND_ITERATIONS; i++) {
+		temp_rht_payload(data, &tg);
+
+		ret = i2c_master_send(data->client, (const char *)&tg,
+				      sizeof(tg));
+		if (ret != sizeof(tg)) {
+			dev_warn(data->dev,
+				 "i2c_master_send ret: %d sizeof: %zu\n",
+				 ret, sizeof(tg));
+			return ret < 0 ? ret : -EIO;
+		}
+
+		msleep(50);
+
+		if (i != SGP41_COND_ITERATIONS - 1)
+			msleep(950);
+	}
+
+	data->conditioned = true;
+
+	return 0;
+}
+
+static int sgp41_measure_raw(struct sgp41_data *data,
+					u16 *voc_raw,
+					u16 *nox_raw)
+{
+	struct sgp41_tg_measure tg = {
+		.command = { 0x26, 0x19 },
+	};
+	struct sgp41_tg_result tgres;
+	u8 crc;
+	int ret;
+
+	mutex_lock(&data->lock);
+
+	if (!data->conditioned) {
+		ret = sgp41_run_conditioning(data);
+		if (ret)
+			goto unlock;
+	}
+
+	temp_rht_payload(data, &tg);
+
+	ret = i2c_master_send(data->client, (const char *)&tg, sizeof(tg));
+	if (ret != sizeof(tg)) {
+		dev_warn(data->dev,
+			 "i2c_master_send ret: %d sizeof: %zu\n",
+			 ret, sizeof(tg));
+		ret = ret < 0 ? ret : -EIO;
+		goto unlock;
+	}
+
+	msleep(50);
+
+	ret = i2c_master_recv(data->client, (u8 *)&tgres, sizeof(tgres));
+	if (ret < 0)
+		goto unlock;
+
+	if (ret != sizeof(tgres)) {
+		dev_warn(data->dev,
+			 "i2c_master_recv ret: %d sizeof: %zu\n",
+			 ret, sizeof(tgres));
+		ret = -EIO;
+		goto unlock;
+	}
+
+	crc = crc8(sgp41_crc8_table, (u8 *)&tgres.voc_res_ticks, 2,
+		   SGP41_CRC8_INIT);
+	if (crc != tgres.voc_res_crc) {
+		dev_err(data->dev, "CRC error while measure-voc_raw\n");
+		ret = -EIO;
+		goto unlock;
+	}
+
+	crc = crc8(sgp41_crc8_table, (u8 *)&tgres.nox_res_ticks, 2,
+		   SGP41_CRC8_INIT);
+	if (crc != tgres.nox_res_crc) {
+		dev_err(data->dev, "CRC error while measure-nox_raw\n");
+		ret = -EIO;
+		goto unlock;
+	}
+
+	*voc_raw = be16_to_cpu(tgres.voc_res_ticks);
+	*nox_raw = be16_to_cpu(tgres.nox_res_ticks);
+	ret = 0;
+
+unlock:
+	mutex_unlock(&data->lock);
+
+	return ret;
+}
+
+static int sgp41_read_raw(struct iio_dev *indio_dev,
+			  struct iio_chan_spec const *chan, int *val,
+			  int *val2, long mask)
+{
+	struct sgp41_data *data = iio_priv(indio_dev);
+	u16 voc_raw;
+	u16 nox_raw;
+	int ret;
+
+	switch (mask) {
+	case IIO_CHAN_INFO_RAW:
+		switch (chan->type) {
+		case IIO_RESISTANCE:
+			ret = sgp41_measure_raw(data,
+							   &voc_raw,
+							   &nox_raw);
+			if (ret)
+				return ret;
+
+			switch (chan->channel) {
+			case 0:
+				*val = voc_raw;
+				return IIO_VAL_INT;
+			case 1:
+				*val = nox_raw;
+				return IIO_VAL_INT;
+			default:
+				return -EINVAL;
+			}
+
+		case IIO_TEMP:
+			mutex_lock(&data->lock);
+			*val = data->temp;
+			mutex_unlock(&data->lock);
+			return IIO_VAL_INT;
+
+		case IIO_HUMIDITYRELATIVE:
+			mutex_lock(&data->lock);
+			*val = data->rht;
+			mutex_unlock(&data->lock);
+			return IIO_VAL_INT;
+
+		default:
+			return -EINVAL;
+		}
+
+	default:
+		return -EINVAL;
+	}
+}
+
+static int sgp41_write_raw(struct iio_dev *indio_dev,
+			struct iio_chan_spec const *chan, int val,
+			int val2, long mask)
+{
+	struct sgp41_data *data = iio_priv(indio_dev);
+
+	switch (mask) {
+	case IIO_CHAN_INFO_RAW:
+		switch (chan->type) {
+		case IIO_TEMP:
+			if ((val < -45000) || (val > 130000))
+				return -EINVAL;
+
+			mutex_lock(&data->lock);
+			data->temp = val;
+			mutex_unlock(&data->lock);
+			return 0;
+
+		case IIO_HUMIDITYRELATIVE:
+			if ((val < 0) || (val > 100000))
+				return -EINVAL;
+
+			mutex_lock(&data->lock);
+			data->rht = val;
+			mutex_unlock(&data->lock);
+			return 0;
+
+		default:
+			return -EINVAL;
+			}
+	default:
+		return -EINVAL;
+	}
+}
+
+static int sgp41_heater_off(struct sgp41_data *data)
+{
+	u8 heater_off_cmd[2] = { 0x36, 0x15 };
+	int ret;
+
+	ret = i2c_master_send(data->client, heater_off_cmd,
+			      sizeof(heater_off_cmd));
+	if (ret != sizeof(heater_off_cmd)) {
+		dev_warn(data->dev,
+			 "i2c_master_send ret: %d sizeof: %zu\n",
+			 ret, sizeof(heater_off_cmd));
+		return ret < 0 ? ret : -EIO;
+	}
+
+	dev_dbg(data->dev, "heater turned off\n");
+
+	return 0;
+}
+
+static void sgp41_remove(struct i2c_client *client)
+{
+	struct iio_dev *indio_dev = i2c_get_clientdata(client);
+	struct sgp41_data *data = iio_priv(indio_dev);
+
+	sgp41_heater_off(data);
+}
+
+static const struct iio_info sgp41_info = {
+	.read_raw = sgp41_read_raw,
+	.write_raw = sgp41_write_raw,
+};
+
+static int sgp41_probe(struct i2c_client *client)
+{
+	const struct i2c_device_id *id = i2c_client_get_device_id(client);
+	struct device *dev = &client->dev;
+	struct iio_dev *indio_dev;
+	struct sgp41_data *data;
+	int ret;
+
+	indio_dev = devm_iio_device_alloc(dev, sizeof(*data));
+	if (!indio_dev)
+		return -ENOMEM;
+
+	i2c_set_clientdata(client, indio_dev);
+
+	data = iio_priv(indio_dev);
+	data->client = client;
+	data->dev = dev;
+	data->conditioned = false;
+
+	crc8_populate_msb(sgp41_crc8_table, SGP41_CRC8_POLYNOMIAL);
+
+	mutex_init(&data->lock);
+
+	data->rht = 50000;
+	data->temp = 25000;
+
+	indio_dev->info = &sgp41_info;
+
+	if (id)
+		indio_dev->name = id->name;
+	else
+		indio_dev->name = "sgp41";
+
+	indio_dev->modes = INDIO_DIRECT_MODE;
+	indio_dev->channels = sgp41_channels;
+	indio_dev->num_channels = ARRAY_SIZE(sgp41_channels);
+
+	ret = devm_iio_device_register(dev, indio_dev);
+	if (ret)
+		dev_err(dev, "failed to register iio device\n");
+
+	return ret;
+}
+
+static const struct i2c_device_id sgp41_id[] = {
+	{ .name = "sgp41" },
+	{ }
+};
+
+MODULE_DEVICE_TABLE(i2c, sgp41_id);
+
+static const struct of_device_id sgp41_dt_ids[] = {
+	{ .compatible = "sensirion,sgp41" },
+	{ }
+};
+
+MODULE_DEVICE_TABLE(of, sgp41_dt_ids);
+
+static struct i2c_driver sgp41_driver = {
+	.driver = {
+		.name = "sgp41",
+		.of_match_table = sgp41_dt_ids,
+	},
+	.probe = sgp41_probe,
+	.id_table = sgp41_id,
+	.remove = sgp41_remove,
+};
+
+module_i2c_driver(sgp41_driver);
+
+MODULE_AUTHOR("Akshat Chandra <notmissinge@gmail.com>");
+MODULE_DESCRIPTION("Sensirion SGP41 gas sensor");
+MODULE_LICENSE("GPL v2");
-- 
2.53.0


             reply	other threads:[~2026-08-31 15:13 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-31 15:12 Akshat Chandra [this message]
2026-08-31 17:27 ` [PATCH] iio: chemical: add SGP41 driver Andy Shevchenko
2026-08-31 21:21 ` Joshua Crofts
2026-09-04  3:22   ` Jonathan Cameron

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=20260831151235.64076-1-notmissinge@gmail.com \
    --to=notmissinge@gmail.com \
    --cc=andy@kernel.org \
    --cc=dlechner@baylibre.com \
    --cc=jic23@kernel.org \
    --cc=linux-iio@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=nuno.sa@analog.com \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.