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 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 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 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 */ 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 */ 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 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 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