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