xref: /linux/drivers/iio/chemical/mhz19b.c (revision c26f4fbd58375bd6ef74f95eb73d61762ad97c59)
1 // SPDX-License-Identifier: GPL-2.0
2 /*
3  * mh-z19b CO₂ sensor driver
4  *
5  * Copyright (c) 2025 Gyeyoung Baek <gye976@gmail.com>
6  *
7  * Datasheet:
8  * https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf
9  */
10 
11 #include <linux/array_size.h>
12 #include <linux/completion.h>
13 #include <linux/device.h>
14 #include <linux/errno.h>
15 #include <linux/iio/iio.h>
16 #include <linux/iio/sysfs.h>
17 #include <linux/jiffies.h>
18 #include <linux/kstrtox.h>
19 #include <linux/minmax.h>
20 #include <linux/mod_devicetable.h>
21 #include <linux/module.h>
22 #include <linux/regulator/consumer.h>
23 #include <linux/serdev.h>
24 #include <linux/string.h>
25 #include <linux/types.h>
26 #include <linux/unaligned.h>
27 
28 /*
29  * Commands have following format:
30  *
31  * +------+------+-----+------+------+------+------+------+-------+
32  * | 0xFF | 0x01 | cmd | arg0 | arg1 | 0x00 | 0x00 | 0x00 | cksum |
33  * +------+------+-----+------+------+------+------+------+-------+
34  */
35 #define MHZ19B_CMD_SIZE 9
36 
37 /* ABC logic in MHZ19B means auto calibration. */
38 #define MHZ19B_ABC_LOGIC_CMD		0x79
39 #define MHZ19B_READ_CO2_CMD		0x86
40 #define MHZ19B_SPAN_POINT_CMD		0x88
41 #define MHZ19B_ZERO_POINT_CMD		0x87
42 
43 #define MHZ19B_SPAN_POINT_PPM_MIN	1000
44 #define MHZ19B_SPAN_POINT_PPM_MAX	5000
45 
46 #define MHZ19B_SERDEV_TIMEOUT msecs_to_jiffies(100)
47 
48 struct mhz19b_state {
49 	struct serdev_device *serdev;
50 
51 	/* Must wait until the 'buf' is filled with 9 bytes.*/
52 	struct completion buf_ready;
53 
54 	u8 buf_idx;
55 	/*
56 	 * Serdev receive buffer.
57 	 * When data is received from the MH-Z19B,
58 	 * the 'mhz19b_receive_buf' callback function is called and fills this buffer.
59 	 */
60 	u8 buf[MHZ19B_CMD_SIZE] __aligned(IIO_DMA_MINALIGN);
61 };
62 
mhz19b_get_checksum(u8 * cmd_buf)63 static u8 mhz19b_get_checksum(u8 *cmd_buf)
64 {
65 	u8 i, checksum = 0;
66 
67 /*
68  * +------+------+-----+------+------+------+------+------+-------+
69  * | 0xFF | 0x01 | cmd | arg0 | arg1 | 0x00 | 0x00 | 0x00 | cksum |
70  * +------+------+-----+------+------+------+------+------+-------+
71  *	     i:1    2      3      4      5      6      7
72  *
73  *  Sum all cmd_buf elements from index 1 to 7.
74  */
75 	for (i = 1; i < 8; i++)
76 		checksum += cmd_buf[i];
77 
78 	return -checksum;
79 }
80 
mhz19b_serdev_cmd(struct iio_dev * indio_dev,int cmd,u16 arg)81 static int mhz19b_serdev_cmd(struct iio_dev *indio_dev, int cmd, u16 arg)
82 {
83 	struct mhz19b_state *st = iio_priv(indio_dev);
84 	struct serdev_device *serdev = st->serdev;
85 	struct device *dev = &indio_dev->dev;
86 	int ret;
87 
88 	/*
89 	 * cmd_buf[3,4] : arg0,1
90 	 * cmd_buf[8]	: checksum
91 	 */
92 	u8 cmd_buf[MHZ19B_CMD_SIZE] = {
93 		0xFF, 0x01, cmd,
94 	};
95 
96 	switch (cmd) {
97 	case MHZ19B_ABC_LOGIC_CMD:
98 		cmd_buf[3] = arg ? 0xA0 : 0;
99 		break;
100 	case MHZ19B_SPAN_POINT_CMD:
101 		put_unaligned_be16(arg, &cmd_buf[3]);
102 		break;
103 	default:
104 		break;
105 	}
106 	cmd_buf[8] = mhz19b_get_checksum(cmd_buf);
107 
108 	/* Write buf to uart ctrl synchronously */
109 	ret = serdev_device_write(serdev, cmd_buf, MHZ19B_CMD_SIZE, 0);
110 	if (ret < 0)
111 		return ret;
112 	if (ret != MHZ19B_CMD_SIZE)
113 		return -EIO;
114 
115 	switch (cmd) {
116 	case MHZ19B_READ_CO2_CMD:
117 		ret = wait_for_completion_interruptible_timeout(&st->buf_ready,
118 			MHZ19B_SERDEV_TIMEOUT);
119 		if (ret < 0)
120 			return ret;
121 		if (!ret)
122 			return -ETIMEDOUT;
123 
124 		if (st->buf[8] != mhz19b_get_checksum(st->buf)) {
125 			dev_err(dev, "checksum err");
126 			return -EINVAL;
127 		}
128 
129 		return get_unaligned_be16(&st->buf[2]);
130 	default:
131 		/* No response commands. */
132 		return 0;
133 	}
134 }
135 
mhz19b_read_raw(struct iio_dev * indio_dev,struct iio_chan_spec const * chan,int * val,int * val2,long mask)136 static int mhz19b_read_raw(struct iio_dev *indio_dev,
137 			   struct iio_chan_spec const *chan,
138 			   int *val, int *val2, long mask)
139 {
140 	int ret;
141 
142 	ret = mhz19b_serdev_cmd(indio_dev, MHZ19B_READ_CO2_CMD, 0);
143 	if (ret < 0)
144 		return ret;
145 
146 	*val = ret;
147 	return IIO_VAL_INT;
148 }
149 
150 /*
151  * echo 0 > calibration_auto_enable : ABC logic off
152  * echo 1 > calibration_auto_enable : ABC logic on
153  */
calibration_auto_enable_store(struct device * dev,struct device_attribute * attr,const char * buf,size_t len)154 static ssize_t calibration_auto_enable_store(struct device *dev,
155 					     struct device_attribute *attr,
156 					     const char *buf, size_t len)
157 {
158 	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
159 	bool enable;
160 	int ret;
161 
162 	ret = kstrtobool(buf, &enable);
163 	if (ret)
164 		return ret;
165 
166 	ret = mhz19b_serdev_cmd(indio_dev, MHZ19B_ABC_LOGIC_CMD, enable);
167 	if (ret < 0)
168 		return ret;
169 
170 	return len;
171 }
172 static IIO_DEVICE_ATTR_WO(calibration_auto_enable, 0);
173 
174 /*
175  * echo 0 > calibration_forced_value		 : zero point calibration
176  *	(make sure the sensor has been working under 400ppm for over 20 minutes.)
177  * echo [1000 1 5000] > calibration_forced_value : span point calibration
178  *	(make sure the sensor has been working under a certain level CO₂ for over 20 minutes.)
179  */
calibration_forced_value_store(struct device * dev,struct device_attribute * attr,const char * buf,size_t len)180 static ssize_t calibration_forced_value_store(struct device *dev,
181 					      struct device_attribute *attr,
182 					      const char *buf, size_t len)
183 {
184 	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
185 	u16 ppm;
186 	int cmd, ret;
187 
188 	ret = kstrtou16(buf, 0, &ppm);
189 	if (ret)
190 		return ret;
191 
192 	if (ppm) {
193 		if (!in_range(ppm, MHZ19B_SPAN_POINT_PPM_MIN,
194 			MHZ19B_SPAN_POINT_PPM_MAX - MHZ19B_SPAN_POINT_PPM_MIN + 1)) {
195 			dev_dbg(&indio_dev->dev,
196 				"span point ppm should be in a range [%d-%d]\n",
197 				MHZ19B_SPAN_POINT_PPM_MIN, MHZ19B_SPAN_POINT_PPM_MAX);
198 			return -EINVAL;
199 		}
200 
201 		cmd = MHZ19B_SPAN_POINT_CMD;
202 	} else {
203 		cmd = MHZ19B_ZERO_POINT_CMD;
204 	}
205 
206 	ret = mhz19b_serdev_cmd(indio_dev, cmd, ppm);
207 	if (ret < 0)
208 		return ret;
209 
210 	return len;
211 }
212 static IIO_DEVICE_ATTR_WO(calibration_forced_value, 0);
213 
214 static struct attribute *mhz19b_attrs[] = {
215 	&iio_dev_attr_calibration_auto_enable.dev_attr.attr,
216 	&iio_dev_attr_calibration_forced_value.dev_attr.attr,
217 	NULL
218 };
219 
220 static const struct attribute_group mhz19b_attr_group = {
221 	.attrs = mhz19b_attrs,
222 };
223 
224 static const struct iio_info mhz19b_info = {
225 	.attrs = &mhz19b_attr_group,
226 	.read_raw = mhz19b_read_raw,
227 };
228 
229 static const struct iio_chan_spec mhz19b_channels[] = {
230 	{
231 		.type = IIO_CONCENTRATION,
232 		.channel2 = IIO_MOD_CO2,
233 		.modified = 1,
234 		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
235 	},
236 };
237 
mhz19b_receive_buf(struct serdev_device * serdev,const u8 * data,size_t len)238 static size_t mhz19b_receive_buf(struct serdev_device *serdev,
239 			      const u8 *data, size_t len)
240 {
241 	struct iio_dev *indio_dev = dev_get_drvdata(&serdev->dev);
242 	struct mhz19b_state *st = iio_priv(indio_dev);
243 
244 	memcpy(st->buf + st->buf_idx, data, len);
245 	st->buf_idx += len;
246 
247 	if (st->buf_idx == MHZ19B_CMD_SIZE) {
248 		st->buf_idx = 0;
249 		complete(&st->buf_ready);
250 	}
251 
252 	return len;
253 }
254 
255 static const struct serdev_device_ops mhz19b_ops = {
256 	.receive_buf = mhz19b_receive_buf,
257 	.write_wakeup = serdev_device_write_wakeup,
258 };
259 
mhz19b_probe(struct serdev_device * serdev)260 static int mhz19b_probe(struct serdev_device *serdev)
261 {
262 	int ret;
263 	struct device *dev = &serdev->dev;
264 	struct iio_dev *indio_dev;
265 	struct mhz19b_state *st;
266 
267 	serdev_device_set_client_ops(serdev, &mhz19b_ops);
268 	ret = devm_serdev_device_open(dev, serdev);
269 	if (ret)
270 		return ret;
271 	serdev_device_set_baudrate(serdev, 9600);
272 	serdev_device_set_flow_control(serdev, false);
273 	ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
274 	if (ret)
275 		return ret;
276 
277 	indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
278 	if (!indio_dev)
279 		return -ENOMEM;
280 	serdev_device_set_drvdata(serdev, indio_dev);
281 
282 	st = iio_priv(indio_dev);
283 	st->serdev = serdev;
284 
285 	init_completion(&st->buf_ready);
286 
287 	ret = devm_regulator_get_enable(dev, "vin");
288 	if (ret)
289 		return ret;
290 
291 	indio_dev->name = "mh-z19b";
292 	indio_dev->channels = mhz19b_channels;
293 	indio_dev->num_channels = ARRAY_SIZE(mhz19b_channels);
294 	indio_dev->info = &mhz19b_info;
295 
296 	return devm_iio_device_register(dev, indio_dev);
297 }
298 
299 static const struct of_device_id mhz19b_of_match[] = {
300 	{ .compatible = "winsen,mhz19b", },
301 	{ }
302 };
303 MODULE_DEVICE_TABLE(of, mhz19b_of_match);
304 
305 static struct serdev_device_driver mhz19b_driver = {
306 	.driver = {
307 		.name = "mhz19b",
308 		.of_match_table = mhz19b_of_match,
309 	},
310 	.probe = mhz19b_probe,
311 };
312 module_serdev_device_driver(mhz19b_driver);
313 
314 MODULE_AUTHOR("Gyeyoung Baek");
315 MODULE_DESCRIPTION("MH-Z19B CO2 sensor driver using serdev interface");
316 MODULE_LICENSE("GPL");
317