From patchwork Tue Nov 21 16:11:25 2017 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Andreas Brauchli X-Patchwork-Id: 10068571 Return-Path: Received: from mail.wl.linuxfoundation.org (pdx-wl-mail.web.codeaurora.org [172.30.200.125]) by pdx-korg-patchwork.web.codeaurora.org (Postfix) with ESMTP id EE1FB6022E for ; Tue, 21 Nov 2017 16:12:03 +0000 (UTC) Received: from mail.wl.linuxfoundation.org (localhost [127.0.0.1]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id DCE2029820 for ; Tue, 21 Nov 2017 16:12:03 +0000 (UTC) Received: by mail.wl.linuxfoundation.org (Postfix, from userid 486) id D18D42982E; Tue, 21 Nov 2017 16:12:03 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on pdx-wl-mail.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-6.8 required=2.0 tests=BAYES_00,DKIM_SIGNED, RCVD_IN_DNSWL_HI,T_DKIM_INVALID autolearn=unavailable version=3.3.1 Received: from vger.kernel.org (vger.kernel.org [209.132.180.67]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id 4B05C2982A for ; Tue, 21 Nov 2017 16:12:02 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1751310AbdKUQLc (ORCPT ); Tue, 21 Nov 2017 11:11:32 -0500 Received: from mail-wm0-f41.google.com ([74.125.82.41]:41628 "EHLO mail-wm0-f41.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1751187AbdKUQL2 (ORCPT ); Tue, 21 Nov 2017 11:11:28 -0500 Received: by mail-wm0-f41.google.com with SMTP id b189so4531681wmd.0 for ; Tue, 21 Nov 2017 08:11:27 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=elementarea-net.20150623.gappssmtp.com; s=20150623; h=message-id:subject:from:to:cc:date:mime-version :content-transfer-encoding; bh=6Fagms894OOrGellU9H7TOE9wIh0nwGxYdhr6hhlwMg=; b=cl+vnGk3JYLtsDCMFoTrHsH7m/N9lL1WltvVSxw7LtFEE/leOVLqdIaMQ7x8iH9tpe 5jKvYcWesOkUxXMAzL660pmJvHT+caQIOHbRY0IRzP6qKR8si8NzlmujUDemVwl7/ggW JjH4MEqJfJzxN2g2ORd30KVexjY2zSsZF/2f5G0dVt5WaKZzKaK15cn59jLZA3/LjdNo lSTTPMDJa15R6BolQvmq5M1Wp0zOSCelHxjChCiTZkssfRIutRlahhQJz2gnKCCSqTRs 3qL/JYziN/U/0jle1iAto330nr4lR2NsNYnjlskMBtDh5OUlcLaQ0PDQ1JoPl2H1F6VN eviQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:message-id:subject:from:to:cc:date:mime-version :content-transfer-encoding; bh=6Fagms894OOrGellU9H7TOE9wIh0nwGxYdhr6hhlwMg=; b=d2+JgyLr56xHwpNhZdvUuX1wIv7KSmSGL0Ku2lty4tTnEOStuNNtPLqB8b2sBqNu80 Y0RjE9keD2oStlj6g9Hvf6rRQOOQ15SUXjlNENQVABHKlLBZfDpZEfPNLHL+4ieTEa9N T2PZBbtJ42eYTtrp+hoZ5178mQuZoH8aG8pkjvNM5YamkKXfvT3SyZNBg0v47Bm1rZ1z b1N6sioBZ0GEaSs8oF0pgEWzJkC48hB81FZU67MO4n2W5wJfclwaQwnhrDMSp7GtNT5A te9xyKAzkeeTP3Br0U7hAKaZ2RK/ByHFqCZQqh37Ol1p5lovKrD4xXn5lFr2srE9XqE+ OL6g== X-Gm-Message-State: AJaThX56yZEm/0o2q8gZlX7pwT5GHVUtzgVZawzaKyVsOdYTorymfcE1 uP0p4wWBwit0e71EnQ0rQsxpRQ== X-Google-Smtp-Source: AGs4zMbFaCcgF/nRS+Gmey0ZXB1rR41zxj7HitO2aCsyo6tEf8QtGdO3ODgRS5NlUZRgehJORb9WSA== X-Received: by 10.80.212.133 with SMTP id s5mr25668044edi.286.1511280686443; Tue, 21 Nov 2017 08:11:26 -0800 (PST) Received: from ch-sta-l-li0012 (mail.sensirion.com. [212.243.229.186]) by smtp.gmail.com with ESMTPSA id o15sm2383338edk.25.2017.11.21.08.11.25 (version=TLS1_2 cipher=ECDHE-RSA-CHACHA20-POLY1305 bits=256/256); Tue, 21 Nov 2017 08:11:25 -0800 (PST) Message-ID: <1511280685.12439.35.camel@elementarea.net> Subject: [PATCH 1/2] iio: chemical: sgpxx: Support Sensirion SGPxx sensors From: Andreas Brauchli To: Jonathan Cameron , Hartmut Knaack , Lars-Peter Clausen , Peter Meerwald-Stadler Cc: linux-iio@vger.kernel.org, linux-kernel@vger.kernel.org Date: Tue, 21 Nov 2017 17:11:25 +0100 X-Mailer: Evolution 3.26.1-1 Mime-Version: 1.0 Sender: linux-iio-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: linux-iio@vger.kernel.org X-Virus-Scanned: ClamAV using ClamSMTP 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 --- 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 + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 "); +MODULE_AUTHOR("Pascal Sachs "); +MODULE_DESCRIPTION("Sensirion SGPxx gas sensors"); +MODULE_LICENSE("GPL v2"); +MODULE_VERSION("0.5.0");