Author | Tokens | Token Proportion | Commits | Commit Proportion |
---|---|---|---|---|
Andreas Brauchli | 2321 | 98.56% | 1 | 20.00% |
Jonathan Cameron | 33 | 1.40% | 3 | 60.00% |
Lee Jones | 1 | 0.04% | 1 | 20.00% |
Total | 2355 | 5 |
// SPDX-License-Identifier: GPL-2.0 /* * sgp30.c - Support for Sensirion SGP Gas Sensors * * Copyright (C) 2018 Andreas Brauchli <andreas.brauchli@sensirion.com> * * I2C slave address: 0x58 * * Datasheets: * https://www.sensirion.com/file/datasheet_sgp30 * https://www.sensirion.com/file/datasheet_sgpc3 * * TODO: * - baseline support * - humidity compensation * - power mode switching (SGPC3) */ #include <linux/crc8.h> #include <linux/delay.h> #include <linux/kthread.h> #include <linux/module.h> #include <linux/mod_devicetable.h> #include <linux/mutex.h> #include <linux/i2c.h> #include <linux/iio/iio.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 12000 #define SGP_MEASUREMENT_DURATION_US 50000 #define SGP_CMD_LEN SGP_WORD_LEN #define SGP_CMD_MAX_BUF_SIZE (SGP_CMD_LEN + 2 * SGP_WORD_LEN) #define SGP_MEASUREMENT_LEN 2 #define SGP30_MEASURE_INTERVAL_HZ 1 #define SGPC3_MEASURE_INTERVAL_HZ 2 #define SGP_VERS_PRODUCT(data) ((((data)->feature_set) & 0xf000) >> 12) #define SGP_VERS_RESERVED(data) ((((data)->feature_set) & 0x0800) >> 11) #define SGP_VERS_GEN(data) ((((data)->feature_set) & 0x0600) >> 9) #define SGP_VERS_ENG_BIT(data) ((((data)->feature_set) & 0x0100) >> 8) #define SGP_VERS_MAJOR(data) ((((data)->feature_set) & 0x00e0) >> 5) #define SGP_VERS_MINOR(data) (((data)->feature_set) & 0x001f) 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, }; 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_FEATURE_SET = SGP_CMD(0x202f), SGP_CMD_GET_SERIAL_ID = SGP_CMD(0x3682), SGP30_CMD_MEASURE_SIGNAL = SGP_CMD(0x2050), SGPC3_CMD_MEASURE_RAW = SGP_CMD(0x2046), }; 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]; }; enum _iaq_buffer_state { IAQ_BUFFER_EMPTY = 0, IAQ_BUFFER_DEFAULT_VALS, IAQ_BUFFER_VALID, }; struct sgp_data { struct i2c_client *client; struct task_struct *iaq_thread; struct mutex data_lock; unsigned long iaq_init_start_jiffies; unsigned long iaq_defval_skip_jiffies; u16 product_id; u16 feature_set; unsigned long measure_interval_jiffies; enum sgp_cmd iaq_init_cmd; enum sgp_cmd measure_iaq_cmd; enum sgp_cmd measure_gas_signals_cmd; union sgp_reading buffer; union sgp_reading iaq_buffer; enum _iaq_buffer_state iaq_buffer_state; }; 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, .modified = 1, .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), .address = SGP30_IAQ_TVOC_IDX, }, { .type = IIO_CONCENTRATION, .channel2 = IIO_MOD_CO2, .modified = 1, .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), .address = SGP30_IAQ_CO2EQ_IDX, }, { .type = IIO_CONCENTRATION, .channel2 = IIO_MOD_ETHANOL, .modified = 1, .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), .address = SGP30_SIG_ETOH_IDX, }, { .type = IIO_CONCENTRATION, .channel2 = IIO_MOD_H2, .modified = 1, .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), .address = SGP30_SIG_H2_IDX, }, }; static const struct iio_chan_spec sgpc3_channels[] = { { .type = IIO_CONCENTRATION, .channel2 = IIO_MOD_VOC, .modified = 1, .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), .address = SGPC3_IAQ_TVOC_IDX, }, { .type = IIO_CONCENTRATION, .channel2 = IIO_MOD_ETHANOL, .modified = 1, .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), .address = SGPC3_SIG_ETOH_IDX, }, }; static const 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 * @buf: Raw data buffer * @word_count: Num data words stored in the buffer, excluding CRC bytes * * Return: 0 on success, negative error otherwise. */ static int sgp_verify_buffer(const struct sgp_data *data, union sgp_reading *buf, size_t word_count) { size_t size = word_count * (SGP_WORD_LEN + SGP_CRC8_LEN); int i; u8 crc; u8 *data_buf = &buf->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_cmd() - reads data from 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 * @buf: Raw data buffer to use * @word_count: Num words to read, excluding CRC bytes * @duration_us: Time taken to sensor to take a reading and data to be ready. * * Return: 0 on success, negative error otherwise. */ static int sgp_read_cmd(struct sgp_data *data, enum sgp_cmd cmd, union sgp_reading *buf, 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; ret = i2c_master_send(client, (const char *)&cmd, SGP_CMD_LEN); if (ret != SGP_CMD_LEN) return -EIO; usleep_range(duration_us, duration_us + 1000); if (word_count == 0) return 0; data_buf = &buf->start; ret = i2c_master_recv(client, data_buf, size); if (ret < 0) return ret; if (ret != size) return -EIO; return sgp_verify_buffer(data, buf, word_count); } /** * sgp_measure_iaq() - measure and retrieve IAQ values from sensor * The caller must hold data->data_lock for the duration of the call. * @data: SGP data * * Return: 0 on success, -EBUSY on default values, negative error * otherwise. */ static int sgp_measure_iaq(struct sgp_data *data) { int ret; /* data contains default values */ bool default_vals = !time_after(jiffies, data->iaq_init_start_jiffies + data->iaq_defval_skip_jiffies); ret = sgp_read_cmd(data, data->measure_iaq_cmd, &data->iaq_buffer, SGP_MEASUREMENT_LEN, SGP_MEASUREMENT_DURATION_US); if (ret < 0) return ret; data->iaq_buffer_state = IAQ_BUFFER_DEFAULT_VALS; if (default_vals) return -EBUSY; data->iaq_buffer_state = IAQ_BUFFER_VALID; return 0; } static void sgp_iaq_thread_sleep_until(const struct sgp_data *data, unsigned long sleep_jiffies) { const long IAQ_POLL = 50000; while (!time_after(jiffies, sleep_jiffies)) { usleep_range(IAQ_POLL, IAQ_POLL + 10000); if (kthread_should_stop() || data->iaq_init_start_jiffies == 0) return; } } static int sgp_iaq_threadfn(void *p) { struct sgp_data *data = (struct sgp_data *)p; unsigned long next_update_jiffies; int ret; while (!kthread_should_stop()) { mutex_lock(&data->data_lock); if (data->iaq_init_start_jiffies == 0) { ret = sgp_read_cmd(data, data->iaq_init_cmd, NULL, 0, SGP_CMD_DURATION_US); if (ret < 0) goto unlock_sleep_continue; data->iaq_init_start_jiffies = jiffies; } ret = sgp_measure_iaq(data); if (ret && ret != -EBUSY) { dev_warn(&data->client->dev, "IAQ measurement error [%d]\n", ret); } unlock_sleep_continue: next_update_jiffies = jiffies + data->measure_interval_jiffies; mutex_unlock(&data->data_lock); sgp_iaq_thread_sleep_until(data, next_update_jiffies); } return 0; } 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_buffer_state != IAQ_BUFFER_VALID) { mutex_unlock(&data->data_lock); return -EBUSY; } words = data->iaq_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); if (chan->address == SGPC3_SIG_ETOH_IDX) { if (data->iaq_buffer_state == IAQ_BUFFER_EMPTY) ret = -EBUSY; else ret = 0; words = data->iaq_buffer.raw_words; } else { ret = sgp_read_cmd(data, data->measure_gas_signals_cmd, &data->buffer, SGP_MEASUREMENT_LEN, SGP_MEASUREMENT_DURATION_US); words = data->buffer.raw_words; } if (ret) { mutex_unlock(&data->data_lock); return ret; } 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; default: ret = -EINVAL; break; } mutex_unlock(&data->data_lock); break; default: return -EINVAL; } return ret; } static int sgp_check_compat(struct sgp_data *data, unsigned int product_id) { struct device *dev = &data->client->dev; const struct sgp_version *supported_versions; u16 ix, num_fs; u16 product, generation, major, minor; /* driver does not match product */ generation = SGP_VERS_GEN(data); if (generation != 0) { dev_err(dev, "incompatible product generation %d != 0", generation); return -ENODEV; } product = SGP_VERS_PRODUCT(data); if (product != product_id) { dev_err(dev, "sensor reports a different product: 0x%04x\n", product); return -ENODEV; } if (SGP_VERS_RESERVED(data)) dev_warn(dev, "reserved bit is set\n"); /* engineering samples are not supported: no interface guarantees */ if (SGP_VERS_ENG_BIT(data)) return -ENODEV; switch (product) { case SGP30: supported_versions = supported_versions_sgp30; num_fs = ARRAY_SIZE(supported_versions_sgp30); break; case SGPC3: supported_versions = supported_versions_sgpc3; num_fs = ARRAY_SIZE(supported_versions_sgpc3); break; default: return -ENODEV; } major = SGP_VERS_MAJOR(data); minor = SGP_VERS_MINOR(data); for (ix = 0; ix < num_fs; ix++) { if (major == supported_versions[ix].major && minor >= supported_versions[ix].minor) return 0; } dev_err(dev, "unsupported sgp version: %d.%d\n", major, minor); return -ENODEV; } static void sgp_init(struct sgp_data *data) { data->iaq_init_cmd = SGP_CMD_IAQ_INIT; data->iaq_init_start_jiffies = 0; data->iaq_buffer_state = IAQ_BUFFER_EMPTY; switch (SGP_VERS_PRODUCT(data)) { case SGP30: data->measure_interval_jiffies = SGP30_MEASURE_INTERVAL_HZ * HZ; data->measure_iaq_cmd = SGP_CMD_IAQ_MEASURE; data->measure_gas_signals_cmd = SGP30_CMD_MEASURE_SIGNAL; data->product_id = SGP30; data->iaq_defval_skip_jiffies = 15 * HZ; break; case SGPC3: data->measure_interval_jiffies = SGPC3_MEASURE_INTERVAL_HZ * HZ; data->measure_iaq_cmd = SGPC3_CMD_MEASURE_RAW; data->measure_gas_signals_cmd = SGPC3_CMD_MEASURE_RAW; data->product_id = SGPC3; data->iaq_defval_skip_jiffies = 43 * data->measure_interval_jiffies; break; } } static const struct iio_info sgp_info = { .read_raw = sgp_read_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 device *dev = &client->dev; struct iio_dev *indio_dev; struct sgp_data *data; unsigned long product_id; int ret; indio_dev = devm_iio_device_alloc(dev, sizeof(*data)); if (!indio_dev) return -ENOMEM; if (dev_fwnode(dev)) product_id = (unsigned long)device_get_match_data(dev); else product_id = id->driver_data; 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); /* get feature set version and write it to client data */ ret = sgp_read_cmd(data, SGP_CMD_GET_FEATURE_SET, &data->buffer, 1, SGP_CMD_DURATION_US); if (ret < 0) return ret; data->feature_set = be16_to_cpu(data->buffer.raw_words[0].value); ret = sgp_check_compat(data, product_id); if (ret) return ret; indio_dev->info = &sgp_info; indio_dev->name = id->name; indio_dev->modes = INDIO_DIRECT_MODE; indio_dev->channels = sgp_devices[product_id].channels; indio_dev->num_channels = sgp_devices[product_id].num_channels; sgp_init(data); ret = devm_iio_device_register(dev, indio_dev); if (ret) { dev_err(dev, "failed to register iio device\n"); return ret; } data->iaq_thread = kthread_run(sgp_iaq_threadfn, data, "%s-iaq", data->client->name); return 0; } static int sgp_remove(struct i2c_client *client) { struct iio_dev *indio_dev = i2c_get_clientdata(client); struct sgp_data *data = iio_priv(indio_dev); if (data->iaq_thread) kthread_stop(data->iaq_thread); 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 = "sgp30", .of_match_table = 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 SGP gas sensors"); MODULE_LICENSE("GPL v2");
Information contained on this website is for historical information purposes only and does not indicate or represent copyright ownership.
Created with Cregit http://github.com/cregit/cregit
Version 2.0-RC1