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