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