xref: /linux/drivers/iio/chemical/mhz19b.c (revision c26f4fbd58375bd6ef74f95eb73d61762ad97c59)
14572a70bSGyeyoung Baek // SPDX-License-Identifier: GPL-2.0
24572a70bSGyeyoung Baek /*
34572a70bSGyeyoung Baek  * mh-z19b CO₂ sensor driver
44572a70bSGyeyoung Baek  *
54572a70bSGyeyoung Baek  * Copyright (c) 2025 Gyeyoung Baek <gye976@gmail.com>
64572a70bSGyeyoung Baek  *
74572a70bSGyeyoung Baek  * Datasheet:
84572a70bSGyeyoung Baek  * https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf
94572a70bSGyeyoung Baek  */
104572a70bSGyeyoung Baek 
114572a70bSGyeyoung Baek #include <linux/array_size.h>
124572a70bSGyeyoung Baek #include <linux/completion.h>
134572a70bSGyeyoung Baek #include <linux/device.h>
144572a70bSGyeyoung Baek #include <linux/errno.h>
154572a70bSGyeyoung Baek #include <linux/iio/iio.h>
164572a70bSGyeyoung Baek #include <linux/iio/sysfs.h>
174572a70bSGyeyoung Baek #include <linux/jiffies.h>
184572a70bSGyeyoung Baek #include <linux/kstrtox.h>
194572a70bSGyeyoung Baek #include <linux/minmax.h>
204572a70bSGyeyoung Baek #include <linux/mod_devicetable.h>
214572a70bSGyeyoung Baek #include <linux/module.h>
224572a70bSGyeyoung Baek #include <linux/regulator/consumer.h>
234572a70bSGyeyoung Baek #include <linux/serdev.h>
244572a70bSGyeyoung Baek #include <linux/string.h>
254572a70bSGyeyoung Baek #include <linux/types.h>
264572a70bSGyeyoung Baek #include <linux/unaligned.h>
274572a70bSGyeyoung Baek 
284572a70bSGyeyoung Baek /*
294572a70bSGyeyoung Baek  * Commands have following format:
304572a70bSGyeyoung Baek  *
314572a70bSGyeyoung Baek  * +------+------+-----+------+------+------+------+------+-------+
324572a70bSGyeyoung Baek  * | 0xFF | 0x01 | cmd | arg0 | arg1 | 0x00 | 0x00 | 0x00 | cksum |
334572a70bSGyeyoung Baek  * +------+------+-----+------+------+------+------+------+-------+
344572a70bSGyeyoung Baek  */
354572a70bSGyeyoung Baek #define MHZ19B_CMD_SIZE 9
364572a70bSGyeyoung Baek 
374572a70bSGyeyoung Baek /* ABC logic in MHZ19B means auto calibration. */
384572a70bSGyeyoung Baek #define MHZ19B_ABC_LOGIC_CMD		0x79
394572a70bSGyeyoung Baek #define MHZ19B_READ_CO2_CMD		0x86
404572a70bSGyeyoung Baek #define MHZ19B_SPAN_POINT_CMD		0x88
414572a70bSGyeyoung Baek #define MHZ19B_ZERO_POINT_CMD		0x87
424572a70bSGyeyoung Baek 
434572a70bSGyeyoung Baek #define MHZ19B_SPAN_POINT_PPM_MIN	1000
444572a70bSGyeyoung Baek #define MHZ19B_SPAN_POINT_PPM_MAX	5000
454572a70bSGyeyoung Baek 
464572a70bSGyeyoung Baek #define MHZ19B_SERDEV_TIMEOUT msecs_to_jiffies(100)
474572a70bSGyeyoung Baek 
484572a70bSGyeyoung Baek struct mhz19b_state {
494572a70bSGyeyoung Baek 	struct serdev_device *serdev;
504572a70bSGyeyoung Baek 
514572a70bSGyeyoung Baek 	/* Must wait until the 'buf' is filled with 9 bytes.*/
524572a70bSGyeyoung Baek 	struct completion buf_ready;
534572a70bSGyeyoung Baek 
544572a70bSGyeyoung Baek 	u8 buf_idx;
554572a70bSGyeyoung Baek 	/*
564572a70bSGyeyoung Baek 	 * Serdev receive buffer.
574572a70bSGyeyoung Baek 	 * When data is received from the MH-Z19B,
584572a70bSGyeyoung Baek 	 * the 'mhz19b_receive_buf' callback function is called and fills this buffer.
594572a70bSGyeyoung Baek 	 */
604572a70bSGyeyoung Baek 	u8 buf[MHZ19B_CMD_SIZE] __aligned(IIO_DMA_MINALIGN);
614572a70bSGyeyoung Baek };
624572a70bSGyeyoung Baek 
mhz19b_get_checksum(u8 * cmd_buf)634572a70bSGyeyoung Baek static u8 mhz19b_get_checksum(u8 *cmd_buf)
644572a70bSGyeyoung Baek {
654572a70bSGyeyoung Baek 	u8 i, checksum = 0;
664572a70bSGyeyoung Baek 
674572a70bSGyeyoung Baek /*
684572a70bSGyeyoung Baek  * +------+------+-----+------+------+------+------+------+-------+
694572a70bSGyeyoung Baek  * | 0xFF | 0x01 | cmd | arg0 | arg1 | 0x00 | 0x00 | 0x00 | cksum |
704572a70bSGyeyoung Baek  * +------+------+-----+------+------+------+------+------+-------+
714572a70bSGyeyoung Baek  *	     i:1    2      3      4      5      6      7
724572a70bSGyeyoung Baek  *
734572a70bSGyeyoung Baek  *  Sum all cmd_buf elements from index 1 to 7.
744572a70bSGyeyoung Baek  */
754572a70bSGyeyoung Baek 	for (i = 1; i < 8; i++)
764572a70bSGyeyoung Baek 		checksum += cmd_buf[i];
774572a70bSGyeyoung Baek 
784572a70bSGyeyoung Baek 	return -checksum;
794572a70bSGyeyoung Baek }
804572a70bSGyeyoung Baek 
mhz19b_serdev_cmd(struct iio_dev * indio_dev,int cmd,u16 arg)814572a70bSGyeyoung Baek static int mhz19b_serdev_cmd(struct iio_dev *indio_dev, int cmd, u16 arg)
824572a70bSGyeyoung Baek {
834572a70bSGyeyoung Baek 	struct mhz19b_state *st = iio_priv(indio_dev);
844572a70bSGyeyoung Baek 	struct serdev_device *serdev = st->serdev;
854572a70bSGyeyoung Baek 	struct device *dev = &indio_dev->dev;
864572a70bSGyeyoung Baek 	int ret;
874572a70bSGyeyoung Baek 
884572a70bSGyeyoung Baek 	/*
894572a70bSGyeyoung Baek 	 * cmd_buf[3,4] : arg0,1
904572a70bSGyeyoung Baek 	 * cmd_buf[8]	: checksum
914572a70bSGyeyoung Baek 	 */
924572a70bSGyeyoung Baek 	u8 cmd_buf[MHZ19B_CMD_SIZE] = {
934572a70bSGyeyoung Baek 		0xFF, 0x01, cmd,
944572a70bSGyeyoung Baek 	};
954572a70bSGyeyoung Baek 
964572a70bSGyeyoung Baek 	switch (cmd) {
974572a70bSGyeyoung Baek 	case MHZ19B_ABC_LOGIC_CMD:
984572a70bSGyeyoung Baek 		cmd_buf[3] = arg ? 0xA0 : 0;
994572a70bSGyeyoung Baek 		break;
1004572a70bSGyeyoung Baek 	case MHZ19B_SPAN_POINT_CMD:
1014572a70bSGyeyoung Baek 		put_unaligned_be16(arg, &cmd_buf[3]);
1024572a70bSGyeyoung Baek 		break;
1034572a70bSGyeyoung Baek 	default:
1044572a70bSGyeyoung Baek 		break;
1054572a70bSGyeyoung Baek 	}
1064572a70bSGyeyoung Baek 	cmd_buf[8] = mhz19b_get_checksum(cmd_buf);
1074572a70bSGyeyoung Baek 
1084572a70bSGyeyoung Baek 	/* Write buf to uart ctrl synchronously */
1094572a70bSGyeyoung Baek 	ret = serdev_device_write(serdev, cmd_buf, MHZ19B_CMD_SIZE, 0);
1104572a70bSGyeyoung Baek 	if (ret < 0)
1114572a70bSGyeyoung Baek 		return ret;
1124572a70bSGyeyoung Baek 	if (ret != MHZ19B_CMD_SIZE)
1134572a70bSGyeyoung Baek 		return -EIO;
1144572a70bSGyeyoung Baek 
1154572a70bSGyeyoung Baek 	switch (cmd) {
1164572a70bSGyeyoung Baek 	case MHZ19B_READ_CO2_CMD:
1174572a70bSGyeyoung Baek 		ret = wait_for_completion_interruptible_timeout(&st->buf_ready,
1184572a70bSGyeyoung Baek 			MHZ19B_SERDEV_TIMEOUT);
1194572a70bSGyeyoung Baek 		if (ret < 0)
1204572a70bSGyeyoung Baek 			return ret;
1214572a70bSGyeyoung Baek 		if (!ret)
1224572a70bSGyeyoung Baek 			return -ETIMEDOUT;
1234572a70bSGyeyoung Baek 
1244572a70bSGyeyoung Baek 		if (st->buf[8] != mhz19b_get_checksum(st->buf)) {
1254572a70bSGyeyoung Baek 			dev_err(dev, "checksum err");
1264572a70bSGyeyoung Baek 			return -EINVAL;
1274572a70bSGyeyoung Baek 		}
1284572a70bSGyeyoung Baek 
1294572a70bSGyeyoung Baek 		return get_unaligned_be16(&st->buf[2]);
1304572a70bSGyeyoung Baek 	default:
1314572a70bSGyeyoung Baek 		/* No response commands. */
1324572a70bSGyeyoung Baek 		return 0;
1334572a70bSGyeyoung Baek 	}
1344572a70bSGyeyoung Baek }
1354572a70bSGyeyoung Baek 
mhz19b_read_raw(struct iio_dev * indio_dev,struct iio_chan_spec const * chan,int * val,int * val2,long mask)1364572a70bSGyeyoung Baek static int mhz19b_read_raw(struct iio_dev *indio_dev,
1374572a70bSGyeyoung Baek 			   struct iio_chan_spec const *chan,
1384572a70bSGyeyoung Baek 			   int *val, int *val2, long mask)
1394572a70bSGyeyoung Baek {
1404572a70bSGyeyoung Baek 	int ret;
1414572a70bSGyeyoung Baek 
1424572a70bSGyeyoung Baek 	ret = mhz19b_serdev_cmd(indio_dev, MHZ19B_READ_CO2_CMD, 0);
1434572a70bSGyeyoung Baek 	if (ret < 0)
1444572a70bSGyeyoung Baek 		return ret;
1454572a70bSGyeyoung Baek 
1464572a70bSGyeyoung Baek 	*val = ret;
1474572a70bSGyeyoung Baek 	return IIO_VAL_INT;
1484572a70bSGyeyoung Baek }
1494572a70bSGyeyoung Baek 
1504572a70bSGyeyoung Baek /*
1514572a70bSGyeyoung Baek  * echo 0 > calibration_auto_enable : ABC logic off
1524572a70bSGyeyoung Baek  * echo 1 > calibration_auto_enable : ABC logic on
1534572a70bSGyeyoung Baek  */
calibration_auto_enable_store(struct device * dev,struct device_attribute * attr,const char * buf,size_t len)1544572a70bSGyeyoung Baek static ssize_t calibration_auto_enable_store(struct device *dev,
1554572a70bSGyeyoung Baek 					     struct device_attribute *attr,
1564572a70bSGyeyoung Baek 					     const char *buf, size_t len)
1574572a70bSGyeyoung Baek {
1584572a70bSGyeyoung Baek 	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
1594572a70bSGyeyoung Baek 	bool enable;
1604572a70bSGyeyoung Baek 	int ret;
1614572a70bSGyeyoung Baek 
1624572a70bSGyeyoung Baek 	ret = kstrtobool(buf, &enable);
1634572a70bSGyeyoung Baek 	if (ret)
1644572a70bSGyeyoung Baek 		return ret;
1654572a70bSGyeyoung Baek 
1664572a70bSGyeyoung Baek 	ret = mhz19b_serdev_cmd(indio_dev, MHZ19B_ABC_LOGIC_CMD, enable);
1674572a70bSGyeyoung Baek 	if (ret < 0)
1684572a70bSGyeyoung Baek 		return ret;
1694572a70bSGyeyoung Baek 
1704572a70bSGyeyoung Baek 	return len;
1714572a70bSGyeyoung Baek }
1724572a70bSGyeyoung Baek static IIO_DEVICE_ATTR_WO(calibration_auto_enable, 0);
1734572a70bSGyeyoung Baek 
1744572a70bSGyeyoung Baek /*
1754572a70bSGyeyoung Baek  * echo 0 > calibration_forced_value		 : zero point calibration
1764572a70bSGyeyoung Baek  *	(make sure the sensor has been working under 400ppm for over 20 minutes.)
1774572a70bSGyeyoung Baek  * echo [1000 1 5000] > calibration_forced_value : span point calibration
1784572a70bSGyeyoung Baek  *	(make sure the sensor has been working under a certain level CO₂ for over 20 minutes.)
1794572a70bSGyeyoung Baek  */
calibration_forced_value_store(struct device * dev,struct device_attribute * attr,const char * buf,size_t len)1804572a70bSGyeyoung Baek static ssize_t calibration_forced_value_store(struct device *dev,
1814572a70bSGyeyoung Baek 					      struct device_attribute *attr,
1824572a70bSGyeyoung Baek 					      const char *buf, size_t len)
1834572a70bSGyeyoung Baek {
1844572a70bSGyeyoung Baek 	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
1854572a70bSGyeyoung Baek 	u16 ppm;
1864572a70bSGyeyoung Baek 	int cmd, ret;
1874572a70bSGyeyoung Baek 
1884572a70bSGyeyoung Baek 	ret = kstrtou16(buf, 0, &ppm);
1894572a70bSGyeyoung Baek 	if (ret)
1904572a70bSGyeyoung Baek 		return ret;
1914572a70bSGyeyoung Baek 
1924572a70bSGyeyoung Baek 	if (ppm) {
1934572a70bSGyeyoung Baek 		if (!in_range(ppm, MHZ19B_SPAN_POINT_PPM_MIN,
1944572a70bSGyeyoung Baek 			MHZ19B_SPAN_POINT_PPM_MAX - MHZ19B_SPAN_POINT_PPM_MIN + 1)) {
1954572a70bSGyeyoung Baek 			dev_dbg(&indio_dev->dev,
1964572a70bSGyeyoung Baek 				"span point ppm should be in a range [%d-%d]\n",
1974572a70bSGyeyoung Baek 				MHZ19B_SPAN_POINT_PPM_MIN, MHZ19B_SPAN_POINT_PPM_MAX);
1984572a70bSGyeyoung Baek 			return -EINVAL;
1994572a70bSGyeyoung Baek 		}
2004572a70bSGyeyoung Baek 
2014572a70bSGyeyoung Baek 		cmd = MHZ19B_SPAN_POINT_CMD;
2024572a70bSGyeyoung Baek 	} else {
2034572a70bSGyeyoung Baek 		cmd = MHZ19B_ZERO_POINT_CMD;
2044572a70bSGyeyoung Baek 	}
2054572a70bSGyeyoung Baek 
2064572a70bSGyeyoung Baek 	ret = mhz19b_serdev_cmd(indio_dev, cmd, ppm);
2074572a70bSGyeyoung Baek 	if (ret < 0)
2084572a70bSGyeyoung Baek 		return ret;
2094572a70bSGyeyoung Baek 
2104572a70bSGyeyoung Baek 	return len;
2114572a70bSGyeyoung Baek }
2124572a70bSGyeyoung Baek static IIO_DEVICE_ATTR_WO(calibration_forced_value, 0);
2134572a70bSGyeyoung Baek 
2144572a70bSGyeyoung Baek static struct attribute *mhz19b_attrs[] = {
2154572a70bSGyeyoung Baek 	&iio_dev_attr_calibration_auto_enable.dev_attr.attr,
2164572a70bSGyeyoung Baek 	&iio_dev_attr_calibration_forced_value.dev_attr.attr,
2174572a70bSGyeyoung Baek 	NULL
2184572a70bSGyeyoung Baek };
2194572a70bSGyeyoung Baek 
2204572a70bSGyeyoung Baek static const struct attribute_group mhz19b_attr_group = {
2214572a70bSGyeyoung Baek 	.attrs = mhz19b_attrs,
2224572a70bSGyeyoung Baek };
2234572a70bSGyeyoung Baek 
2244572a70bSGyeyoung Baek static const struct iio_info mhz19b_info = {
2254572a70bSGyeyoung Baek 	.attrs = &mhz19b_attr_group,
2264572a70bSGyeyoung Baek 	.read_raw = mhz19b_read_raw,
2274572a70bSGyeyoung Baek };
2284572a70bSGyeyoung Baek 
2294572a70bSGyeyoung Baek static const struct iio_chan_spec mhz19b_channels[] = {
2304572a70bSGyeyoung Baek 	{
2314572a70bSGyeyoung Baek 		.type = IIO_CONCENTRATION,
2324572a70bSGyeyoung Baek 		.channel2 = IIO_MOD_CO2,
2334572a70bSGyeyoung Baek 		.modified = 1,
2344572a70bSGyeyoung Baek 		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
2354572a70bSGyeyoung Baek 	},
2364572a70bSGyeyoung Baek };
2374572a70bSGyeyoung Baek 
mhz19b_receive_buf(struct serdev_device * serdev,const u8 * data,size_t len)2384572a70bSGyeyoung Baek static size_t mhz19b_receive_buf(struct serdev_device *serdev,
2394572a70bSGyeyoung Baek 			      const u8 *data, size_t len)
2404572a70bSGyeyoung Baek {
2414572a70bSGyeyoung Baek 	struct iio_dev *indio_dev = dev_get_drvdata(&serdev->dev);
2424572a70bSGyeyoung Baek 	struct mhz19b_state *st = iio_priv(indio_dev);
2434572a70bSGyeyoung Baek 
2444572a70bSGyeyoung Baek 	memcpy(st->buf + st->buf_idx, data, len);
2454572a70bSGyeyoung Baek 	st->buf_idx += len;
2464572a70bSGyeyoung Baek 
2474572a70bSGyeyoung Baek 	if (st->buf_idx == MHZ19B_CMD_SIZE) {
2484572a70bSGyeyoung Baek 		st->buf_idx = 0;
2494572a70bSGyeyoung Baek 		complete(&st->buf_ready);
2504572a70bSGyeyoung Baek 	}
2514572a70bSGyeyoung Baek 
2524572a70bSGyeyoung Baek 	return len;
2534572a70bSGyeyoung Baek }
2544572a70bSGyeyoung Baek 
2554572a70bSGyeyoung Baek static const struct serdev_device_ops mhz19b_ops = {
2564572a70bSGyeyoung Baek 	.receive_buf = mhz19b_receive_buf,
2574572a70bSGyeyoung Baek 	.write_wakeup = serdev_device_write_wakeup,
2584572a70bSGyeyoung Baek };
2594572a70bSGyeyoung Baek 
mhz19b_probe(struct serdev_device * serdev)2604572a70bSGyeyoung Baek static int mhz19b_probe(struct serdev_device *serdev)
2614572a70bSGyeyoung Baek {
2624572a70bSGyeyoung Baek 	int ret;
2634572a70bSGyeyoung Baek 	struct device *dev = &serdev->dev;
2644572a70bSGyeyoung Baek 	struct iio_dev *indio_dev;
2654572a70bSGyeyoung Baek 	struct mhz19b_state *st;
2664572a70bSGyeyoung Baek 
2674572a70bSGyeyoung Baek 	serdev_device_set_client_ops(serdev, &mhz19b_ops);
2684572a70bSGyeyoung Baek 	ret = devm_serdev_device_open(dev, serdev);
2694572a70bSGyeyoung Baek 	if (ret)
2704572a70bSGyeyoung Baek 		return ret;
2714572a70bSGyeyoung Baek 	serdev_device_set_baudrate(serdev, 9600);
2724572a70bSGyeyoung Baek 	serdev_device_set_flow_control(serdev, false);
2734572a70bSGyeyoung Baek 	ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
2744572a70bSGyeyoung Baek 	if (ret)
2754572a70bSGyeyoung Baek 		return ret;
2764572a70bSGyeyoung Baek 
2774572a70bSGyeyoung Baek 	indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
2784572a70bSGyeyoung Baek 	if (!indio_dev)
279*805bbd3aSDan Carpenter 		return -ENOMEM;
2804572a70bSGyeyoung Baek 	serdev_device_set_drvdata(serdev, indio_dev);
2814572a70bSGyeyoung Baek 
2824572a70bSGyeyoung Baek 	st = iio_priv(indio_dev);
2834572a70bSGyeyoung Baek 	st->serdev = serdev;
2844572a70bSGyeyoung Baek 
2854572a70bSGyeyoung Baek 	init_completion(&st->buf_ready);
2864572a70bSGyeyoung Baek 
2874572a70bSGyeyoung Baek 	ret = devm_regulator_get_enable(dev, "vin");
2884572a70bSGyeyoung Baek 	if (ret)
2894572a70bSGyeyoung Baek 		return ret;
2904572a70bSGyeyoung Baek 
2914572a70bSGyeyoung Baek 	indio_dev->name = "mh-z19b";
2924572a70bSGyeyoung Baek 	indio_dev->channels = mhz19b_channels;
2934572a70bSGyeyoung Baek 	indio_dev->num_channels = ARRAY_SIZE(mhz19b_channels);
2944572a70bSGyeyoung Baek 	indio_dev->info = &mhz19b_info;
2954572a70bSGyeyoung Baek 
2964572a70bSGyeyoung Baek 	return devm_iio_device_register(dev, indio_dev);
2974572a70bSGyeyoung Baek }
2984572a70bSGyeyoung Baek 
2994572a70bSGyeyoung Baek static const struct of_device_id mhz19b_of_match[] = {
3004572a70bSGyeyoung Baek 	{ .compatible = "winsen,mhz19b", },
3014572a70bSGyeyoung Baek 	{ }
3024572a70bSGyeyoung Baek };
3034572a70bSGyeyoung Baek MODULE_DEVICE_TABLE(of, mhz19b_of_match);
3044572a70bSGyeyoung Baek 
3054572a70bSGyeyoung Baek static struct serdev_device_driver mhz19b_driver = {
3064572a70bSGyeyoung Baek 	.driver = {
3074572a70bSGyeyoung Baek 		.name = "mhz19b",
3084572a70bSGyeyoung Baek 		.of_match_table = mhz19b_of_match,
3094572a70bSGyeyoung Baek 	},
3104572a70bSGyeyoung Baek 	.probe = mhz19b_probe,
3114572a70bSGyeyoung Baek };
3124572a70bSGyeyoung Baek module_serdev_device_driver(mhz19b_driver);
3134572a70bSGyeyoung Baek 
3144572a70bSGyeyoung Baek MODULE_AUTHOR("Gyeyoung Baek");
3154572a70bSGyeyoung Baek MODULE_DESCRIPTION("MH-Z19B CO2 sensor driver using serdev interface");
3164572a70bSGyeyoung Baek MODULE_LICENSE("GPL");
317