Linux preempt-rt

Check our new training course

Real-Time Linux with PREEMPT_RT

Check our new training course
with Creative Commons CC-BY-SA
lecture and lab materials

Bootlin logo

Elixir Cross Referencer

/*
 * vz89x.c - Support for SGX Sensortech MiCS VZ89X VOC sensors
 *
 * Copyright (C) 2015 Matt Ranostay <mranostay@gmail.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.
 *
 */

#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/init.h>
#include <linux/i2c.h>

#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>

#define VZ89X_REG_MEASUREMENT		0x09
#define VZ89X_REG_MEASUREMENT_SIZE	6

#define VZ89X_VOC_CO2_IDX		0
#define VZ89X_VOC_SHORT_IDX		1
#define VZ89X_VOC_TVOC_IDX		2
#define VZ89X_VOC_RESISTANCE_IDX	3

struct vz89x_data {
	struct i2c_client *client;
	struct mutex lock;
	unsigned long last_update;

	u8 buffer[VZ89X_REG_MEASUREMENT_SIZE];
};

static const struct iio_chan_spec vz89x_channels[] = {
	{
		.type = IIO_CONCENTRATION,
		.channel2 = IIO_MOD_CO2,
		.modified = 1,
		.info_mask_separate =
			BIT(IIO_CHAN_INFO_OFFSET) | BIT(IIO_CHAN_INFO_RAW),
		.address = VZ89X_VOC_CO2_IDX,
	},
	{
		.type = IIO_CONCENTRATION,
		.channel2 = IIO_MOD_VOC,
		.modified = 1,
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
		.address = VZ89X_VOC_SHORT_IDX,
		.extend_name = "short",
	},
	{
		.type = IIO_CONCENTRATION,
		.channel2 = IIO_MOD_VOC,
		.modified = 1,
		.info_mask_separate =
			BIT(IIO_CHAN_INFO_OFFSET) | BIT(IIO_CHAN_INFO_RAW),
		.address = VZ89X_VOC_TVOC_IDX,
	},
	{
		.type = IIO_RESISTANCE,
		.info_mask_separate =
			BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE),
		.address = VZ89X_VOC_RESISTANCE_IDX,
	},
};

static IIO_CONST_ATTR(in_concentration_co2_scale, "0.00000698689");
static IIO_CONST_ATTR(in_concentration_voc_scale, "0.00000000436681223");

static struct attribute *vz89x_attributes[] = {
	&iio_const_attr_in_concentration_co2_scale.dev_attr.attr,
	&iio_const_attr_in_concentration_voc_scale.dev_attr.attr,
	NULL,
};

static const struct attribute_group vz89x_attrs_group = {
	.attrs = vz89x_attributes,
};

/*
 * Chipset sometime updates in the middle of a reading causing it to reset the
 * data pointer, and causing invalid reading of previous data.
 * We can check for this by reading MSB of the resistance reading that is
 * always zero, and by also confirming the VOC_short isn't zero.
 */

static int vz89x_measurement_is_valid(struct vz89x_data *data)
{
	if (data->buffer[VZ89X_VOC_SHORT_IDX] == 0)
		return 1;

	return !!(data->buffer[VZ89X_REG_MEASUREMENT_SIZE - 1] > 0);
}

static int vz89x_get_measurement(struct vz89x_data *data)
{
	int ret;
	int i;

	/* sensor can only be polled once a second max per datasheet */
	if (!time_after(jiffies, data->last_update + HZ))
		return 0;

	ret = i2c_smbus_write_word_data(data->client,
					VZ89X_REG_MEASUREMENT, 0);
	if (ret < 0)
		return ret;

	for (i = 0; i < VZ89X_REG_MEASUREMENT_SIZE; i++) {
		ret = i2c_smbus_read_byte(data->client);
		if (ret < 0)
			return ret;
		data->buffer[i] = ret;
	}

	ret = vz89x_measurement_is_valid(data);
	if (ret)
		return -EAGAIN;

	data->last_update = jiffies;

	return 0;
}

static int vz89x_get_resistance_reading(struct vz89x_data *data)
{
	u8 *buf = &data->buffer[VZ89X_VOC_RESISTANCE_IDX];

	return buf[0] | (buf[1] << 8);
}

static int vz89x_read_raw(struct iio_dev *indio_dev,
			  struct iio_chan_spec const *chan, int *val,
			  int *val2, long mask)
{
	struct vz89x_data *data = iio_priv(indio_dev);
	int ret = -EINVAL;

	switch (mask) {
	case IIO_CHAN_INFO_RAW:
		mutex_lock(&data->lock);
		ret = vz89x_get_measurement(data);
		mutex_unlock(&data->lock);

		if (ret)
			return ret;

		switch (chan->address) {
		case VZ89X_VOC_CO2_IDX:
		case VZ89X_VOC_SHORT_IDX:
		case VZ89X_VOC_TVOC_IDX:
			*val = data->buffer[chan->address];
			return IIO_VAL_INT;
		case VZ89X_VOC_RESISTANCE_IDX:
			*val = vz89x_get_resistance_reading(data);
			return IIO_VAL_INT;
		default:
			return -EINVAL;
		}
		break;
	case IIO_CHAN_INFO_SCALE:
		switch (chan->type) {
		case IIO_RESISTANCE:
			*val = 10;
			return IIO_VAL_INT;
		default:
			return -EINVAL;
		}
		break;
	case IIO_CHAN_INFO_OFFSET:
		switch (chan->address) {
		case VZ89X_VOC_CO2_IDX:
			*val = 44;
			*val2 = 250000;
			return IIO_VAL_INT_PLUS_MICRO;
		case VZ89X_VOC_TVOC_IDX:
			*val = -13;
			return IIO_VAL_INT;
		default:
			return -EINVAL;
		}
	}

	return ret;
}

static const struct iio_info vz89x_info = {
	.attrs		= &vz89x_attrs_group,
	.read_raw	= vz89x_read_raw,
	.driver_module	= THIS_MODULE,
};

static int vz89x_probe(struct i2c_client *client,
		       const struct i2c_device_id *id)
{
	struct iio_dev *indio_dev;
	struct vz89x_data *data;

	if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WORD_DATA |
				     I2C_FUNC_SMBUS_BYTE))
		return -ENODEV;

	indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
	if (!indio_dev)
		return -ENOMEM;

	data = iio_priv(indio_dev);
	i2c_set_clientdata(client, indio_dev);
	data->client = client;
	data->last_update = jiffies - HZ;
	mutex_init(&data->lock);

	indio_dev->dev.parent = &client->dev;
	indio_dev->info = &vz89x_info,
	indio_dev->name = dev_name(&client->dev);
	indio_dev->modes = INDIO_DIRECT_MODE;

	indio_dev->channels = vz89x_channels;
	indio_dev->num_channels = ARRAY_SIZE(vz89x_channels);

	return devm_iio_device_register(&client->dev, indio_dev);
}

static const struct i2c_device_id vz89x_id[] = {
	{ "vz89x", 0 },
	{ }
};
MODULE_DEVICE_TABLE(i2c, vz89x_id);

static const struct of_device_id vz89x_dt_ids[] = {
	{ .compatible = "sgx,vz89x" },
	{ }
};
MODULE_DEVICE_TABLE(of, vz89x_dt_ids);

static struct i2c_driver vz89x_driver = {
	.driver = {
		.name	= "vz89x",
		.of_match_table = of_match_ptr(vz89x_dt_ids),
	},
	.probe = vz89x_probe,
	.id_table = vz89x_id,
};
module_i2c_driver(vz89x_driver);

MODULE_AUTHOR("Matt Ranostay <mranostay@gmail.com>");
MODULE_DESCRIPTION("SGX Sensortech MiCS VZ89X VOC sensors");
MODULE_LICENSE("GPL v2");