Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

iio: chemical: Add support for Winsen MHZ19B CO2 sensor

Add support for Winsen MHZ19B CO2 sensor.

Datasheet: https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf
Signed-off-by: Gyeyoung Baek <gye976@gmail.com>
Reviewed-by: Andy Shevchenko <andy@kernel.org>
Link: https://patch.msgid.link/20250423194100.53934-4-gye976@gmail.com
Signed-off-by: Jonathan Cameron <Jonathan.Cameron@huawei.com>

authored by

Gyeyoung Baek and committed by
Jonathan Cameron
4572a70b fd3730b2

+327
+10
drivers/iio/chemical/Kconfig
··· 108 108 iAQ-Core Continuous/Pulsed VOC (Volatile Organic Compounds) 109 109 sensors 110 110 111 + config MHZ19B 112 + tristate "Winsen MHZ19B CO2 sensor" 113 + depends on SERIAL_DEV_BUS 114 + help 115 + Say Y here to build Serdev interface support for the Winsen 116 + MHZ19B CO2 sensor. 117 + 118 + To compile this driver as a module, choose M here: the module will 119 + be called mhz19b. 120 + 111 121 config PMS7003 112 122 tristate "Plantower PMS7003 particulate matter sensor" 113 123 depends on SERIAL_DEV_BUS
+1
drivers/iio/chemical/Makefile
··· 15 15 obj-$(CONFIG_ENS160_I2C) += ens160_i2c.o 16 16 obj-$(CONFIG_ENS160_SPI) += ens160_spi.o 17 17 obj-$(CONFIG_IAQCORE) += ams-iaq-core.o 18 + obj-$(CONFIG_MHZ19B) += mhz19b.o 18 19 obj-$(CONFIG_PMS7003) += pms7003.o 19 20 obj-$(CONFIG_SCD30_CORE) += scd30_core.o 20 21 obj-$(CONFIG_SCD30_I2C) += scd30_i2c.o
+316
drivers/iio/chemical/mhz19b.c
··· 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 ret; 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");