Re: [PATCH v2] iio: chemical: ccs811: Add support for AMS CCS811 VOC sensor

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Wed, Jul 12, 2017 at 08:09:23PM +0800, jic23@xxxxxxxxxx wrote:
> On Wed, 12 Jul 2017 12:40:23 +0300
> Narcisa Ana Maria Vasile <narcisaanamaria12@xxxxxxxxx> wrote:
> 
> >Add support for CCS811 VOC sensor. This patch adds support
> >for reading current and voltage across the sensor and TVOC
> >and equivalent CO2 values.
> >
> >Scale and offset values have been computed according to datasheet:
> >    - For current: raw value is in microamps
> >        => 0.001 scale to convert to milliamps
> >    - For voltage: 1.65V = 1023, therefore 1650mV = 1023
> >        => 1650.0/1023 = 1.612903 scale to convert to millivolts
> >    - For eCO2: raw value range is from 400ppm to 8192ppm.
> >        => (val - 400) * (100 - 0) / (8192 - 400) + 0 =
> >           (val - 400) * 0.01283367 => offset: -400, scale = 0.012834
> >           to get a percentage value
> >    -For TVOC: raw value range is from 0ppb to 1187ppb.
> >        => (val - 0) * 100 / (1187 - 0) + 0 = val * 0.0842459 =>
> >           scale = 0.084246 for getting a percentage value
> >
> >Cc: Daniel Baluta <daniel.baluta@xxxxxxxxx>
> >Cc: Alison Schofield <amsfield22@xxxxxxxxx>
> >Signed-off-by: Narcisa Ana Maria Vasile <narcisaanamaria12@xxxxxxxxx>
> Good progress.  Now we are on to the little details for v3!
> 
 Thank you very much for the helpful feedback(and for your patience!)
 I have a few questions about the changes you suggested, wrote them
 inline.
> See inline.
> >---
> >Changes in v2:
> >    - Add scale and offset to obtain values expressed with correct
> >units.
> >    - RAW_READ register is not read anymore, since values for
> >current and
> >      voltage are read using ALG_RESULT_DATA register address.
> >Remove the
> >      check for I2C_SMBUS_WORD_DATA, since the corresponding function
> >      is not used anymore.
> >    - Define packed struct to hold data read from alg_result_data
> >register.
> >    - Add mutex to protect readings.
> >    - Add _remove function and set chip to IDLE inside it.
> >    - Group measurement reading and "data_ready" check inside new
> >function.
> >    - Introduce sleeps to wait for data to be ready.
> >    - Add ccs811 prefix to enum operation_mode and to STATUS macros.
> >    - Use ret instead of status variable to hold return values
> >from functions.
> >    - Use enum operation_mode instead of int as parameter to
> >set_drive_mode().
> >    - Drop temporary variable meas_mode.
> >    - Drop pointless comments.
> >    - Add macro for error bit in STATUS register.
> >    - Drop useless break.
> >    - Drop useless return.
> >    - Change format of multi-line comment.
> >    - Change word "SENSOR" to lowercase letters inside
> >MODULE_DESCRIPTION.
> >    - Remove empty comment line.
> >    - Introduce MIN and MAX values to ccs811_operation_mode to
> >detect invalid
> >      values.
> >    - Change style of braces.
> >    - Replace VOC with "volatile organic compounds" in
> >MODULE_DESCRIPTION.
> >    - Add datasheet link.
> >    - Check hardware version in probe
> >
> > drivers/iio/chemical/Kconfig  |   7 +
> > drivers/iio/chemical/Makefile |   1 +
> > drivers/iio/chemical/ccs811.c | 343
> >++++++++++++++++++++++++++++++++++++++++++
> > 3 files changed, 351 insertions(+)
> > create mode 100644 drivers/iio/chemical/ccs811.c
> >
> >diff --git a/drivers/iio/chemical/Kconfig
> >b/drivers/iio/chemical/Kconfig
> >index cea7f98..4d799b5 100644
> >--- a/drivers/iio/chemical/Kconfig
> >+++ b/drivers/iio/chemical/Kconfig
> >@@ -21,6 +21,13 @@ config ATLAS_PH_SENSOR
> > 	 To compile this driver as module, choose M here: the
> > 	 module will be called atlas-ph-sensor.
> >
> >+config CCS811
> >+	tristate "AMS CCS811 VOC sensor"
> >+	depends on I2C
> >+	help
> >+	  Say Y here to build I2C interface support for the AMS
> >+	  CCS811 VOC (Volatile Organic Compounds) sensor
> >+
> > config IAQCORE
> > 	tristate "AMS iAQ-Core VOC sensors"
> > 	depends on I2C
> >diff --git a/drivers/iio/chemical/Makefile
> >b/drivers/iio/chemical/Makefile
> >index b02202b..a629b29 100644
> >--- a/drivers/iio/chemical/Makefile
> >+++ b/drivers/iio/chemical/Makefile
> >@@ -4,5 +4,6 @@
> >
> > # When adding new entries keep the list in alphabetical order
> > obj-$(CONFIG_ATLAS_PH_SENSOR)	+= atlas-ph-sensor.o
> >+obj-$(CONFIG_CCS811)		+= ccs811.o
> > obj-$(CONFIG_IAQCORE)		+= ams-iaq-core.o
> > obj-$(CONFIG_VZ89X)		+= vz89x.o
> >diff --git a/drivers/iio/chemical/ccs811.c
> >b/drivers/iio/chemical/ccs811.c
> >new file mode 100644
> >index 0000000..d816daa
> >--- /dev/null
> >+++ b/drivers/iio/chemical/ccs811.c
> >@@ -0,0 +1,343 @@
> >+/*
> >+ * ccs811.c - Support for AMS CCS811 VOC Sensor
> >+ *
> >+ * Copyright (C) 2017 Narcisa Vasile <narcisaanamaria12@xxxxxxxxx>
> >+ *
> >+ * Datasheet:
> >ams.com/content/download/951091/2269479/CCS811_DS000459_3-00.pdf
> >+ *
> >+ * This program is free software; you can redistribute it and/or
> >modify
> >+ * it under the terms of the GNU General Public License version 2 as
> >+ * published by the Free Software Foundation.
> >+ *
> >+ * IIO driver for AMS CCS811 (I2C address 0x5A/0x5B set by ADDR
> >Low/High)
> >+ *
> >+ * TODO:
> >+ * 1. Make the drive mode selectable form userspace
> >+ * 2. Add support for interrupts
> 3. Read what the error is when it occurs and put something in the
> log at least.
> >+ */
> >+
> >+#include <linux/delay.h>
> >+#include <linux/i2c.h>
> >+#include <linux/iio/iio.h>
> >+#include <linux/module.h>
> >+
> >+#define CCS811_STATUS		0x00
> >+#define CCS811_MEAS_MODE	0x01
> >+#define CCS811_ALG_RESULT_DATA	0x02
> >+#define CCS811_RAW_DATA		0x03
> >+#define CCS811_HW_ID		0x20
> >+#define CCS811_HW_VERSION	0x21
> >+#define CCS811_ERR		0xE0
> >+
> >+/* Used to transition from boot to application mode */
> >+#define CCS811_APP_START	0xF4
> >+
> >+#define CCS811_VOLTAGE_MASK	0x3FF
> >+
> >+#define CCS811_STATUS_ERROR		BIT(0)
> >+#define CCS811_STATUS_DATA_READY	BIT(3)
> >+#define CCS811_STATUS_APP_VALID	BIT(4)
> >+#define CCS811_STATUS_FW_MODE		BIT(7)
> >+
> >+enum ccs811_operation_mode {
> >+	CCS811_MIN_MODE = -1,
> >+	CCS811_MODE_IDLE = 0,
> >+	CCS811_MODE_IAQ_SEC_1 = 1,
> >+	CCS811_MODE_IAQ_SEC_10 = 2,
> >+	CCS811_MODE_IAQ_SEC_60 = 3,
> >+	CCS811_MODE_RAW_DATA = 4,
> >+	CCS811_MAX_MODE
> >+};
> >+
> >+struct ccs811_reading {
> >+	__be16 co2;
> >+	__be16 voc;
> >+	u8 status;
> >+	u8 error;
> >+	__be16 resistance;
> >+} __attribute__((__packed__));
> >+
> >+struct ccs811_data {
> >+	struct i2c_client *client;
> >+	struct mutex lock; /* Protect readings */
> >+	struct ccs811_reading buffer;
> >+};
> >+
> >+static const struct iio_chan_spec ccs811_channels[] = {
> >+	{
> >+		.type = IIO_CURRENT,
> >+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
> >+				      BIT(IIO_CHAN_INFO_SCALE)
> >+	}, {
> >+		.type = IIO_VOLTAGE,
> >+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
> >+				      BIT(IIO_CHAN_INFO_SCALE)
> >+	}, {
> >+		.type = IIO_CONCENTRATION,
> >+		.channel2 = IIO_MOD_CO2,
> >+		.modified = 1,
> >+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
> >+				      BIT(IIO_CHAN_INFO_OFFSET) |
> >+				      BIT(IIO_CHAN_INFO_SCALE)
> >+	}, {
> >+		.type = IIO_CONCENTRATION,
> >+		.channel2 = IIO_MOD_VOC,
> >+		.modified = 1,
> >+		.info_mask_separate =  BIT(IIO_CHAN_INFO_RAW) |
> >+				       BIT(IIO_CHAN_INFO_SCALE)
> >+	},
> >+};
> >+
> >+static int ccs811_set_drive_mode(struct i2c_client *client,
> >+				 enum ccs811_operation_mode mode)
> >+{
> >+	int ret;
> >+
> >+	if (mode <= CCS811_MIN_MODE || mode >= CCS811_MAX_MODE)
> >+		return -EINVAL;
> I don't think you should ever need this given you are using an enum.
> Any code that writes a larger value should be picked up by the
> compiler or static checkers.
> 
> >+
> >+	ret = i2c_smbus_write_byte_data(client, CCS811_MEAS_MODE, (mode
> ><< 4));
> That shift 4 should be handled as a macro.  We don't want to know
> the shifts
> in the code itself - merely that something is done.
> 
> >+	if (ret < 0)
> >+		return ret;
> >+
> >+	ret = i2c_smbus_read_byte_data(client, CCS811_STATUS);
> >+	if (ret < 0)
> >+		return ret;
> >+
> >+	if (ret & CCS811_STATUS_ERROR)
> >+		return -EIO;
> You could define a function to wrap up the error checking.. Might be
> cleaner than having it inline.
> 
> I'm also not immediately seeing any where in the documents that says
> we should check the status after setting the measurement mode.
> If it is in there somewhere, a comment with reference would be good
> here.
  Indeed, it doesn't say in the documents to check after this operation.
  I'm thinking to drop this check here and remove function
  ccs811_set_drive_mode(). Instead, I will directly configure
  measurement mode inside ccs811_setup(), with one single
  i2c_smbus_write_byte_data(). This will also lead to removing the
  operation_mode enum and replace it with masks corresponding to the operation
  modes, that will be passed to write_byte_data directly.
  (e. g #define CCS811_MODE_IAQ_1SEC 0x10)

  But this makes me wonder, when should I check the status for errors?

> >+
> >+	return 0;
> >+}
> >+
> >+static int ccs811_switch_to_application_mode(struct i2c_client
> >*client)
> >+{
> I'd like a bit of documentation here.  What is application mode?
> 
 The CCS811 powers-up in boot mode. A setup write to CCS811_APP_START
 will transition the sensor to application mode.
 Should I write this in a comment above the function, or maybe write it
 in the commit message? Maybe rename the function something like
 "ccs811_start_sensor_application" ?
> >+	int ret;
> >+
> >+	ret = i2c_smbus_read_byte_data(client, CCS811_STATUS);
> >+	if (ret < 0)
> >+		return ret;
> >+
> >+	if (!(ret & CCS811_STATUS_APP_VALID))
> >+		return -EIO;
> This is a little clearer in naming than the FW_MODE below, but
> still might benefit from being described as a mask and a value
> pair.
> >+
> >+	ret = i2c_smbus_read_byte_data(client, CCS811_APP_START);
> >+	if (ret < 0)
> >+		return ret;
> A I read the datasheet this should be a single byte write of the
> address only.   Here you are reading back a byte of data. No idea
> what you get in that, but the datasheet definitely suggests you
> shouldn't be doing this.
> 
> This probably works but at the very least need documentation.
> I'd try and see if i2c_smbus_write_byte(client, CCS811_APP_START)
> does the job as well.  If it does, still worth a comment explaining
> what is going on as this is an 'odd' bit of device specific stuff.
> 
> 
  You are right, I should have used i2c_smbus_write_byte.
  In order to transition from boot mode to application mode,
  this setup write to 0xF4(APP_START) needs to be done. This means
  that a write with just this address: 0xF4 needs to be made.
  There's no data to be read from APP_START; this mailbox doesn't
  even have an underlying register, but reading also worked because it also
  allowed 0xF4 command to be written. However, it was _not_ the right way to do it.
> >+
> >+	ret = i2c_smbus_read_byte_data(client, CCS811_STATUS);
> >+	if (ret < 0)
> >+		return ret;
> >+
> >+	if (!(ret & CCS811_STATUS_FW_MODE))
> The naming here is confusing.  Either go with
> if ((ret & CCS811_STATUS_FW_MODE_MASK) !=
> CCS811_STATUS_FW_MODE_APPLICATION)
> where the both the mode mask and mode_application defines would be
> BIT(7).
> That would mean the code made it clear what was happening (which
> is always nicer than documenting something non obvious!)
> >+		return -EIO;
> >+
> >+	return 0;
> >+}
> >+
> >+static int ccs811_setup(struct i2c_client *client)
> >+{
> >+	int ret;
> >+
> >+	ret = ccs811_switch_to_application_mode(client);
> >+	if (ret < 0)
> >+		return ret;
> >+
> >+	ret = ccs811_set_drive_mode(client, CCS811_MODE_IAQ_SEC_1);
> >+	if (ret < 0)
> >+		return ret;
> >+
> >+	return 0;
> return ccs811_set_drive_mode(...)
> It returns 0 on success?
> >+}
> >+
> >+static int ccs811_get_measurement(struct ccs811_data *data)
> >+{
> >+	int ret, tries = 11;
> >+
> >+	/*
> >+	 * After 11 tries, more than a second has passed (11 * 100ms >
> >1s) after
> >+	 * the first checking of "data_ready" bit. As measurements are
> >+	 * made every second, data should be ready now. Error out if
> >it's not.
> >+	 */
> Comment should explicitly include the maximum expected time we'd
> have to wait.
> 
> >+	while (tries-- > 0) {
> >+		ret = i2c_smbus_read_byte_data(data->client, CCS811_STATUS);
> >+		if (ret < 0)
> >+			return ret;
> >+
> >+		if (ret & CCS811_STATUS_DATA_READY)
> >+			break;
> >+		msleep(100);
> >+	}
> >+	if (tries < 0)
> >+		return -EIO;
> >+
> >+	ret = i2c_smbus_read_i2c_block_data(data->client,
> >+					    CCS811_ALG_RESULT_DATA, 8,
> >+					    (char *)&data->buffer);
> >+
> >+	if (ret != 8 || (data->buffer.status & CCS811_STATUS_ERROR))
> >+		return -EIO;
> So what errors can occur.
> 1. Short read - only way that can happen without another error being
> reported is if length > I2C_SMBUS_BLOCK_MAX which it isn't. (In think
> - theoretically i2c drivers could modify the length part).
  Should I drop the short read check?
> 2. Negative error from the call.  This should always be passed on up
> so should not be replaced with -EIO.
  Right, I'll check for negative error.
> 3. Status error - fair enough with return -EIO.
  As with status checking after setting measurement mode,
  maybe I should also drop the status error checking here.

  So, just keep the negative ret checking?
> >+
> >+	return 0;
> >+}
> >+
> >+static int ccs811_read_raw(struct iio_dev *indio_dev,
> >+			   struct iio_chan_spec const *chan,
> >+			   int *val, int *val2, long mask)
> >+{
> >+	struct ccs811_data *data = iio_priv(indio_dev);
> >+	int ret;
> >+
> >+	switch (mask) {
> >+	case IIO_CHAN_INFO_RAW:
> >+		mutex_lock(&data->lock);
> >+		ret = ccs811_get_measurement(data);
> >+		mutex_unlock(&data->lock);
> I got the impression this lock was to protect the data?
> You are reading from it below without the lock held which
> means the protection isn't doing anything useful.  You need
> to hold the lock until you are done with that data buffer.
> 
> Note that sysfs reads are not serialized, so there could
> be several in flight at the same time.
> >+		if (ret < 0)
> >+			return ret;
> >+
> >+		switch (chan->type) {
> >+		case IIO_VOLTAGE:
> >+			*val = be16_to_cpu(data->buffer.resistance) &
> >+					   CCS811_VOLTAGE_MASK;
> >+			return IIO_VAL_INT;
> >+		case IIO_CURRENT:
> >+			*val = be16_to_cpu(data->buffer.resistance) >> 10;
> >+			return IIO_VAL_INT;
> >+		case IIO_CONCENTRATION:
> >+			switch (chan->channel2) {
> >+			case IIO_MOD_CO2:
> >+				*val = be16_to_cpu(data->buffer.co2);
> >+				return IIO_VAL_INT;
> >+			case IIO_MOD_VOC:
> >+				*val = be16_to_cpu(data->buffer.voc);
> >+				return IIO_VAL_INT;
> >+			default:
> >+				return -EINVAL;
> >+			}
> >+		default:
> >+			return -EINVAL;
> >+		}
> >+	case IIO_CHAN_INFO_SCALE:
> >+		switch (chan->type) {
> >+		case IIO_VOLTAGE:
> >+			*val = 1;
> >+			*val2 = 612903;
> >+			return IIO_VAL_INT_PLUS_MICRO;
> >+		case IIO_CURRENT:
> >+			*val = 0;
> >+			*val2 = 1000;
> >+			return IIO_VAL_INT_PLUS_MICRO;
> >+		case IIO_CONCENTRATION:
> >+			switch (chan->channel2) {
> >+			case IIO_MOD_CO2:
> >+				*val = 0;
> >+				*val2 = 12834;
> >+				return IIO_VAL_INT_PLUS_MICRO;
> >+			case IIO_MOD_VOC:
> >+				*val = 0;
> >+				*val2 = 84246;
> >+				return IIO_VAL_INT_PLUS_MICRO;
> >+			default:
> >+				return -EINVAL;
> >+			}
> >+		default:
> >+			return -EINVAL;
> >+		}
> >+	case IIO_CHAN_INFO_OFFSET:
> >+		if (!(chan->type == IIO_CONCENTRATION &&
> >+		      chan->channel2 == IIO_MOD_CO2))
> >+			return -EINVAL;
> >+		*val = -400;
> >+		return IIO_VAL_INT;
> >+	default:
> >+		return -EINVAL;
> >+	}
> >+}
> >+
> >+static const struct iio_info ccs811_info = {
> >+	.read_raw = ccs811_read_raw,
> at the moment we still need
> .driver_module = THIS_MODULE;
> 
> please keep it here. I'll fix up when applying if that requirement has
> gone away in the meantime.
> 
> I'm not 100% sure it will actually be used in this driver (As no
> chardev yet)
> but it's documented as being required so please put it in for now.
> 
> >+};
> >+
> >+static int ccs811_probe(struct i2c_client *client,
> >+			const struct i2c_device_id *id)
> >+{
> >+	struct iio_dev *indio_dev;
> >+	struct ccs811_data *data;
> >+	int ret;
> >+
> >+	if (!i2c_check_functionality(client->adapter,
> >I2C_FUNC_SMBUS_BYTE_DATA
> >+				     | I2C_FUNC_SMBUS_READ_I2C_BLOCK))
> >+		return -EOPNOTSUPP;
> >+
> >+	/* Check hardware id (should be 0x81 for this family of devices) */
> >+	ret = i2c_smbus_read_byte_data(client, CCS811_HW_ID);
> >+	if (ret < 0)
> >+		return ret;
> >+
> >+	if (ret != 0x81) {
> Use a define for the magic number 0x81 CCS881_HW_ID_VALUE or
> something like that.
> >+		dev_err(&client->dev, "hardware id doesn't match CCS81x\n");
> >+		return -ENODEV;
> >+	}
> >+
> >+	ret = i2c_smbus_read_byte_data(client, CCS811_HW_VERSION);
> >+	if (ret < 0)
> >+		return ret;
> >+
> >+	if ((ret & 0xF0) != 0x10) {
> Define a mask and a value for this rather than magic numbers in the
> code.
> 
> >+		dev_err(&client->dev, "no CCS811 sensor\n");
> >+		return -ENODEV;
> >+	}
> >+
> >+	indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
> >+	if (!indio_dev)
> >+		return -ENOMEM;
> >+
> >+	ret = ccs811_setup(client);
> >+	if (ret < 0)
> >+		return ret;
> >+
> >+	data = iio_priv(indio_dev);
> >+	i2c_set_clientdata(client, indio_dev);
> >+	data->client = client;
> >+
> >+	mutex_init(&data->lock);
> >+
> >+	indio_dev->dev.parent = &client->dev;
> >+	indio_dev->name = id->name;
> >+	indio_dev->info = &ccs811_info;
> >+
> >+	indio_dev->channels = ccs811_channels;
> >+	indio_dev->num_channels = ARRAY_SIZE(ccs811_channels);
> >+
> >+	return iio_device_register(indio_dev);
> >+}
> >+
> >+static int ccs811_remove(struct i2c_client *client)
> >+{
> >+	struct iio_dev *indio_dev = i2c_get_clientdata(client);
> >+
> >+	iio_device_unregister(indio_dev);
> >+
> >+	return ccs811_set_drive_mode(client, CCS811_MODE_IDLE);
> >+}
> >+
> >+static const struct i2c_device_id ccs811_id[] = {
> >+	{"ccs811", 0},
> >+	{	}
> >+};
> >+MODULE_DEVICE_TABLE(i2c, ccs811_id);
> >+
> >+static struct i2c_driver ccs811_driver = {
> >+	.driver = {
> >+		.name = "ccs811",
> >+	},
> >+	.probe = ccs811_probe,
> >+	.remove = ccs811_remove,
> >+	.id_table = ccs811_id,
> >+};
> >+module_i2c_driver(ccs811_driver);
> >+
> >+MODULE_AUTHOR("Narcisa Vasile <narcisaanamaria12@xxxxxxxxx>");
> >+MODULE_DESCRIPTION("CCS811 volatile organic compounds sensor");
> >+MODULE_LICENSE("GPL v2");
> 
--
To unsubscribe from this list: send the line "unsubscribe linux-iio" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html



[Index of Archives]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Yosemite News]     [Linux Input]     [Linux Kernel]     [Linux SCSI]     [X.org]

  Powered by Linux