iio: chemical: add support for Aosong AGS02MA
authorAnshul Dalal <anshulusr@gmail.com>
Fri, 15 Dec 2023 16:23:11 +0000 (21:53 +0530)
committerJonathan Cameron <Jonathan.Cameron@huawei.com>
Sun, 17 Dec 2023 14:44:35 +0000 (14:44 +0000)
A simple driver for the TVOC (Total Volatile Organic Compounds)
sensor from Aosong: AGS02MA

Steps in reading the VOC sensor value over i2c:
  1. Read 5 bytes from the register `AGS02MA_TVOC_READ_REG` [0x00]
  2. The first 4 bytes are taken as the big endian sensor data with final
     byte being the CRC
  3. The CRC is verified and the value is returned over an
     `IIO_CHAN_INFO_RAW` channel as percents

Tested on Raspberry Pi Zero 2W

Datasheet: https://asairsensors.com/wp-content/uploads/2021/09/AGS02MA.pdf
Signed-off-by: Anshul Dalal <anshulusr@gmail.com>
Link: https://lore.kernel.org/r/20231215162312.143568-3-anshulusr@gmail.com
Signed-off-by: Jonathan Cameron <Jonathan.Cameron@huawei.com>
MAINTAINERS
drivers/iio/chemical/Kconfig
drivers/iio/chemical/Makefile
drivers/iio/chemical/ags02ma.c [new file with mode: 0644]

index 4eddc4212f2b8b452dacf2cbf3f8edacab00407f..3029841e92a8596f83a374f393f2400baff89c37 100644 (file)
@@ -3071,6 +3071,13 @@ S:       Supported
 W:     http://www.akm.com/
 F:     drivers/iio/magnetometer/ak8974.c
 
+AOSONG AGS02MA TVOC SENSOR DRIVER
+M:     Anshul Dalal <anshulusr@gmail.com>
+L:     linux-iio@vger.kernel.org
+S:     Maintained
+F:     Documentation/devicetree/bindings/iio/chemical/aosong,ags02ma.yaml
+F:     drivers/iio/chemical/ags02ma.c
+
 ASC7621 HARDWARE MONITOR DRIVER
 M:     George Joseph <george.joseph@fairview5.com>
 L:     linux-hwmon@vger.kernel.org
index c30657e10ee17a400cab7e6e1a8e5e3244ec977c..02649ab81b3cef9d78935a1433f54b60aa6f77ea 100644 (file)
@@ -5,6 +5,17 @@
 
 menu "Chemical Sensors"
 
+config AOSONG_AGS02MA
+       tristate "Aosong AGS02MA TVOC sensor driver"
+       depends on I2C
+       select CRC8
+       help
+         Say Y here to build support for Aosong AGS02MA TVOC (Total Volatile
+         Organic Compounds) sensor.
+
+         To compile this driver as module, choose M here: the module will be
+         called ags02ma.
+
 config ATLAS_PH_SENSOR
        tristate "Atlas Scientific OEM SM sensors"
        depends on I2C
index a11e777a7a00521d7eb8806ae8745ad7b12c651b..2f3dee8bb779600d874711f5c33392de5ec196ba 100644 (file)
@@ -4,6 +4,7 @@
 #
 
 # When adding new entries keep the list in alphabetical order
+obj-$(CONFIG_AOSONG_AGS02MA)   += ags02ma.o
 obj-$(CONFIG_ATLAS_PH_SENSOR)  += atlas-sensor.o
 obj-$(CONFIG_ATLAS_EZO_SENSOR) += atlas-ezo-sensor.o
 obj-$(CONFIG_BME680) += bme680_core.o
diff --git a/drivers/iio/chemical/ags02ma.c b/drivers/iio/chemical/ags02ma.c
new file mode 100644 (file)
index 0000000..8fcd809
--- /dev/null
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2023 Anshul Dalal <anshulusr@gmail.com>
+ *
+ * Driver for Aosong AGS02MA
+ *
+ * Datasheet:
+ *   https://asairsensors.com/wp-content/uploads/2021/09/AGS02MA.pdf
+ * Product Page:
+ *   http://www.aosong.com/m/en/products-33.html
+ */
+
+#include <linux/crc8.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+
+#include <linux/iio/iio.h>
+
+#define AGS02MA_TVOC_READ_REG             0x00
+#define AGS02MA_VERSION_REG               0x11
+
+#define AGS02MA_VERSION_PROCESSING_DELAY   30
+#define AGS02MA_TVOC_READ_PROCESSING_DELAY 1500
+
+#define AGS02MA_CRC8_INIT                 0xff
+#define AGS02MA_CRC8_POLYNOMIAL                   0x31
+
+DECLARE_CRC8_TABLE(ags02ma_crc8_table);
+
+struct ags02ma_data {
+       struct i2c_client *client;
+};
+
+struct ags02ma_reading {
+       __be32 data;
+       u8 crc;
+} __packed;
+
+static int ags02ma_register_read(struct i2c_client *client, u8 reg, u16 delay,
+                                u32 *val)
+{
+       int ret;
+       u8 crc;
+       struct ags02ma_reading read_buffer;
+
+       ret = i2c_master_send(client, &reg, sizeof(reg));
+       if (ret < 0) {
+               dev_err(&client->dev,
+                       "Failed to send data to register 0x%x: %d", reg, ret);
+               return ret;
+       }
+
+       /* Processing Delay, Check Table 7.7 in the datasheet */
+       msleep_interruptible(delay);
+
+       ret = i2c_master_recv(client, (u8 *)&read_buffer, sizeof(read_buffer));
+       if (ret < 0) {
+               dev_err(&client->dev,
+                       "Failed to receive from register 0x%x: %d", reg, ret);
+               return ret;
+       }
+
+       crc = crc8(ags02ma_crc8_table, (u8 *)&read_buffer.data,
+                  sizeof(read_buffer.data), AGS02MA_CRC8_INIT);
+       if (crc != read_buffer.crc) {
+               dev_err(&client->dev, "CRC error\n");
+               return -EIO;
+       }
+
+       *val = be32_to_cpu(read_buffer.data);
+       return 0;
+}
+
+static int ags02ma_read_raw(struct iio_dev *iio_device,
+                           struct iio_chan_spec const *chan, int *val,
+                           int *val2, long mask)
+{
+       int ret;
+       struct ags02ma_data *data = iio_priv(iio_device);
+
+       switch (mask) {
+       case IIO_CHAN_INFO_RAW:
+               ret = ags02ma_register_read(data->client, AGS02MA_TVOC_READ_REG,
+                                           AGS02MA_TVOC_READ_PROCESSING_DELAY,
+                                           val);
+               if (ret < 0)
+                       return ret;
+               return IIO_VAL_INT;
+       case IIO_CHAN_INFO_SCALE:
+               /* The sensor reads data as ppb */
+               *val = 0;
+               *val2 = 100;
+               return IIO_VAL_INT_PLUS_NANO;
+       default:
+               return -EINVAL;
+       }
+}
+
+static const struct iio_info ags02ma_info = {
+       .read_raw = ags02ma_read_raw,
+};
+
+static const struct iio_chan_spec ags02ma_channel = {
+       .type = IIO_CONCENTRATION,
+       .channel2 = IIO_MOD_VOC,
+       .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+               BIT(IIO_CHAN_INFO_SCALE),
+};
+
+static int ags02ma_probe(struct i2c_client *client)
+{
+       int ret;
+       struct ags02ma_data *data;
+       struct iio_dev *indio_dev;
+       u32 version;
+
+       indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
+       if (!indio_dev)
+               return -ENOMEM;
+
+       crc8_populate_msb(ags02ma_crc8_table, AGS02MA_CRC8_POLYNOMIAL);
+
+       ret = ags02ma_register_read(client, AGS02MA_VERSION_REG,
+                                   AGS02MA_VERSION_PROCESSING_DELAY, &version);
+       if (ret < 0)
+               return dev_err_probe(&client->dev, ret,
+                             "Failed to read device version\n");
+       dev_dbg(&client->dev, "Aosong AGS02MA, Version: 0x%x", version);
+
+       data = iio_priv(indio_dev);
+       data->client = client;
+       indio_dev->info = &ags02ma_info;
+       indio_dev->channels = &ags02ma_channel;
+       indio_dev->num_channels = 1;
+       indio_dev->name = "ags02ma";
+
+       return devm_iio_device_register(&client->dev, indio_dev);
+}
+
+static const struct i2c_device_id ags02ma_id_table[] = {
+       { "ags02ma" },
+       { /* Sentinel */ }
+};
+MODULE_DEVICE_TABLE(i2c, ags02ma_id_table);
+
+static const struct of_device_id ags02ma_of_table[] = {
+       { .compatible = "aosong,ags02ma" },
+       { /* Sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, ags02ma_of_table);
+
+static struct i2c_driver ags02ma_driver = {
+       .driver = {
+               .name = "ags02ma",
+               .of_match_table = ags02ma_of_table,
+       },
+       .id_table = ags02ma_id_table,
+       .probe = ags02ma_probe,
+};
+module_i2c_driver(ags02ma_driver);
+
+MODULE_AUTHOR("Anshul Dalal <anshulusr@gmail.com>");
+MODULE_DESCRIPTION("Aosong AGS02MA TVOC Driver");
+MODULE_LICENSE("GPL");