// SPDX-License-Identifier: GPL-2.0 /* * mh-z19b CO₂ sensor driver * * Copyright (c) 2025 Gyeyoung Baek * * Datasheet: * https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* * Commands have following format: * * +------+------+-----+------+------+------+------+------+-------+ * | 0xFF | 0x01 | cmd | arg0 | arg1 | 0x00 | 0x00 | 0x00 | cksum | * +------+------+-----+------+------+------+------+------+-------+ */ #define MHZ19B_CMD_SIZE 9 /* ABC logic in MHZ19B means auto calibration. */ #define MHZ19B_ABC_LOGIC_CMD 0x79 #define MHZ19B_READ_CO2_CMD 0x86 #define MHZ19B_SPAN_POINT_CMD 0x88 #define MHZ19B_ZERO_POINT_CMD 0x87 #define MHZ19B_SPAN_POINT_PPM_MIN 1000 #define MHZ19B_SPAN_POINT_PPM_MAX 5000 #define MHZ19B_SERDEV_TIMEOUT msecs_to_jiffies(100) struct mhz19b_state { struct serdev_device *serdev; /* Must wait until the 'buf' is filled with 9 bytes.*/ struct completion buf_ready; u8 buf_idx; /* * Serdev receive buffer. * When data is received from the MH-Z19B, * the 'mhz19b_receive_buf' callback function is called and fills this buffer. */ u8 buf[MHZ19B_CMD_SIZE] __aligned(IIO_DMA_MINALIGN); }; static u8 mhz19b_get_checksum(u8 *cmd_buf) { u8 i, checksum = 0; /* * +------+------+-----+------+------+------+------+------+-------+ * | 0xFF | 0x01 | cmd | arg0 | arg1 | 0x00 | 0x00 | 0x00 | cksum | * +------+------+-----+------+------+------+------+------+-------+ * i:1 2 3 4 5 6 7 * * Sum all cmd_buf elements from index 1 to 7. */ for (i = 1; i < 8; i++) checksum += cmd_buf[i]; return -checksum; } static int mhz19b_serdev_cmd(struct iio_dev *indio_dev, int cmd, u16 arg) { struct mhz19b_state *st = iio_priv(indio_dev); struct serdev_device *serdev = st->serdev; struct device *dev = &indio_dev->dev; int ret; /* * cmd_buf[3,4] : arg0,1 * cmd_buf[8] : checksum */ u8 cmd_buf[MHZ19B_CMD_SIZE] = { 0xFF, 0x01, cmd, }; switch (cmd) { case MHZ19B_ABC_LOGIC_CMD: cmd_buf[3] = arg ? 0xA0 : 0; break; case MHZ19B_SPAN_POINT_CMD: put_unaligned_be16(arg, &cmd_buf[3]); break; default: break; } cmd_buf[8] = mhz19b_get_checksum(cmd_buf); /* Write buf to uart ctrl synchronously */ ret = serdev_device_write(serdev, cmd_buf, MHZ19B_CMD_SIZE, 0); if (ret < 0) return ret; if (ret != MHZ19B_CMD_SIZE) return -EIO; switch (cmd) { case MHZ19B_READ_CO2_CMD: ret = wait_for_completion_interruptible_timeout(&st->buf_ready, MHZ19B_SERDEV_TIMEOUT); if (ret < 0) return ret; if (!ret) return -ETIMEDOUT; if (st->buf[8] != mhz19b_get_checksum(st->buf)) { dev_err(dev, "checksum err"); return -EINVAL; } return get_unaligned_be16(&st->buf[2]); default: /* No response commands. */ return 0; } } static int mhz19b_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val, int *val2, long mask) { int ret; ret = mhz19b_serdev_cmd(indio_dev, MHZ19B_READ_CO2_CMD, 0); if (ret < 0) return ret; *val = ret; return IIO_VAL_INT; } /* * echo 0 > calibration_auto_enable : ABC logic off * echo 1 > calibration_auto_enable : ABC logic on */ static ssize_t calibration_auto_enable_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t len) { struct iio_dev *indio_dev = dev_to_iio_dev(dev); bool enable; int ret; ret = kstrtobool(buf, &enable); if (ret) return ret; ret = mhz19b_serdev_cmd(indio_dev, MHZ19B_ABC_LOGIC_CMD, enable); if (ret < 0) return ret; return len; } static IIO_DEVICE_ATTR_WO(calibration_auto_enable, 0); /* * echo 0 > calibration_forced_value : zero point calibration * (make sure the sensor has been working under 400ppm for over 20 minutes.) * echo [1000 1 5000] > calibration_forced_value : span point calibration * (make sure the sensor has been working under a certain level CO₂ for over 20 minutes.) */ static ssize_t calibration_forced_value_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t len) { struct iio_dev *indio_dev = dev_to_iio_dev(dev); u16 ppm; int cmd, ret; ret = kstrtou16(buf, 0, &ppm); if (ret) return ret; if (ppm) { if (!in_range(ppm, MHZ19B_SPAN_POINT_PPM_MIN, MHZ19B_SPAN_POINT_PPM_MAX - MHZ19B_SPAN_POINT_PPM_MIN + 1)) { dev_dbg(&indio_dev->dev, "span point ppm should be in a range [%d-%d]\n", MHZ19B_SPAN_POINT_PPM_MIN, MHZ19B_SPAN_POINT_PPM_MAX); return -EINVAL; } cmd = MHZ19B_SPAN_POINT_CMD; } else { cmd = MHZ19B_ZERO_POINT_CMD; } ret = mhz19b_serdev_cmd(indio_dev, cmd, ppm); if (ret < 0) return ret; return len; } static IIO_DEVICE_ATTR_WO(calibration_forced_value, 0); static struct attribute *mhz19b_attrs[] = { &iio_dev_attr_calibration_auto_enable.dev_attr.attr, &iio_dev_attr_calibration_forced_value.dev_attr.attr, NULL }; static const struct attribute_group mhz19b_attr_group = { .attrs = mhz19b_attrs, }; static const struct iio_info mhz19b_info = { .attrs = &mhz19b_attr_group, .read_raw = mhz19b_read_raw, }; static const struct iio_chan_spec mhz19b_channels[] = { { .type = IIO_CONCENTRATION, .channel2 = IIO_MOD_CO2, .modified = 1, .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), }, }; static size_t mhz19b_receive_buf(struct serdev_device *serdev, const u8 *data, size_t len) { struct iio_dev *indio_dev = dev_get_drvdata(&serdev->dev); struct mhz19b_state *st = iio_priv(indio_dev); memcpy(st->buf + st->buf_idx, data, len); st->buf_idx += len; if (st->buf_idx == MHZ19B_CMD_SIZE) { st->buf_idx = 0; complete(&st->buf_ready); } return len; } static const struct serdev_device_ops mhz19b_ops = { .receive_buf = mhz19b_receive_buf, .write_wakeup = serdev_device_write_wakeup, }; static int mhz19b_probe(struct serdev_device *serdev) { int ret; struct device *dev = &serdev->dev; struct iio_dev *indio_dev; struct mhz19b_state *st; serdev_device_set_client_ops(serdev, &mhz19b_ops); ret = devm_serdev_device_open(dev, serdev); if (ret) return ret; serdev_device_set_baudrate(serdev, 9600); serdev_device_set_flow_control(serdev, false); ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE); if (ret) return ret; indio_dev = devm_iio_device_alloc(dev, sizeof(*st)); if (!indio_dev) return -ENOMEM; serdev_device_set_drvdata(serdev, indio_dev); st = iio_priv(indio_dev); st->serdev = serdev; init_completion(&st->buf_ready); ret = devm_regulator_get_enable(dev, "vin"); if (ret) return ret; indio_dev->name = "mh-z19b"; indio_dev->channels = mhz19b_channels; indio_dev->num_channels = ARRAY_SIZE(mhz19b_channels); indio_dev->info = &mhz19b_info; return devm_iio_device_register(dev, indio_dev); } static const struct of_device_id mhz19b_of_match[] = { { .compatible = "winsen,mhz19b", }, { } }; MODULE_DEVICE_TABLE(of, mhz19b_of_match); static struct serdev_device_driver mhz19b_driver = { .driver = { .name = "mhz19b", .of_match_table = mhz19b_of_match, }, .probe = mhz19b_probe, }; module_serdev_device_driver(mhz19b_driver); MODULE_AUTHOR("Gyeyoung Baek"); MODULE_DESCRIPTION("MH-Z19B CO2 sensor driver using serdev interface"); MODULE_LICENSE("GPL");