[PATCH 1/2] iio: chemical: sgpxx: Support Sensirion SGPxx sensors

From: Andreas Brauchli
Date: Tue Nov 21 2017 - 11:11:35 EST


Support Sensirion SGP30 and SGPC3 multi-pixel I2C gas sensors

Supported Features:

* Indoor Air Quality (IAQ) concentrations for
SGP30 and SGPC3:
- tVOC (in_concentration_voc_input)
SGP30 only:
- CO2eq (in_concentration_co2_input)
IAQ must first be initialized by writing a non-empty value to
out_iaq_init. After initializing IAQ, at least one IAQ signal must
be read out every second (SGP30) / every two seconds (SGPC3) for the
sensor to correctly maintain its internal baseline
* Baseline support for IAQ (in_iaq_baseline, out_iaq_baseline)
* Gas concentration signals for
SGP30 and SGPC3:
- Ethanol (in_concentration_ethanol_raw)
SGP30 only:
- H2 (in_concentration_h2_raw)
* On-chip self test (in_selftest)
The self test interferes with IAQ operations. If needed, first
retrieve the current baseline, then reset it after the self test
* Sensor interface version (in_feature_set_version)
* Sensor serial number (in_serial_id)
* Humidity compensation for SGP30
With the help of a humidity signal, the gas signals can be
humidity-compensated.
* Checksummed I2C communication

For all features, refer to the data sheet or the documentation in
Documentation/iio/chemical/sgpxx.txt for more details.

Signed-off-by: Andreas Brauchli <andreas.brauchli@xxxxxxxxxxxxx>
---
Documentation/iio/chemical/sgpxx.txt | 112 +++++
drivers/iio/chemical/Kconfig | 13 +
drivers/iio/chemical/Makefile | 1 +
drivers/iio/chemical/sgpxx.c | 894 +++++++++++++++++++++++++++++++++++
4 files changed, 1020 insertions(+)
create mode 100644 Documentation/iio/chemical/sgpxx.txt
create mode 100644 drivers/iio/chemical/sgpxx.c

diff --git a/Documentation/iio/chemical/sgpxx.txt b/Documentation/iio/chemical/sgpxx.txt
new file mode 100644
index 000000000000..f49b2f365df3
--- /dev/null
+++ b/Documentation/iio/chemical/sgpxx.txt
@@ -0,0 +1,112 @@
+sgpxx: Industrial IO driver for Sensirion i2c Multi-Pixel Gas Sensors
+
+1. Overview
+
+The sgpxx driver supports the Sensirion SGP30 and SGPC3 multi-pixel gas sensors.
+
+Datasheets:
+https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9_Gas_Sensors/Sensirion_Gas_Sensors_SGP30_Datasheet_EN.pdf
+https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9_Gas_Sensors/Sensirion_Gas_Sensors_SGPC3_Datasheet_EN.pdf
+
+2. Modes of Operation
+
+2.1. Driver Instantiation
+
+The sgpxx driver must be instantiated on the corresponding i2c bus with the
+product name (sgp30 or sgpc3) and i2c address (0x58).
+
+Example instantiation of an sgp30 on i2c bus 1 (i2c-1):
+
+ $ echo sgp30 0x58 | sudo tee /sys/bus/i2c/devices/i2c-1/new_device
+
+Using the wrong product name results in an instantiation error. Check dmesg.
+
+2.2. Indoor Air Quality (IAQ) concentrations
+
+* tVOC (in_concentration_voc_input) at ppb precision (1e-9)
+* CO2eq (in_concentration_co2_input) at ppm precision (1e-6) -- SGP30 only
+
+2.2.1. IAQ Initialization
+Before Indoor Air Quality (IAQ) values can be read, the IAQ mode must be
+initialized by writing a non-empty value to out_iaq_init:
+
+ $ echo init > out_iaq_init
+
+After initializing IAQ, at least one IAQ signal must be read out every second
+(SGP30) / every two seconds (SGPC3) for the sensor to correctly maintain its
+internal baseline:
+
+ SGP30:
+ $ watch -n1 cat in_concentration_voc_input
+
+ SGPC3:
+ $ watch -n2 cat in_concentration_voc_input
+
+For the first 15s of operation after writing to out_iaq_init, default values are
+retured by the sensor.
+
+2.2.2. Pausing and Resuming IAQ
+
+For best performance and faster startup times, the baseline should be saved
+once every hour, after 12h of operation. The baseline is restored by writing a
+non-empty value to out_iaq_init, followed by writing an unmodified retrieved
+baseline value from in_iaq_baseline to out_iaq_baseline.
+
+ Saving the baseline:
+ $ baseline=$(cat in_iaq_baseline)
+
+ Restoring the baseline:
+ $ echo init > out_iaq_init
+ $ echo -n $baseline > out_iaq_baseline
+
+2.3. Gas Concentration Signals
+
+* Ethanol (in_concentration_ethanol_raw)
+* H2 (in_concentration_h2_raw) -- SGP30 only
+
+The gas signals in_concentration_ethanol_raw and in_concentration_h2_raw may be
+used without prior write to out_iaq_init.
+
+2.4. Humidity Compensation (SGP30)
+
+The SGP30 features an on-chip humidity compensation that requires the
+(in-device) environment's absolute humidity.
+
+Set the absolute humidity by writing the absolute humidity concentration (in
+mg/m^3) to out_concentration_ah_raw. The absolute humidity is obtained by
+converting the relative humidity and temperature. The following units are used:
+AH in mg/m^3, RH in percent (0..100), T in degrees Celsius, and exp() being the
+base-e exponential function.
+
+ RH exp(17.62 * T)
+ ----- * 6.112 * --------------
+ 100.0 243.12 + T
+ AH = 216.7 * ------------------------------- * 1000
+ 273.15 + T
+
+Writing a value of 0 to out_absolute_humidity disables the humidity
+compensation.
+
+2.5. On-chip self test
+
+ $ cat in_selftest
+
+in_selftest returns OK or FAILED.
+
+The self test interferes with IAQ operations. If needed, first save the current
+baseline, then restore it after the self test:
+
+ $ baseline=$(cat in_iaq_baseline)
+ $ cat in_selftest
+ $ echo init > out_iaq_init
+ $ echo -n $baseline > out_iaq_baseline
+
+If the sensor's current operating duration is less than 12h the baseline should
+not be restored by skipping the last step.
+
+3. Sensor Interface
+
+ $ cat in_feature_set_version
+
+The SGP sensors' minor interface (feature set) version guarantees interface
+stability: a sensor with feature set 1.1 works with a driver for feature set 1.0
diff --git a/drivers/iio/chemical/Kconfig b/drivers/iio/chemical/Kconfig
index 5cb5be7612b4..4574dd687513 100644
--- a/drivers/iio/chemical/Kconfig
+++ b/drivers/iio/chemical/Kconfig
@@ -38,6 +38,19 @@ config IAQCORE
iAQ-Core Continuous/Pulsed VOC (Volatile Organic Compounds)
sensors

+config SENSIRION_SGPXX
+ tristate "Sensirion SGPxx gas sensors"
+ depends on I2C
+ select CRC8
+ help
+ Say Y here to build I2C interface support for the following
+ Sensirion SGP gas sensors:
+ * SGP30 gas sensor
+ * SGPC3 gas sensor
+
+ To compile this driver as module, choose M here: the
+ module will be called sgpxx.
+
config VZ89X
tristate "SGX Sensortech MiCS VZ89X VOC sensor"
depends on I2C
diff --git a/drivers/iio/chemical/Makefile b/drivers/iio/chemical/Makefile
index a629b29d1e0b..6090a0ae3981 100644
--- a/drivers/iio/chemical/Makefile
+++ b/drivers/iio/chemical/Makefile
@@ -6,4 +6,5 @@
obj-$(CONFIG_ATLAS_PH_SENSOR) += atlas-ph-sensor.o
obj-$(CONFIG_CCS811) += ccs811.o
obj-$(CONFIG_IAQCORE) += ams-iaq-core.o
+obj-$(CONFIG_SENSIRION_SGPXX) += sgpxx.o
obj-$(CONFIG_VZ89X) += vz89x.o
diff --git a/drivers/iio/chemical/sgpxx.c b/drivers/iio/chemical/sgpxx.c
new file mode 100644
index 000000000000..aea55e41d4cc
--- /dev/null
+++ b/drivers/iio/chemical/sgpxx.c
@@ -0,0 +1,894 @@
+/*
+ * sgpxx.c - Support for Sensirion SGP Gas Sensors
+ *
+ * Copyright (C) 2017 Andreas Brauchli <andreas.brauchli@xxxxxxxxxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * Datasheets:
+ * https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9_Gas_Sensors/Sensirion_Gas_Sensors_SGP30_Datasheet_EN.pdf
+ * https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9_Gas_Sensors/Sensirion_Gas_Sensors_SGPC3_Datasheet_EN.pdf
+ */
+
+#include <linux/crc8.h>
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/init.h>
+#include <linux/i2c.h>
+#include <linux/of_device.h>
+#include <linux/iio/iio.h>
+#include <linux/iio/buffer.h>
+#include <linux/iio/sysfs.h>
+
+#define SGP_WORD_LEN 2
+#define SGP_CRC8_POLYNOMIAL 0x31
+#define SGP_CRC8_INIT 0xff
+#define SGP_CRC8_LEN 1
+#define SGP_CMD(cmd_word) cpu_to_be16(cmd_word)
+#define SGP_CMD_DURATION_US 50000
+#define SGP_SELFTEST_DURATION_US 220000
+#define SGP_CMD_HANDLING_DURATION_US 10000
+#define SGP_CMD_LEN SGP_WORD_LEN
+#define SGP30_MEASUREMENT_LEN 2
+#define SGPC3_MEASUREMENT_LEN 2
+#define SGP30_MEASURE_INTERVAL_HZ 1
+#define SGPC3_MEASURE_INTERVAL_HZ 2
+#define SGP_SELFTEST_OK 0xd400
+
+DECLARE_CRC8_TABLE(sgp_crc8_table);
+
+enum sgp_product_id {
+ SGP30 = 0,
+ SGPC3
+};
+
+enum sgp30_channel_idx {
+ SGP30_IAQ_TVOC_IDX = 0,
+ SGP30_IAQ_CO2EQ_IDX,
+ SGP30_SIG_ETOH_IDX,
+ SGP30_SIG_H2_IDX,
+ SGP30_SET_AH_IDX,
+};
+
+enum sgpc3_channel_idx {
+ SGPC3_IAQ_TVOC_IDX = 10,
+ SGPC3_SIG_ETOH_IDX,
+};
+
+enum sgp_cmd {
+ SGP_CMD_IAQ_INIT = SGP_CMD(0x2003),
+ SGP_CMD_IAQ_MEASURE = SGP_CMD(0x2008),
+ SGP_CMD_GET_BASELINE = SGP_CMD(0x2015),
+ SGP_CMD_SET_BASELINE = SGP_CMD(0x201e),
+ SGP_CMD_GET_FEATURE_SET = SGP_CMD(0x202f),
+ SGP_CMD_GET_SERIAL_ID = SGP_CMD(0x3682),
+ SGP_CMD_MEASURE_TEST = SGP_CMD(0x2032),
+
+ SGP30_CMD_MEASURE_SIGNAL = SGP_CMD(0x2050),
+ SGP30_CMD_SET_ABSOLUTE_HUMIDITY = SGP_CMD(0x2061),
+
+ SGPC3_CMD_IAQ_INIT0 = SGP_CMD(0x2089),
+ SGPC3_CMD_IAQ_INIT16 = SGP_CMD(0x2024),
+ SGPC3_CMD_IAQ_INIT64 = SGP_CMD(0x2003),
+ SGPC3_CMD_IAQ_INIT184 = SGP_CMD(0x206a),
+ SGPC3_CMD_MEASURE_RAW = SGP_CMD(0x2046),
+};
+
+enum sgp_measure_mode {
+ SGP_MEASURE_MODE_UNKNOWN,
+ SGP_MEASURE_MODE_IAQ,
+ SGP_MEASURE_MODE_SIGNAL,
+ SGP_MEASURE_MODE_ALL,
+};
+
+struct sgp_version {
+ u8 major;
+ u8 minor;
+};
+
+struct sgp_crc_word {
+ __be16 value;
+ u8 crc8;
+} __attribute__((__packed__));
+
+union sgp_reading {
+ u8 start;
+ struct sgp_crc_word raw_words[4];
+};
+
+struct sgp_data {
+ struct i2c_client *client;
+ struct mutex data_lock; /* mutex to lock access to data buffer */
+ struct mutex i2c_lock; /* mutex to lock access to i2c */
+ unsigned long last_update;
+
+ u64 serial_id;
+ u16 chip_id;
+ u16 feature_set;
+ u16 measurement_len;
+ int measure_interval_hz;
+ enum sgp_cmd measure_iaq_cmd;
+ enum sgp_cmd measure_signal_cmd;
+ enum sgp_measure_mode measure_mode;
+ char *baseline_format;
+ bool iaq_initialized;
+ u8 baseline_len;
+ union sgp_reading buffer;
+};
+
+struct sgp_device {
+ const struct iio_chan_spec *channels;
+ int num_channels;
+};
+
+static const struct sgp_version supported_versions_sgp30[] = {
+ {
+ .major = 1,
+ .minor = 0,
+ }
+};
+
+static const struct sgp_version supported_versions_sgpc3[] = {
+ {
+ .major = 0,
+ .minor = 4,
+ }
+};
+
+static const struct iio_chan_spec sgp30_channels[] = {
+ {
+ .type = IIO_CONCENTRATION,
+ .channel2 = IIO_MOD_VOC,
+ .datasheet_name = "TVOC signal",
+ .scan_index = 0,
+ .modified = 1,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
+ .address = SGP30_IAQ_TVOC_IDX,
+ },
+ {
+ .type = IIO_CONCENTRATION,
+ .channel2 = IIO_MOD_CO2,
+ .datasheet_name = "CO2eq signal",
+ .scan_index = 1,
+ .modified = 1,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
+ .address = SGP30_IAQ_CO2EQ_IDX,
+ },
+ {
+ .type = IIO_CONCENTRATION,
+ .info_mask_separate =
+ BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE),
+ .address = SGP30_SIG_ETOH_IDX,
+ .extend_name = "ethanol",
+ .datasheet_name = "Ethanol signal",
+ .scan_index = 2,
+ .scan_type = {
+ .endianness = IIO_BE,
+ },
+ },
+ {
+ .type = IIO_CONCENTRATION,
+ .info_mask_separate =
+ BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE),
+ .address = SGP30_SIG_H2_IDX,
+ .extend_name = "h2",
+ .datasheet_name = "H2 signal",
+ .scan_index = 3,
+ .scan_type = {
+ .endianness = IIO_BE,
+ },
+ },
+ IIO_CHAN_SOFT_TIMESTAMP(4),
+ {
+ .type = IIO_CONCENTRATION,
+ .address = SGP30_SET_AH_IDX,
+ .extend_name = "ah",
+ .datasheet_name = "absolute humidty",
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
+ .output = 1,
+ .scan_index = 5
+ },
+};
+
+static const struct iio_chan_spec sgpc3_channels[] = {
+ {
+ .type = IIO_CONCENTRATION,
+ .channel2 = IIO_MOD_VOC,
+ .datasheet_name = "TVOC signal",
+ .modified = 1,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
+ .address = SGPC3_IAQ_TVOC_IDX,
+ },
+ {
+ .type = IIO_CONCENTRATION,
+ .info_mask_separate =
+ BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE),
+ .address = SGPC3_SIG_ETOH_IDX,
+ .extend_name = "ethanol",
+ .datasheet_name = "Ethanol signal",
+ .scan_index = 0,
+ .scan_type = {
+ .endianness = IIO_BE,
+ },
+ },
+ IIO_CHAN_SOFT_TIMESTAMP(2),
+};
+
+static struct sgp_device sgp_devices[] = {
+ [SGP30] = {
+ .channels = sgp30_channels,
+ .num_channels = ARRAY_SIZE(sgp30_channels),
+ },
+ [SGPC3] = {
+ .channels = sgpc3_channels,
+ .num_channels = ARRAY_SIZE(sgpc3_channels),
+ },
+};
+
+/**
+ * sgp_verify_buffer() - verify the checksums of the data buffer words
+ *
+ * @data: SGP data containing the raw buffer
+ * @word_count: Num data words stored in the buffer, excluding CRC bytes
+ *
+ * Return: 0 on success, negative error code otherwise
+ */
+static int sgp_verify_buffer(struct sgp_data *data, size_t word_count)
+{
+ size_t size = word_count * (SGP_WORD_LEN + SGP_CRC8_LEN);
+ int i;
+ u8 crc;
+ u8 *data_buf = &data->buffer.start;
+
+ for (i = 0; i < size; i += SGP_WORD_LEN + SGP_CRC8_LEN) {
+ crc = crc8(sgp_crc8_table, &data_buf[i], SGP_WORD_LEN,
+ SGP_CRC8_INIT);
+ if (crc != data_buf[i + SGP_WORD_LEN]) {
+ dev_err(&data->client->dev, "CRC error\n");
+ return -EIO;
+ }
+ }
+ return 0;
+}
+
+/**
+ * sgp_read_from_cmd() - reads data from SGP sensor after issuing a command
+ * The caller must hold data->data_lock for the duration of the call.
+ * @data: SGP data
+ * @cmd: SGP Command to issue
+ * @word_count: Num words to read, excluding CRC bytes
+ *
+ * Return: 0 on success, negative error otherwise.
+ */
+static int sgp_read_from_cmd(struct sgp_data *data,
+ enum sgp_cmd cmd,
+ size_t word_count,
+ unsigned long duration_us)
+{
+ int ret;
+ struct i2c_client *client = data->client;
+ size_t size = word_count * (SGP_WORD_LEN + SGP_CRC8_LEN);
+ u8 *data_buf = &data->buffer.start;
+
+ mutex_lock(&data->i2c_lock);
+ ret = i2c_master_send(client, (const char *)&cmd, SGP_CMD_LEN);
+ if (ret != SGP_CMD_LEN) {
+ mutex_unlock(&data->i2c_lock);
+ return -EIO;
+ }
+ usleep_range(duration_us, duration_us + 1000);
+
+ ret = i2c_master_recv(client, data_buf, size);
+ mutex_unlock(&data->i2c_lock);
+
+ if (ret < 0)
+ ret = -ETXTBSY;
+ else if (ret != size)
+ ret = -EINTR;
+ else
+ ret = sgp_verify_buffer(data, word_count);
+
+ return ret;
+}
+
+/**
+ * sgp_i2c_write_from_cmd() - write data to SGP sensor with a command
+ * @data: SGP data
+ * @cmd: SGP Command to issue
+ * @buf: Data to write
+ * @buf_size: Data size of the buffer
+ *
+ * Return: 0 on success, negative error otherwise.
+ */
+static int sgp_write_from_cmd(struct sgp_data *data,
+ enum sgp_cmd cmd,
+ u16 *buf,
+ size_t buf_size,
+ unsigned long duration_us)
+{
+ int ret, ix;
+ u16 buf_idx = 0;
+ u16 buffer_size = SGP_CMD_LEN + buf_size *
+ (SGP_WORD_LEN + SGP_CRC8_LEN);
+ u8 buffer[buffer_size];
+
+ /* assemble buffer */
+ *((u16 *)&buffer[0]) = cmd;
+ buf_idx += SGP_CMD_LEN;
+ for (ix = 0; ix < buf_size; ix++) {
+ *((u16 *)&buffer[buf_idx]) = ntohs(buf[ix] & 0xffff);
+ buf_idx += SGP_WORD_LEN;
+ buffer[buf_idx] = crc8(sgp_crc8_table,
+ &buffer[buf_idx - SGP_WORD_LEN],
+ SGP_WORD_LEN, SGP_CRC8_INIT);
+ buf_idx += SGP_CRC8_LEN;
+ }
+ mutex_lock(&data->i2c_lock);
+ ret = i2c_master_send(data->client, buffer, buffer_size);
+ if (ret != buffer_size) {
+ ret = -EIO;
+ goto unlock_return_count;
+ }
+ ret = 0;
+ /* Wait inside lock to ensure the chip is ready before next command */
+ usleep_range(duration_us, duration_us + 1000);
+
+unlock_return_count:
+ mutex_unlock(&data->i2c_lock);
+ return ret;
+}
+
+/**
+ * sgp_get_measurement() - retrieve measurement result from sensor
+ * The caller must hold data->data_lock for the duration of the call.
+ * @data: SGP data
+ * @cmd: SGP Command to issue
+ * @measure_mode: SGP measurement mode
+ *
+ * Return: 0 on success, negative error otherwise.
+ */
+static int sgp_get_measurement(struct sgp_data *data, enum sgp_cmd cmd,
+ enum sgp_measure_mode measure_mode)
+{
+ int ret;
+
+ /* if all channels are measured, we don't need to distinguish between
+ * different measure modes
+ */
+ if (data->measure_mode == SGP_MEASURE_MODE_ALL)
+ measure_mode = SGP_MEASURE_MODE_ALL;
+
+ /* Always measure if measure mode changed
+ * SGP30 should only be polled once a second
+ * SGPC3 should only be polled once every two seconds
+ */
+ if (measure_mode == data->measure_mode &&
+ !time_after(jiffies,
+ data->last_update + data->measure_interval_hz * HZ)) {
+ return 0;
+ }
+
+ ret = sgp_read_from_cmd(data, cmd, data->measurement_len,
+ SGP_CMD_DURATION_US);
+
+ if (ret < 0)
+ return ret;
+
+ data->measure_mode = measure_mode;
+ data->last_update = jiffies;
+
+ return 0;
+}
+
+static int sgp_absolute_humidity_store(struct sgp_data *data,
+ int val, int val2)
+{
+ u32 ah;
+ u16 ah_scaled;
+
+ if (val < 0 || val > 256 || (val == 256 && val2 > 0))
+ return -EINVAL;
+
+ ah = val * 1000 + val2 / 1000;
+ /* ah_scaled = (u16)((ah / 1000.0) * 256.0) */
+ ah_scaled = (u16)(((u64)ah * 256 * 16777) >> 24);
+
+ /* ensure we don't disable AH compensation due to rounding */
+ if (ah > 0 && ah_scaled == 0)
+ ah_scaled = 1;
+
+ return sgp_write_from_cmd(data, SGP30_CMD_SET_ABSOLUTE_HUMIDITY,
+ &ah_scaled, 1, SGP_CMD_HANDLING_DURATION_US);
+}
+
+static int sgp_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int *val,
+ int *val2, long mask)
+{
+ struct sgp_data *data = iio_priv(indio_dev);
+ struct sgp_crc_word *words;
+ int ret;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_PROCESSED:
+ mutex_lock(&data->data_lock);
+ if (!data->iaq_initialized) {
+ dev_warn(&data->client->dev,
+ "IAQ potentially uninitialized\n");
+ }
+ ret = sgp_get_measurement(data, data->measure_iaq_cmd,
+ SGP_MEASURE_MODE_IAQ);
+ if (ret)
+ goto unlock_fail;
+ words = data->buffer.raw_words;
+ switch (chan->address) {
+ case SGP30_IAQ_TVOC_IDX:
+ case SGPC3_IAQ_TVOC_IDX:
+ *val = 0;
+ *val2 = be16_to_cpu(words[1].value);
+ ret = IIO_VAL_INT_PLUS_NANO;
+ break;
+ case SGP30_IAQ_CO2EQ_IDX:
+ *val = 0;
+ *val2 = be16_to_cpu(words[0].value);
+ ret = IIO_VAL_INT_PLUS_MICRO;
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ mutex_unlock(&data->data_lock);
+ break;
+ case IIO_CHAN_INFO_RAW:
+ mutex_lock(&data->data_lock);
+ ret = sgp_get_measurement(data, data->measure_signal_cmd,
+ SGP_MEASURE_MODE_SIGNAL);
+ if (ret)
+ goto unlock_fail;
+ words = data->buffer.raw_words;
+ switch (chan->address) {
+ case SGP30_SIG_ETOH_IDX:
+ *val = be16_to_cpu(words[1].value);
+ ret = IIO_VAL_INT;
+ break;
+ case SGPC3_SIG_ETOH_IDX:
+ case SGP30_SIG_H2_IDX:
+ *val = be16_to_cpu(words[0].value);
+ ret = IIO_VAL_INT;
+ break;
+ }
+unlock_fail:
+ mutex_unlock(&data->data_lock);
+ break;
+ case IIO_CHAN_INFO_SCALE:
+ switch (chan->address) {
+ case SGP30_SIG_ETOH_IDX:
+ case SGPC3_SIG_ETOH_IDX:
+ case SGP30_SIG_H2_IDX:
+ *val = 0;
+ *val2 = 1953125;
+ ret = IIO_VAL_INT_PLUS_NANO;
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ break;
+ default:
+ ret = -EINVAL;
+ }
+ return ret;
+}
+
+static int sgp_write_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int val, int val2, long mask)
+{
+ struct sgp_data *data = iio_priv(indio_dev);
+ int ret;
+
+ switch (chan->address) {
+ case SGP30_SET_AH_IDX:
+ ret = sgp_absolute_humidity_store(data, val, val2);
+ break;
+ default:
+ ret = -EINVAL;
+ }
+
+ return ret;
+}
+
+static ssize_t sgp_iaq_init_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct sgp_data *data = iio_priv(dev_to_iio_dev(dev));
+ u32 init_time;
+ enum sgp_cmd cmd;
+ int ret;
+
+ cmd = SGP_CMD_IAQ_INIT;
+ if (data->chip_id == SGPC3) {
+ ret = kstrtou32(buf, 10, &init_time);
+
+ if (ret)
+ return -EINVAL;
+
+ switch (init_time) {
+ case 0:
+ cmd = SGPC3_CMD_IAQ_INIT0;
+ break;
+ case 16:
+ cmd = SGPC3_CMD_IAQ_INIT16;
+ break;
+ case 64:
+ cmd = SGPC3_CMD_IAQ_INIT64;
+ break;
+ case 184:
+ cmd = SGPC3_CMD_IAQ_INIT184;
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+
+ mutex_lock(&data->data_lock);
+ ret = sgp_read_from_cmd(data, cmd, 0, SGP_CMD_DURATION_US);
+
+ if (ret < 0)
+ goto unlock_fail;
+
+ data->iaq_initialized = true;
+ mutex_unlock(&data->data_lock);
+ return count;
+
+unlock_fail:
+ mutex_unlock(&data->data_lock);
+ return ret;
+}
+
+static ssize_t sgp_iaq_baseline_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct sgp_data *data = iio_priv(dev_to_iio_dev(dev));
+ u32 baseline;
+ u16 baseline_word;
+ int ret, ix;
+
+ mutex_lock(&data->data_lock);
+ ret = sgp_read_from_cmd(data, SGP_CMD_GET_BASELINE, data->baseline_len,
+ SGP_CMD_DURATION_US);
+
+ if (ret < 0)
+ goto unlock_fail;
+
+ baseline = 0;
+ for (ix = 0; ix < data->baseline_len; ix++) {
+ baseline_word = be16_to_cpu(data->buffer.raw_words[ix].value);
+ baseline |= baseline_word << (16 * ix);
+ }
+
+ mutex_unlock(&data->data_lock);
+ return sprintf(buf, data->baseline_format, baseline);
+
+unlock_fail:
+ mutex_unlock(&data->data_lock);
+ return ret;
+}
+
+static ssize_t sgp_iaq_baseline_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct sgp_data *data = iio_priv(dev_to_iio_dev(dev));
+ int newline = (count > 0 && buf[count - 1] == '\n');
+ u16 words[2];
+ int ret = 0;
+
+ /* 1 word (4 chars) per signal */
+ if (count - newline == (data->baseline_len * 4)) {
+ if (data->baseline_len == 1)
+ ret = sscanf(buf, "%04hx", &words[0]);
+ else if (data->baseline_len == 2)
+ ret = sscanf(buf, "%04hx%04hx", &words[0], &words[1]);
+ else
+ return -EIO;
+ }
+
+ /* Check if baseline format is correct */
+ if (ret != data->baseline_len) {
+ dev_err(&data->client->dev, "invalid baseline format\n");
+ return -EIO;
+ }
+
+ ret = sgp_write_from_cmd(data, SGP_CMD_SET_BASELINE, words,
+ data->baseline_len, SGP_CMD_DURATION_US);
+ if (ret < 0)
+ return -EIO;
+
+ return count;
+}
+
+static ssize_t sgp_selftest_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct sgp_data *data = iio_priv(dev_to_iio_dev(dev));
+ u16 measure_test;
+ int ret;
+
+ mutex_lock(&data->data_lock);
+ data->iaq_initialized = false;
+ ret = sgp_read_from_cmd(data, SGP_CMD_MEASURE_TEST, 1,
+ SGP_SELFTEST_DURATION_US);
+
+ if (ret != 0)
+ goto unlock_fail;
+
+ measure_test = be16_to_cpu(data->buffer.raw_words[0].value);
+ mutex_unlock(&data->data_lock);
+
+ return sprintf(buf, "%s\n",
+ measure_test ^ SGP_SELFTEST_OK ? "FAILED" : "OK");
+
+unlock_fail:
+ mutex_unlock(&data->data_lock);
+ return ret;
+}
+
+static ssize_t sgp_serial_id_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct sgp_data *data = iio_priv(dev_to_iio_dev(dev));
+
+ return sprintf(buf, "%llu\n", data->serial_id);
+}
+
+static ssize_t sgp_feature_set_version_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct sgp_data *data = iio_priv(dev_to_iio_dev(dev));
+
+ return sprintf(buf, "%hu.%hu\n", (data->feature_set & 0x00e0) >> 5,
+ data->feature_set & 0x001f);
+}
+
+static int sgp_get_serial_id(struct sgp_data *data)
+{
+ int ret;
+ struct sgp_crc_word *words;
+
+ mutex_lock(&data->data_lock);
+ ret = sgp_read_from_cmd(data, SGP_CMD_GET_SERIAL_ID, 3,
+ SGP_CMD_DURATION_US);
+ if (ret != 0)
+ goto unlock_fail;
+
+ words = data->buffer.raw_words;
+ data->serial_id = (u64)(be16_to_cpu(words[2].value) & 0xffff) |
+ (u64)(be16_to_cpu(words[1].value) & 0xffff) << 16 |
+ (u64)(be16_to_cpu(words[0].value) & 0xffff) << 32;
+
+unlock_fail:
+ mutex_unlock(&data->data_lock);
+ return ret;
+}
+
+static int setup_and_check_sgp_data(struct sgp_data *data,
+ unsigned int chip_id)
+{
+ u16 minor, major, product, eng, ix, num_fs, reserved;
+ struct sgp_version *supported_versions;
+
+ product = (data->feature_set & 0xf000) >> 12;
+ reserved = (data->feature_set & 0x0e00) >> 9;
+ eng = (data->feature_set & 0x0100) >> 8;
+ major = (data->feature_set & 0x00e0) >> 5;
+ minor = data->feature_set & 0x001f;
+
+ /* driver does not match product */
+ if (product != chip_id) {
+ dev_err(&data->client->dev,
+ "sensor reports a different product: 0x%04hx\n",
+ product);
+ return -ENODEV;
+ }
+
+ if (reserved != 0)
+ dev_warn(&data->client->dev, "reserved bits set: 0x%04hx\n",
+ reserved);
+
+ /* engineering samples are not supported */
+ if (eng != 0)
+ return -ENODEV;
+
+ data->iaq_initialized = false;
+ switch (product) {
+ case SGP30:
+ supported_versions =
+ (struct sgp_version *)supported_versions_sgp30;
+ num_fs = ARRAY_SIZE(supported_versions_sgp30);
+ data->measurement_len = SGP30_MEASUREMENT_LEN;
+ data->measure_interval_hz = SGP30_MEASURE_INTERVAL_HZ;
+ data->measure_iaq_cmd = SGP_CMD_IAQ_MEASURE;
+ data->measure_signal_cmd = SGP30_CMD_MEASURE_SIGNAL;
+ data->chip_id = SGP30;
+ data->baseline_len = 2;
+ data->baseline_format = "%08x\n";
+ data->measure_mode = SGP_MEASURE_MODE_UNKNOWN;
+ break;
+ case SGPC3:
+ supported_versions =
+ (struct sgp_version *)supported_versions_sgpc3;
+ num_fs = ARRAY_SIZE(supported_versions_sgpc3);
+ data->measurement_len = SGPC3_MEASUREMENT_LEN;
+ data->measure_interval_hz = SGPC3_MEASURE_INTERVAL_HZ;
+ data->measure_iaq_cmd = SGPC3_CMD_MEASURE_RAW;
+ data->measure_signal_cmd = SGPC3_CMD_MEASURE_RAW;
+ data->chip_id = SGPC3;
+ data->baseline_len = 1;
+ data->baseline_format = "%04x\n";
+ data->measure_mode = SGP_MEASURE_MODE_ALL;
+ break;
+ default:
+ return -ENODEV;
+ };
+
+ for (ix = 0; ix < num_fs; ix++) {
+ if (supported_versions[ix].major == major &&
+ minor >= supported_versions[ix].minor)
+ return 0;
+ }
+
+ dev_err(&data->client->dev, "unsupported sgp version: %d.%d\n",
+ major, minor);
+ return -ENODEV;
+}
+
+static IIO_DEVICE_ATTR(in_serial_id, 0444, sgp_serial_id_show, NULL, 0);
+static IIO_DEVICE_ATTR(in_feature_set_version, 0444,
+ sgp_feature_set_version_show, NULL, 0);
+static IIO_DEVICE_ATTR(in_selftest, 0444, sgp_selftest_show, NULL, 0);
+static IIO_DEVICE_ATTR(out_iaq_init, 0220, NULL, sgp_iaq_init_store, 0);
+static IIO_DEVICE_ATTR(in_iaq_baseline, 0444, sgp_iaq_baseline_show, NULL, 0);
+static IIO_DEVICE_ATTR(out_iaq_baseline, 0220, NULL, sgp_iaq_baseline_store, 0);
+
+static struct attribute *sgp_attributes[] = {
+ &iio_dev_attr_in_serial_id.dev_attr.attr,
+ &iio_dev_attr_in_feature_set_version.dev_attr.attr,
+ &iio_dev_attr_in_selftest.dev_attr.attr,
+ &iio_dev_attr_out_iaq_init.dev_attr.attr,
+ &iio_dev_attr_in_iaq_baseline.dev_attr.attr,
+ &iio_dev_attr_out_iaq_baseline.dev_attr.attr,
+ NULL
+};
+
+static const struct attribute_group sgp_attr_group = {
+ .attrs = sgp_attributes,
+};
+
+static const struct iio_info sgp_info = {
+ .attrs = &sgp_attr_group,
+ .read_raw = sgp_read_raw,
+ .write_raw = sgp_write_raw,
+};
+
+static const struct of_device_id sgp_dt_ids[] = {
+ { .compatible = "sensirion,sgp30", .data = (void *)SGP30 },
+ { .compatible = "sensirion,sgpc3", .data = (void *)SGPC3 },
+ { }
+};
+
+static int sgp_probe(struct i2c_client *client,
+ const struct i2c_device_id *id)
+{
+ struct iio_dev *indio_dev;
+ struct sgp_data *data;
+ struct sgp_device *chip;
+ const struct of_device_id *of_id;
+ unsigned long chip_id;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ of_id = of_match_device(sgp_dt_ids, &client->dev);
+ if (!of_id)
+ chip_id = id->driver_data;
+ else
+ chip_id = (unsigned long)of_id->data;
+
+ chip = &sgp_devices[chip_id];
+ data = iio_priv(indio_dev);
+ i2c_set_clientdata(client, indio_dev);
+ data->client = client;
+ crc8_populate_msb(sgp_crc8_table, SGP_CRC8_POLYNOMIAL);
+ mutex_init(&data->data_lock);
+ mutex_init(&data->i2c_lock);
+
+ /* get serial id and write it to client data */
+ ret = sgp_get_serial_id(data);
+
+ if (ret != 0)
+ return ret;
+
+ /* get feature set version and write it to client data */
+ ret = sgp_read_from_cmd(data, SGP_CMD_GET_FEATURE_SET, 1,
+ SGP_CMD_DURATION_US);
+ if (ret != 0)
+ return ret;
+
+ data->feature_set = be16_to_cpu(data->buffer.raw_words[0].value);
+
+ ret = setup_and_check_sgp_data(data, chip_id);
+ if (ret < 0)
+ goto fail_free;
+
+ /* so initial reading will complete */
+ data->last_update = jiffies - data->measure_interval_hz * HZ;
+
+ indio_dev->dev.parent = &client->dev;
+ indio_dev->info = &sgp_info;
+ indio_dev->name = dev_name(&client->dev);
+ indio_dev->modes = INDIO_BUFFER_SOFTWARE | INDIO_DIRECT_MODE;
+
+ indio_dev->channels = chip->channels;
+ indio_dev->num_channels = chip->num_channels;
+
+ ret = devm_iio_device_register(&client->dev, indio_dev);
+ if (!ret)
+ return ret;
+
+ dev_err(&client->dev, "failed to register iio device\n");
+
+fail_free:
+ mutex_destroy(&data->i2c_lock);
+ mutex_destroy(&data->data_lock);
+ iio_device_free(indio_dev);
+ return ret;
+}
+
+static int sgp_remove(struct i2c_client *client)
+{
+ struct iio_dev *indio_dev = i2c_get_clientdata(client);
+
+ devm_iio_device_unregister(&client->dev, indio_dev);
+ return 0;
+}
+
+static const struct i2c_device_id sgp_id[] = {
+ { "sgp30", SGP30 },
+ { "sgpc3", SGPC3 },
+ { }
+};
+
+MODULE_DEVICE_TABLE(i2c, sgp_id);
+MODULE_DEVICE_TABLE(of, sgp_dt_ids);
+
+static struct i2c_driver sgp_driver = {
+ .driver = {
+ .name = "sgpxx",
+ .of_match_table = of_match_ptr(sgp_dt_ids),
+ },
+ .probe = sgp_probe,
+ .remove = sgp_remove,
+ .id_table = sgp_id,
+};
+module_i2c_driver(sgp_driver);
+
+MODULE_AUTHOR("Andreas Brauchli <andreas.brauchli@xxxxxxxxxxxxx>");
+MODULE_AUTHOR("Pascal Sachs <pascal.sachs@xxxxxxxxxxxxx>");
+MODULE_DESCRIPTION("Sensirion SGPxx gas sensors");
+MODULE_LICENSE("GPL v2");
+MODULE_VERSION("0.5.0");
--
2.14.1