Contributors: 2
Author Tokens Token Proportion Commits Commit Proportion
Gyeyoung Baek 1138 99.82% 1 50.00%
Dan Carpenter 2 0.18% 1 50.00%
Total 1140 2


// SPDX-License-Identifier: GPL-2.0
/*
 * mh-z19b CO₂ sensor driver
 *
 * Copyright (c) 2025 Gyeyoung Baek <gye976@gmail.com>
 *
 * Datasheet:
 * https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf
 */

#include <linux/array_size.h>
#include <linux/completion.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/jiffies.h>
#include <linux/kstrtox.h>
#include <linux/minmax.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/regulator/consumer.h>
#include <linux/serdev.h>
#include <linux/string.h>
#include <linux/types.h>
#include <linux/unaligned.h>

/*
 * Commands have following format:
 *
 * +------+------+-----+------+------+------+------+------+-------+
 * | 0xFF | 0x01 | cmd | arg0 | arg1 | 0x00 | 0x00 | 0x00 | cksum |
 * +------+------+-----+------+------+------+------+------+-------+
 */
#define MHZ19B_CMD_SIZE 9

/* ABC logic in MHZ19B means auto calibration. */
#define MHZ19B_ABC_LOGIC_CMD		0x79
#define MHZ19B_READ_CO2_CMD		0x86
#define MHZ19B_SPAN_POINT_CMD		0x88
#define MHZ19B_ZERO_POINT_CMD		0x87

#define MHZ19B_SPAN_POINT_PPM_MIN	1000
#define MHZ19B_SPAN_POINT_PPM_MAX	5000

#define MHZ19B_SERDEV_TIMEOUT msecs_to_jiffies(100)

struct mhz19b_state {
	struct serdev_device *serdev;

	/* Must wait until the 'buf' is filled with 9 bytes.*/
	struct completion buf_ready;

	u8 buf_idx;
	/*
	 * Serdev receive buffer.
	 * When data is received from the MH-Z19B,
	 * the 'mhz19b_receive_buf' callback function is called and fills this buffer.
	 */
	u8 buf[MHZ19B_CMD_SIZE] __aligned(IIO_DMA_MINALIGN);
};

static u8 mhz19b_get_checksum(u8 *cmd_buf)
{
	u8 i, checksum = 0;

/*
 * +------+------+-----+------+------+------+------+------+-------+
 * | 0xFF | 0x01 | cmd | arg0 | arg1 | 0x00 | 0x00 | 0x00 | cksum |
 * +------+------+-----+------+------+------+------+------+-------+
 *	     i:1    2      3      4      5      6      7
 *
 *  Sum all cmd_buf elements from index 1 to 7.
 */
	for (i = 1; i < 8; i++)
		checksum += cmd_buf[i];

	return -checksum;
}

static int mhz19b_serdev_cmd(struct iio_dev *indio_dev, int cmd, u16 arg)
{
	struct mhz19b_state *st = iio_priv(indio_dev);
	struct serdev_device *serdev = st->serdev;
	struct device *dev = &indio_dev->dev;
	int ret;

	/*
	 * cmd_buf[3,4] : arg0,1
	 * cmd_buf[8]	: checksum
	 */
	u8 cmd_buf[MHZ19B_CMD_SIZE] = {
		0xFF, 0x01, cmd,
	};

	switch (cmd) {
	case MHZ19B_ABC_LOGIC_CMD:
		cmd_buf[3] = arg ? 0xA0 : 0;
		break;
	case MHZ19B_SPAN_POINT_CMD:
		put_unaligned_be16(arg, &cmd_buf[3]);
		break;
	default:
		break;
	}
	cmd_buf[8] = mhz19b_get_checksum(cmd_buf);

	/* Write buf to uart ctrl synchronously */
	ret = serdev_device_write(serdev, cmd_buf, MHZ19B_CMD_SIZE, 0);
	if (ret < 0)
		return ret;
	if (ret != MHZ19B_CMD_SIZE)
		return -EIO;

	switch (cmd) {
	case MHZ19B_READ_CO2_CMD:
		ret = wait_for_completion_interruptible_timeout(&st->buf_ready,
			MHZ19B_SERDEV_TIMEOUT);
		if (ret < 0)
			return ret;
		if (!ret)
			return -ETIMEDOUT;

		if (st->buf[8] != mhz19b_get_checksum(st->buf)) {
			dev_err(dev, "checksum err");
			return -EINVAL;
		}

		return get_unaligned_be16(&st->buf[2]);
	default:
		/* No response commands. */
		return 0;
	}
}

static int mhz19b_read_raw(struct iio_dev *indio_dev,
			   struct iio_chan_spec const *chan,
			   int *val, int *val2, long mask)
{
	int ret;

	ret = mhz19b_serdev_cmd(indio_dev, MHZ19B_READ_CO2_CMD, 0);
	if (ret < 0)
		return ret;

	*val = ret;
	return IIO_VAL_INT;
}

/*
 * echo 0 > calibration_auto_enable : ABC logic off
 * echo 1 > calibration_auto_enable : ABC logic on
 */
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);
	bool enable;
	int ret;

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

	ret = mhz19b_serdev_cmd(indio_dev, MHZ19B_ABC_LOGIC_CMD, enable);
	if (ret < 0)
		return ret;

	return len;
}
static IIO_DEVICE_ATTR_WO(calibration_auto_enable, 0);

/*
 * echo 0 > calibration_forced_value		 : zero point calibration
 *	(make sure the sensor has been working under 400ppm for over 20 minutes.)
 * echo [1000 1 5000] > calibration_forced_value : span point calibration
 *	(make sure the sensor has been working under a certain level CO₂ for over 20 minutes.)
 */
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);
	u16 ppm;
	int cmd, ret;

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

	if (ppm) {
		if (!in_range(ppm, MHZ19B_SPAN_POINT_PPM_MIN,
			MHZ19B_SPAN_POINT_PPM_MAX - MHZ19B_SPAN_POINT_PPM_MIN + 1)) {
			dev_dbg(&indio_dev->dev,
				"span point ppm should be in a range [%d-%d]\n",
				MHZ19B_SPAN_POINT_PPM_MIN, MHZ19B_SPAN_POINT_PPM_MAX);
			return -EINVAL;
		}

		cmd = MHZ19B_SPAN_POINT_CMD;
	} else {
		cmd = MHZ19B_ZERO_POINT_CMD;
	}

	ret = mhz19b_serdev_cmd(indio_dev, cmd, ppm);
	if (ret < 0)
		return ret;

	return len;
}
static IIO_DEVICE_ATTR_WO(calibration_forced_value, 0);

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

static const struct attribute_group mhz19b_attr_group = {
	.attrs = mhz19b_attrs,
};

static const struct iio_info mhz19b_info = {
	.attrs = &mhz19b_attr_group,
	.read_raw = mhz19b_read_raw,
};

static const struct iio_chan_spec mhz19b_channels[] = {
	{
		.type = IIO_CONCENTRATION,
		.channel2 = IIO_MOD_CO2,
		.modified = 1,
		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
	},
};

static size_t mhz19b_receive_buf(struct serdev_device *serdev,
			      const u8 *data, size_t len)
{
	struct iio_dev *indio_dev = dev_get_drvdata(&serdev->dev);
	struct mhz19b_state *st = iio_priv(indio_dev);

	memcpy(st->buf + st->buf_idx, data, len);
	st->buf_idx += len;

	if (st->buf_idx == MHZ19B_CMD_SIZE) {
		st->buf_idx = 0;
		complete(&st->buf_ready);
	}

	return len;
}

static const struct serdev_device_ops mhz19b_ops = {
	.receive_buf = mhz19b_receive_buf,
	.write_wakeup = serdev_device_write_wakeup,
};

static int mhz19b_probe(struct serdev_device *serdev)
{
	int ret;
	struct device *dev = &serdev->dev;
	struct iio_dev *indio_dev;
	struct mhz19b_state *st;

	serdev_device_set_client_ops(serdev, &mhz19b_ops);
	ret = devm_serdev_device_open(dev, serdev);
	if (ret)
		return ret;
	serdev_device_set_baudrate(serdev, 9600);
	serdev_device_set_flow_control(serdev, false);
	ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
	if (ret)
		return ret;

	indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
	if (!indio_dev)
		return -ENOMEM;
	serdev_device_set_drvdata(serdev, indio_dev);

	st = iio_priv(indio_dev);
	st->serdev = serdev;

	init_completion(&st->buf_ready);

	ret = devm_regulator_get_enable(dev, "vin");
	if (ret)
		return ret;

	indio_dev->name = "mh-z19b";
	indio_dev->channels = mhz19b_channels;
	indio_dev->num_channels = ARRAY_SIZE(mhz19b_channels);
	indio_dev->info = &mhz19b_info;

	return devm_iio_device_register(dev, indio_dev);
}

static const struct of_device_id mhz19b_of_match[] = {
	{ .compatible = "winsen,mhz19b", },
	{ }
};
MODULE_DEVICE_TABLE(of, mhz19b_of_match);

static struct serdev_device_driver mhz19b_driver = {
	.driver = {
		.name = "mhz19b",
		.of_match_table = mhz19b_of_match,
	},
	.probe = mhz19b_probe,
};
module_serdev_device_driver(mhz19b_driver);

MODULE_AUTHOR("Gyeyoung Baek");
MODULE_DESCRIPTION("MH-Z19B CO2 sensor driver using serdev interface");
MODULE_LICENSE("GPL");