* [PATCH] iio: chemical: add SGP41 driver
@ 2026-08-31 15:12 Akshat Chandra
2026-08-31 17:27 ` Andy Shevchenko
2026-08-31 21:21 ` Joshua Crofts
0 siblings, 2 replies; 4+ messages in thread
From: Akshat Chandra @ 2026-08-31 15:12 UTC (permalink / raw)
To: linux-iio
Cc: Jonathan Cameron, David Lechner, Nuno Sá, Andy Shevchenko,
linux-kernel, Akshat Chandra
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
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH] iio: chemical: add SGP41 driver
2026-08-31 15:12 [PATCH] iio: chemical: add SGP41 driver Akshat Chandra
@ 2026-08-31 17:27 ` Andy Shevchenko
2026-08-31 21:21 ` Joshua Crofts
1 sibling, 0 replies; 4+ messages in thread
From: Andy Shevchenko @ 2026-08-31 17:27 UTC (permalink / raw)
To: Akshat Chandra
Cc: linux-iio, Jonathan Cameron, David Lechner, Nuno Sá,
Andy Shevchenko, linux-kernel
On Mon, Aug 31, 2026 at 08:42:35PM +0530, Akshat Chandra wrote:
NO, we do not accept this.
Please, try to read kernel documentation and follow it.
This patch is far from the kernel standards, sorry.
I would recommend to also read others' emails in the mailing list (linux-iio@)
along with the code of recently accepted drivers (can be found in v7.3-rc1)
that follow (more or less) the standards we require.
> Signed-off-by: Akshat Chandra <notmissinge@gmail.com>
--
With Best Regards,
Andy Shevchenko
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH] iio: chemical: add SGP41 driver
2026-08-31 15:12 [PATCH] iio: chemical: add SGP41 driver Akshat Chandra
2026-08-31 17:27 ` Andy Shevchenko
@ 2026-08-31 21:21 ` Joshua Crofts
2026-09-04 3:22 ` Jonathan Cameron
1 sibling, 1 reply; 4+ messages in thread
From: Joshua Crofts @ 2026-08-31 21:21 UTC (permalink / raw)
To: Akshat Chandra
Cc: linux-iio, Jonathan Cameron, David Lechner, Nuno Sá,
Andy Shevchenko, linux-kernel
On Mon, 31 Aug 2026 20:42:35 +0530
Akshat Chandra <notmissinge@gmail.com> wrote:
> 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
>
Not only you're missing a commit message, you're also missing a
MAINTAINERS entry and a dt-bindings entry! Please fix this!
(Note, the dt-bindings and MAINTAINERS entry should go together
in one patch, preferrably first in the series).
--
Kind regards,
Joshua Crofts
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH] iio: chemical: add SGP41 driver
2026-08-31 21:21 ` Joshua Crofts
@ 2026-09-04 3:22 ` Jonathan Cameron
0 siblings, 0 replies; 4+ messages in thread
From: Jonathan Cameron @ 2026-09-04 3:22 UTC (permalink / raw)
To: Joshua Crofts
Cc: Akshat Chandra, linux-iio, David Lechner, Nuno Sá,
Andy Shevchenko, linux-kernel
On Mon, 31 Aug 2026 23:21:57 +0200
Joshua Crofts <joshua.crofts1@gmail.com> wrote:
> On Mon, 31 Aug 2026 20:42:35 +0530
> Akshat Chandra <notmissinge@gmail.com> wrote:
>
> > 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
> >
>
> Not only you're missing a commit message, you're also missing a
> MAINTAINERS entry and a dt-bindings entry! Please fix this!
> (Note, the dt-bindings and MAINTAINERS entry should go together
> in one patch, preferrably first in the series).
>
Whilst it should be a separate patch, the route here to use
trivial-devices might make sense. Only thing that might be
worth documenting would be vdd-supply.
I'm would be looking for description in the cover letter of how this
is different from, and not suited to extension of another drive
such as the sgp40 (obviously there are more channels on one than
the other so it isn't trivial!)
Jonathan
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-09-04 3:22 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-31 15:12 [PATCH] iio: chemical: add SGP41 driver Akshat Chandra
2026-08-31 17:27 ` Andy Shevchenko
2026-08-31 21:21 ` Joshua Crofts
2026-09-04 3:22 ` Jonathan Cameron
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox