From: Andreas Brauchli <a.brauchli@elementarea.net>
To: Jonathan Cameron <jic23@kernel.org>,
Hartmut Knaack <knaack.h@gmx.de>,
Lars-Peter Clausen <lars@metafoo.de>,
Peter Meerwald-Stadler <pmeerw@pmeerw.net>
Cc: linux-iio@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH 1/2] iio: chemical: sgpxx: Support Sensirion SGPxx sensors
Date: Tue, 21 Nov 2017 17:11:25 +0100 [thread overview]
Message-ID: <1511280685.12439.35.camel@elementarea.net> (raw)
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@sensirion.com>
---
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@sensirion.com>
+ *
+ * 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@sensirion.com>");
+MODULE_AUTHOR("Pascal Sachs <pascal.sachs@sensirion.com>");
+MODULE_DESCRIPTION("Sensirion SGPxx gas sensors");
+MODULE_LICENSE("GPL v2");
+MODULE_VERSION("0.5.0");
--
2.14.1
next reply other threads:[~2017-11-21 16:11 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2017-11-21 16:11 Andreas Brauchli [this message]
2017-11-21 21:46 ` [PATCH 1/2] iio: chemical: sgpxx: Support Sensirion SGPxx sensors Peter Meerwald-Stadler
2017-11-25 17:41 ` Jonathan Cameron
2018-03-10 22:02 ` Andreas Brauchli
2018-03-11 12:14 ` 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=1511280685.12439.35.camel@elementarea.net \
--to=a.brauchli@elementarea.net \
--cc=jic23@kernel.org \
--cc=knaack.h@gmx.de \
--cc=lars@metafoo.de \
--cc=linux-iio@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=pmeerw@pmeerw.net \
/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.