Contributors: 5
Author Tokens Token Proportion Commits Commit Proportion
Roan van Dijk 3289 99.43% 3 42.86%
Jonathan Cameron 10 0.30% 1 14.29%
Tom Rix 7 0.21% 1 14.29%
Uwe Kleine-König 1 0.03% 1 14.29%
Lars-Peter Clausen 1 0.03% 1 14.29%
Total 3308 7


// SPDX-License-Identifier: GPL-2.0
/*
 * Sensirion SCD4X carbon dioxide sensor i2c driver
 *
 * Copyright (C) 2021 Protonic Holland
 * Author: Roan van Dijk <roan@protonic.nl>
 *
 * I2C slave address: 0x62
 *
 * Datasheets:
 * https://www.sensirion.com/file/datasheet_scd4x
 */

#include <asm/unaligned.h>
#include <linux/crc8.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/i2c.h>
#include <linux/iio/buffer.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/trigger.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/iio/triggered_buffer.h>
#include <linux/iio/types.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/types.h>

#define SCD4X_CRC8_POLYNOMIAL 0x31
#define SCD4X_TIMEOUT_ERR 1000
#define SCD4X_READ_BUF_SIZE 9
#define SCD4X_COMMAND_BUF_SIZE 2
#define SCD4X_WRITE_BUF_SIZE 5
#define SCD4X_FRC_MIN_PPM 0
#define SCD4X_FRC_MAX_PPM 2000
#define SCD4X_PRESSURE_COMP_MIN_MBAR 700
#define SCD4X_PRESSURE_COMP_MAX_MBAR 1200
#define SCD4X_READY_MASK 0x01

/*Commands SCD4X*/
enum scd4x_cmd {
	CMD_START_MEAS          = 0x21b1,
	CMD_READ_MEAS           = 0xec05,
	CMD_STOP_MEAS           = 0x3f86,
	CMD_SET_TEMP_OFFSET     = 0x241d,
	CMD_GET_TEMP_OFFSET     = 0x2318,
	CMD_SET_AMB_PRESSURE	= 0xe000,
	CMD_GET_AMB_PRESSURE	= 0xe000,
	CMD_FRC                 = 0x362f,
	CMD_SET_ASC             = 0x2416,
	CMD_GET_ASC             = 0x2313,
	CMD_GET_DATA_READY      = 0xe4b8,
};

enum scd4x_channel_idx {
	SCD4X_CO2,
	SCD4X_TEMP,
	SCD4X_HR,
};

struct scd4x_state {
	struct i2c_client *client;
	/* maintain access to device, to prevent concurrent reads/writes */
	struct mutex lock;
	struct regulator *vdd;
};

DECLARE_CRC8_TABLE(scd4x_crc8_table);

static int scd4x_i2c_xfer(struct scd4x_state *state, char *txbuf, int txsize,
				char *rxbuf, int rxsize)
{
	struct i2c_client *client = state->client;
	int ret;

	ret = i2c_master_send(client, txbuf, txsize);

	if (ret < 0)
		return ret;
	if (ret != txsize)
		return -EIO;

	if (rxsize == 0)
		return 0;

	ret = i2c_master_recv(client, rxbuf, rxsize);
	if (ret < 0)
		return ret;
	if (ret != rxsize)
		return -EIO;

	return 0;
}

static int scd4x_send_command(struct scd4x_state *state, enum scd4x_cmd cmd)
{
	char buf[SCD4X_COMMAND_BUF_SIZE];
	int ret;

	/*
	 * Measurement needs to be stopped before sending commands.
	 * Except stop and start command.
	 */
	if ((cmd != CMD_STOP_MEAS) && (cmd != CMD_START_MEAS)) {

		ret = scd4x_send_command(state, CMD_STOP_MEAS);
		if (ret)
			return ret;

		/* execution time for stopping measurement */
		msleep_interruptible(500);
	}

	put_unaligned_be16(cmd, buf);
	ret = scd4x_i2c_xfer(state, buf, 2, buf, 0);
	if (ret)
		return ret;

	if ((cmd != CMD_STOP_MEAS) && (cmd != CMD_START_MEAS)) {
		ret = scd4x_send_command(state, CMD_START_MEAS);
		if (ret)
			return ret;
	}

	return 0;
}

static int scd4x_read(struct scd4x_state *state, enum scd4x_cmd cmd,
			void *response, int response_sz)
{
	struct i2c_client *client = state->client;
	char buf[SCD4X_READ_BUF_SIZE];
	char *rsp = response;
	int i, ret;
	char crc;

	/*
	 * Measurement needs to be stopped before sending commands.
	 * Except for reading measurement and data ready command.
	 */
	if ((cmd != CMD_GET_DATA_READY) && (cmd != CMD_READ_MEAS) &&
	    (cmd != CMD_GET_AMB_PRESSURE)) {
		ret = scd4x_send_command(state, CMD_STOP_MEAS);
		if (ret)
			return ret;

		/* execution time for stopping measurement */
		msleep_interruptible(500);
	}

	/* CRC byte for every 2 bytes of data */
	response_sz += response_sz / 2;

	put_unaligned_be16(cmd, buf);
	ret = scd4x_i2c_xfer(state, buf, 2, buf, response_sz);
	if (ret)
		return ret;

	for (i = 0; i < response_sz; i += 3) {
		crc = crc8(scd4x_crc8_table, buf + i, 2, CRC8_INIT_VALUE);
		if (crc != buf[i + 2]) {
			dev_err(&client->dev, "CRC error\n");
			return -EIO;
		}

		*rsp++ = buf[i];
		*rsp++ = buf[i + 1];
	}

	/* start measurement */
	if ((cmd != CMD_GET_DATA_READY) && (cmd != CMD_READ_MEAS) &&
	    (cmd != CMD_GET_AMB_PRESSURE)) {
		ret = scd4x_send_command(state, CMD_START_MEAS);
		if (ret)
			return ret;
	}

	return 0;
}

static int scd4x_write(struct scd4x_state *state, enum scd4x_cmd cmd, uint16_t arg)
{
	char buf[SCD4X_WRITE_BUF_SIZE];
	int ret;
	char crc;

	put_unaligned_be16(cmd, buf);
	put_unaligned_be16(arg, buf + 2);

	crc = crc8(scd4x_crc8_table, buf + 2, 2, CRC8_INIT_VALUE);
	buf[4] = crc;

	/* measurement needs to be stopped before sending commands */
	if (cmd != CMD_SET_AMB_PRESSURE) {
		ret = scd4x_send_command(state, CMD_STOP_MEAS);
		if (ret)
			return ret;
	}

	/* execution time */
	msleep_interruptible(500);

	ret = scd4x_i2c_xfer(state, buf, SCD4X_WRITE_BUF_SIZE, buf, 0);
	if (ret)
		return ret;

	/* start measurement, except for forced calibration command */
	if ((cmd != CMD_FRC) && (cmd != CMD_SET_AMB_PRESSURE)) {
		ret = scd4x_send_command(state, CMD_START_MEAS);
		if (ret)
			return ret;
	}

	return 0;
}

static int scd4x_write_and_fetch(struct scd4x_state *state, enum scd4x_cmd cmd,
				uint16_t arg, void *response, int response_sz)
{
	struct i2c_client *client = state->client;
	char buf[SCD4X_READ_BUF_SIZE];
	char *rsp = response;
	int i, ret;
	char crc;

	ret = scd4x_write(state, CMD_FRC, arg);
	if (ret)
		goto err;

	/* execution time */
	msleep_interruptible(400);

	/* CRC byte for every 2 bytes of data */
	response_sz += response_sz / 2;

	ret = i2c_master_recv(client, buf, response_sz);
	if (ret < 0)
		goto err;
	if (ret != response_sz) {
		ret = -EIO;
		goto err;
	}

	for (i = 0; i < response_sz; i += 3) {
		crc = crc8(scd4x_crc8_table, buf + i, 2, CRC8_INIT_VALUE);
		if (crc != buf[i + 2]) {
			dev_err(&client->dev, "CRC error\n");
			ret = -EIO;
			goto err;
		}

		*rsp++ = buf[i];
		*rsp++ = buf[i + 1];
	}

	return scd4x_send_command(state, CMD_START_MEAS);

err:
	/*
	 * on error try to start the measurement,
	 * puts sensor back into continuous measurement
	 */
	scd4x_send_command(state, CMD_START_MEAS);

	return ret;
}

static int scd4x_read_meas(struct scd4x_state *state, uint16_t *meas)
{
	int i, ret;
	__be16 buf[3];

	ret = scd4x_read(state, CMD_READ_MEAS, buf, sizeof(buf));
	if (ret)
		return ret;

	for (i = 0; i < ARRAY_SIZE(buf); i++)
		meas[i] = be16_to_cpu(buf[i]);

	return 0;
}

static int scd4x_wait_meas_poll(struct scd4x_state *state)
{
	struct i2c_client *client = state->client;
	int tries = 6;
	int ret;

	do {
		__be16 bval;
		uint16_t val;

		ret = scd4x_read(state, CMD_GET_DATA_READY, &bval, sizeof(bval));
		if (ret)
			return -EIO;
		val = be16_to_cpu(bval);

		/* new measurement available */
		if (val & 0x7FF)
			return 0;

		msleep_interruptible(1000);
	} while (--tries);

	/* try to start sensor on timeout */
	ret = scd4x_send_command(state, CMD_START_MEAS);
	if (ret)
		dev_err(&client->dev, "failed to start measurement: %d\n", ret);

	return -ETIMEDOUT;
}

static int scd4x_read_poll(struct scd4x_state *state, uint16_t *buf)
{
	int ret;

	ret = scd4x_wait_meas_poll(state);
	if (ret)
		return ret;

	return scd4x_read_meas(state, buf);
}

static int scd4x_read_channel(struct scd4x_state *state, int chan)
{
	int ret;
	uint16_t buf[3];

	ret = scd4x_read_poll(state, buf);
	if (ret)
		return ret;

	return buf[chan];
}

static int scd4x_read_raw(struct iio_dev *indio_dev,
			struct iio_chan_spec const *chan, int *val,
			int *val2, long mask)
{
	struct scd4x_state *state = iio_priv(indio_dev);
	int ret;
	__be16 tmp;

	switch (mask) {
	case IIO_CHAN_INFO_RAW:
		if (chan->output) {
			mutex_lock(&state->lock);
			ret = scd4x_read(state, CMD_GET_AMB_PRESSURE, &tmp, sizeof(tmp));
			mutex_unlock(&state->lock);

			if (ret)
				return ret;

			*val = be16_to_cpu(tmp);
			return IIO_VAL_INT;
		}

		ret = iio_device_claim_direct_mode(indio_dev);
		if (ret)
			return ret;

		mutex_lock(&state->lock);
		ret = scd4x_read_channel(state, chan->address);
		mutex_unlock(&state->lock);

		iio_device_release_direct_mode(indio_dev);
		if (ret < 0)
			return ret;

		*val = ret;
		return IIO_VAL_INT;
	case IIO_CHAN_INFO_SCALE:
		if (chan->type == IIO_CONCENTRATION) {
			*val = 0;
			*val2 = 100;
			return IIO_VAL_INT_PLUS_MICRO;
		} else if (chan->type == IIO_TEMP) {
			*val = 175000;
			*val2 = 65536;
			return IIO_VAL_FRACTIONAL;
		} else if (chan->type == IIO_HUMIDITYRELATIVE) {
			*val = 100000;
			*val2 = 65536;
			return IIO_VAL_FRACTIONAL;
		}
		return -EINVAL;
	case IIO_CHAN_INFO_OFFSET:
		*val = -16852;
		*val2 = 114286;
		return IIO_VAL_INT_PLUS_MICRO;
	case IIO_CHAN_INFO_CALIBBIAS:
		mutex_lock(&state->lock);
		ret = scd4x_read(state, CMD_GET_TEMP_OFFSET, &tmp, sizeof(tmp));
		mutex_unlock(&state->lock);
		if (ret)
			return ret;

		*val = be16_to_cpu(tmp);

		return IIO_VAL_INT;
	default:
		return -EINVAL;
	}
}

static const int scd4x_pressure_calibbias_available[] = {
	SCD4X_PRESSURE_COMP_MIN_MBAR, 1, SCD4X_PRESSURE_COMP_MAX_MBAR,
};

static int scd4x_read_avail(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
			    const int **vals, int *type, int *length, long mask)
{
	switch (mask) {
	case IIO_CHAN_INFO_RAW:
		*vals = scd4x_pressure_calibbias_available;
		*type = IIO_VAL_INT;

		return IIO_AVAIL_RANGE;
	}

	return -EINVAL;
}


static int scd4x_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
				int val, int val2, long mask)
{
	struct scd4x_state *state = iio_priv(indio_dev);
	int ret = 0;

	switch (mask) {
	case IIO_CHAN_INFO_CALIBBIAS:
		mutex_lock(&state->lock);
		ret = scd4x_write(state, CMD_SET_TEMP_OFFSET, val);
		mutex_unlock(&state->lock);

		return ret;
	case IIO_CHAN_INFO_RAW:
		switch (chan->type) {
		case IIO_PRESSURE:
			if (val < SCD4X_PRESSURE_COMP_MIN_MBAR ||
			    val > SCD4X_PRESSURE_COMP_MAX_MBAR)
				return -EINVAL;

			mutex_lock(&state->lock);
			ret = scd4x_write(state, CMD_SET_AMB_PRESSURE, val);
			mutex_unlock(&state->lock);

			return ret;
		default:
			return -EINVAL;
		}
	default:
		return -EINVAL;
	}
}

static ssize_t calibration_auto_enable_show(struct device *dev,
			struct device_attribute *attr, char *buf)
{
	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
	struct scd4x_state *state = iio_priv(indio_dev);
	int ret;
	__be16 bval;
	u16 val;

	mutex_lock(&state->lock);
	ret = scd4x_read(state, CMD_GET_ASC, &bval, sizeof(bval));
	mutex_unlock(&state->lock);
	if (ret) {
		dev_err(dev, "failed to read automatic calibration");
		return ret;
	}

	val = (be16_to_cpu(bval) & SCD4X_READY_MASK) ? 1 : 0;

	return sysfs_emit(buf, "%d\n", val);
}

static ssize_t calibration_auto_enable_store(struct device *dev,
					struct device_attribute *attr,
					const char *buf, size_t len)
{
	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
	struct scd4x_state *state = iio_priv(indio_dev);
	bool val;
	int ret;
	uint16_t value;

	ret = kstrtobool(buf, &val);
	if (ret)
		return ret;

	value = val;

	mutex_lock(&state->lock);
	ret = scd4x_write(state, CMD_SET_ASC, value);
	mutex_unlock(&state->lock);
	if (ret)
		dev_err(dev, "failed to set automatic calibration");

	return ret ?: len;
}

static ssize_t calibration_forced_value_store(struct device *dev,
					struct device_attribute *attr,
					const char *buf, size_t len)
{
	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
	struct scd4x_state *state = iio_priv(indio_dev);
	uint16_t val, arg;
	int ret;

	ret = kstrtou16(buf, 0, &arg);
	if (ret)
		return ret;

	if (arg < SCD4X_FRC_MIN_PPM || arg > SCD4X_FRC_MAX_PPM)
		return -EINVAL;

	mutex_lock(&state->lock);
	ret = scd4x_write_and_fetch(state, CMD_FRC, arg, &val, sizeof(val));
	mutex_unlock(&state->lock);

	if (ret)
		return ret;

	if (val == 0xff) {
		dev_err(dev, "forced calibration has failed");
		return -EINVAL;
	}

	return len;
}

static IIO_DEVICE_ATTR_RW(calibration_auto_enable, 0);
static IIO_DEVICE_ATTR_WO(calibration_forced_value, 0);

static IIO_CONST_ATTR(calibration_forced_value_available,
	       __stringify([SCD4X_FRC_MIN_PPM 1 SCD4X_FRC_MAX_PPM]));

static struct attribute *scd4x_attrs[] = {
	&iio_dev_attr_calibration_auto_enable.dev_attr.attr,
	&iio_dev_attr_calibration_forced_value.dev_attr.attr,
	&iio_const_attr_calibration_forced_value_available.dev_attr.attr,
	NULL
};

static const struct attribute_group scd4x_attr_group = {
	.attrs = scd4x_attrs,
};

static const struct iio_info scd4x_info = {
	.attrs = &scd4x_attr_group,
	.read_raw = scd4x_read_raw,
	.write_raw = scd4x_write_raw,
	.read_avail = scd4x_read_avail,
};

static const struct iio_chan_spec scd4x_channels[] = {
	{
		/*
		 * this channel is special in a sense we are pretending that
		 * sensor is able to change measurement chamber pressure but in
		 * fact we're just setting pressure compensation value
		 */
		.type = IIO_PRESSURE,
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
		.info_mask_separate_available = BIT(IIO_CHAN_INFO_RAW),
		.output = 1,
		.scan_index = -1,
	},
	{
		.type = IIO_CONCENTRATION,
		.channel2 = IIO_MOD_CO2,
		.modified = 1,
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
					BIT(IIO_CHAN_INFO_SCALE),
		.address = SCD4X_CO2,
		.scan_index = SCD4X_CO2,
		.scan_type = {
			.sign = 'u',
			.realbits = 16,
			.storagebits = 16,
			.endianness = IIO_BE,
		},
	},
	{
		.type = IIO_TEMP,
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
					BIT(IIO_CHAN_INFO_SCALE) |
					BIT(IIO_CHAN_INFO_OFFSET) |
					BIT(IIO_CHAN_INFO_CALIBBIAS),
		.address = SCD4X_TEMP,
		.scan_index = SCD4X_TEMP,
		.scan_type = {
			.sign = 'u',
			.realbits = 16,
			.storagebits = 16,
			.endianness = IIO_BE,
		},
	},
	{
		.type = IIO_HUMIDITYRELATIVE,
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
					BIT(IIO_CHAN_INFO_SCALE),
		.address = SCD4X_HR,
		.scan_index = SCD4X_HR,
		.scan_type = {
			.sign = 'u',
			.realbits = 16,
			.storagebits = 16,
			.endianness = IIO_BE,
		},
	},
};

static int scd4x_suspend(struct device *dev)
{
	struct iio_dev *indio_dev = dev_get_drvdata(dev);
	struct scd4x_state *state  = iio_priv(indio_dev);
	int ret;

	ret = scd4x_send_command(state, CMD_STOP_MEAS);
	if (ret)
		return ret;

	return regulator_disable(state->vdd);
}

static int scd4x_resume(struct device *dev)
{
	struct iio_dev *indio_dev = dev_get_drvdata(dev);
	struct scd4x_state *state = iio_priv(indio_dev);
	int ret;

	ret = regulator_enable(state->vdd);
	if (ret)
		return ret;

	return scd4x_send_command(state, CMD_START_MEAS);
}

static DEFINE_SIMPLE_DEV_PM_OPS(scd4x_pm_ops, scd4x_suspend, scd4x_resume);

static void scd4x_stop_meas(void *state)
{
	scd4x_send_command(state, CMD_STOP_MEAS);
}

static void scd4x_disable_regulator(void *data)
{
	struct scd4x_state *state = data;

	regulator_disable(state->vdd);
}

static irqreturn_t scd4x_trigger_handler(int irq, void *p)
{
	struct iio_poll_func *pf = p;
	struct iio_dev *indio_dev = pf->indio_dev;
	struct scd4x_state *state = iio_priv(indio_dev);
	struct {
		uint16_t data[3];
		int64_t ts __aligned(8);
	} scan;
	int ret;

	memset(&scan, 0, sizeof(scan));
	mutex_lock(&state->lock);
	ret = scd4x_read_poll(state, scan.data);
	mutex_unlock(&state->lock);
	if (ret)
		goto out;

	iio_push_to_buffers_with_timestamp(indio_dev, &scan, iio_get_time_ns(indio_dev));
out:
	iio_trigger_notify_done(indio_dev->trig);
	return IRQ_HANDLED;
}

static int scd4x_probe(struct i2c_client *client)
{
	static const unsigned long scd4x_scan_masks[] = { 0x07, 0x00 };
	struct device *dev = &client->dev;
	struct iio_dev *indio_dev;
	struct scd4x_state *state;
	int ret;

	indio_dev = devm_iio_device_alloc(dev, sizeof(*state));
	if (!indio_dev)
		return -ENOMEM;

	state = iio_priv(indio_dev);
	mutex_init(&state->lock);
	state->client = client;
	crc8_populate_msb(scd4x_crc8_table, SCD4X_CRC8_POLYNOMIAL);

	indio_dev->info = &scd4x_info;
	indio_dev->name = client->name;
	indio_dev->channels = scd4x_channels;
	indio_dev->num_channels = ARRAY_SIZE(scd4x_channels);
	indio_dev->modes = INDIO_DIRECT_MODE;
	indio_dev->available_scan_masks = scd4x_scan_masks;

	state->vdd = devm_regulator_get(dev, "vdd");
	if (IS_ERR(state->vdd))
		return dev_err_probe(dev, PTR_ERR(state->vdd), "failed to get regulator\n");

	ret = regulator_enable(state->vdd);
	if (ret)
		return ret;

	ret = devm_add_action_or_reset(dev, scd4x_disable_regulator, state);
	if (ret)
		return ret;

	ret = scd4x_send_command(state, CMD_STOP_MEAS);
	if (ret) {
		dev_err(dev, "failed to stop measurement: %d\n", ret);
		return ret;
	}

	/* execution time */
	msleep_interruptible(500);

	ret = devm_iio_triggered_buffer_setup(dev, indio_dev, NULL, scd4x_trigger_handler, NULL);
	if (ret)
		return ret;

	ret = scd4x_send_command(state, CMD_START_MEAS);
	if (ret) {
		dev_err(dev, "failed to start measurement: %d\n", ret);
		return ret;
	}

	ret = devm_add_action_or_reset(dev, scd4x_stop_meas, state);
	if (ret)
		return ret;

	return devm_iio_device_register(dev, indio_dev);
}

static const struct of_device_id scd4x_dt_ids[] = {
	{ .compatible = "sensirion,scd40" },
	{ .compatible = "sensirion,scd41" },
	{ }
};
MODULE_DEVICE_TABLE(of, scd4x_dt_ids);

static struct i2c_driver scd4x_i2c_driver = {
	.driver = {
		.name = KBUILD_MODNAME,
		.of_match_table = scd4x_dt_ids,
		.pm = pm_sleep_ptr(&scd4x_pm_ops),
	},
	.probe = scd4x_probe,
};
module_i2c_driver(scd4x_i2c_driver);

MODULE_AUTHOR("Roan van Dijk <roan@protonic.nl>");
MODULE_DESCRIPTION("Sensirion SCD4X carbon dioxide sensor core driver");
MODULE_LICENSE("GPL v2");