From: Jonathan Cameron <jic23@kernel.org>
To: Gyeyoung Baek <gye976@gmail.com>
Cc: linux-iio@vger.kernel.org, devicetree@vger.kernel.org,
lars@metafoo.de, gustavograzs@gmail.com,
javier.carrasco.cruz@gmail.com, robh@kernel.org,
krzk+dt@kernel.org, conor+dt@kernel.org
Subject: Re: [PATCH 1/3] iio: chemical: add support for winsen MHZ19B CO2 sensor
Date: Sun, 30 Mar 2025 15:04:10 +0100 [thread overview]
Message-ID: <20250330150410.23b148da@jic23-huawei> (raw)
In-Reply-To: <20250329164905.632491-2-gye976@gmail.com>
On Sun, 30 Mar 2025 01:49:03 +0900
Gyeyoung Baek <gye976@gmail.com> wrote:
> Add support for winsen MHZ19B CO2 sensor.
Hi,
Good to add a little more detail here.
>
Ideally add a
DataSheet tag here so we have a record in the git log on where to find
a datasheet.
Various comments inline.
The big stuff is that you are adding ABI without documentation.
Also that ABI doesn't seem that well aligned with existing calibration related
ABI.
> Signed-off-by: Gyeyoung Baek <gye976@gmail.com>
> ---
> drivers/iio/chemical/Kconfig | 6 +
> drivers/iio/chemical/Makefile | 1 +
> drivers/iio/chemical/mhz19b.c | 354 ++++++++++++++++++++++++++++++++++
> 3 files changed, 361 insertions(+)
> create mode 100644 drivers/iio/chemical/mhz19b.c
>
> diff --git a/drivers/iio/chemical/mhz19b.c b/drivers/iio/chemical/mhz19b.c
> new file mode 100644
> index 000000000000..de900131035b
> --- /dev/null
> +++ b/drivers/iio/chemical/mhz19b.c
> @@ -0,0 +1,354 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * mh-z19b co2 sensor driver
> + *
> + * Copyright (c) 2025 Gyeyoung Baek <gye976@gmail.com>
> + */
> +
> +#include <linux/module.h>
> +#include <linux/kernel.h>
> +#include <linux/init.h>
> +#include <linux/serdev.h>
> +#include <linux/of.h>
Shouldn't be needed here. I'd guess you want
mod_devicetable.h
> +#include <linux/device.h>
> +#include <linux/iio/iio.h>
> +#include <linux/mutex.h>
> +#include <linux/cleanup.h>
Alphabetical order preferred for includes. As it's an IIO driver
you can pull that out to a separate include block at the end if you
like. I'm fine with it in alphabetical order with the other headers
if you prefer that.
> +
> +struct mhz19b_state {
> + struct serdev_device *serdev;
> +
> + /* serdev receive buffer */
> + char buf[9];
> + int buf_idx;
> +
> + /* protect access to mhz19b_state */
Be more specific. I'd imagine it's the buffer rather
than the serdev pointer...
> + struct mutex lock;
> +};
> +
> +/* ABC logig on/off */
If the names of command defines are good then we don't need the comments.
I'd modify them a little to make that true here and drop the comments
unless they are adding something more
> +#define MHZ19B_ABC_LOGIC_CMD 0x79
> +/* read CO2 concentration */
> +#define MHZ19B_READ_CO2_CMD 0x86
> +/* calibrate Zero Point */
> +#define MHZ19B_ZERO_POINT_CMD 0x87
> +/* calibrate Span Point */
> +#define MHZ19B_SPAN_POINT_CMD 0x88
> +/* set sensor detection range */
> +#define MHZ19B_DETECTION_RANGE_CMD 0x99
}
> +
> +static int mhz19b_serdev_cmd(struct iio_dev *indio_dev, int cmd, const char *str)
> +{
> + int ret = 0;
> + struct serdev_device *serdev;
> + struct mhz19b_state *mhz19b;
> + struct device *dev;
> +
> + mhz19b = iio_priv(indio_dev);
> + serdev = mhz19b->serdev;
> + dev = &indio_dev->dev;
These can all be combined with declarations to save a few lines of code.
> +
> + /*
> + * commands have following format:
> + *
> + * +------+------+-----+------+------+------+------+------+-------+
> + * | 0xFF | 0x01 | cmd | arg0 | arg1 | 0x00 | 0x00 | 0x00 | cksum |
> + * +------+------+-----+------+------+------+------+------+-------+
> + */
> + uint8_t cmd_buf[MHZ19B_CMD_SIZE] = {
> + 0xFF, 0x01, cmd,
> + };
> +
> + switch (cmd) {
> + case MHZ19B_ABC_LOGIC_CMD:
> + {
I'd move the { to the line above.
> + bool enable;
> +
> + ret = kstrtobool(str, &enable);
> + if (ret)
> + return ret;
> +
> + cmd_buf[3] = enable ? 0xA0 : 0x00;
> + break;
> + }
> + case MHZ19B_SPAN_POINT_CMD:
> + {
> + uint16_t ppm;
> +
> + ret = kstrtou16(str, 10, &ppm);
> + if (ret)
> + return ret;
> +
> + /* at least 1000ppm */
> + if (ppm < 1000 || ppm > 5000) {
> + dev_dbg(&indio_dev->dev, "span point ppm should be 1000~5000");
> + return -EINVAL;
> + }
> +
> + cmd_buf[3] = ppm / 256;
> + cmd_buf[4] = ppm % 256;
That's an elaborate way of doing
unaligned_put_be16()
so use that instead as it's also clearly documenting what is going on.
> + break;
> + }
> + case MHZ19B_DETECTION_RANGE_CMD:
> + {
> + uint16_t range;
> +
> + ret = kstrtou16(str, 10, &range);
> + if (ret)
> + return ret;
> +
> + /* Detection Range should be 2000 or 5000 */
> + if (!(range == 2000 || range == 5000)) {
> + dev_dbg(&indio_dev->dev, "detection range should be 2000 or 5000");
> + return -EINVAL;
> + }
> +
> + cmd_buf[3] = range / 256;
> + cmd_buf[4] = range % 256;
Same as above.
> + break;
> + }
> + default:
> + break;
> + }
> + cmd_buf[MHZ19B_CMD_SIZE - 1] = mhz19b_get_checksum(cmd_buf);
> +
> + scoped_guard(mutex, &mhz19b->lock) {
> + ret = serdev_device_write(serdev, cmd_buf, MHZ19B_CMD_SIZE, 0);
> + mhz19b->buf_idx = 0;
> +
> + if (ret != MHZ19B_CMD_SIZE) {
> + dev_err(dev, "write err, %d bytes written", ret);
> + return -EINVAL;
> + }
> +
> + switch (cmd) {
> + case MHZ19B_READ_CO2_CMD:
> + if (mhz19b->buf[MHZ19B_CMD_SIZE - 1] != mhz19b_get_checksum(mhz19b->buf)) {
> + dev_err(dev, "checksum err");
> + return -EINVAL;
> + }
> +
> + ret = (mhz19b->buf[2] << 8) + mhz19b->buf[3];
That's an unaligned_get_be16() I think. If so use that instead of opencoding.
> + break;
> + default:
> + /* no response commands. */
Might as well return early in each of these cases.
> + ret = 0;
> + break;
> + }
> + }
> +
> + return ret;
> +}
> +
> +static int mhz19b_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
Trivial but for IIO I'd prefer we try to keep under 80 chars still when it does
not effect readability. Here adding a wrap after indio_dev doesn't make it harder
to read.
> + int *val, int *val2, long mask)
Align after (
> +{
> + struct mhz19b_state *mhz19b;
> + int ret;
> +
> + mhz19b = iio_priv(indio_dev);
struct mhz19b_state *mhz19b = iio_priv(indio_dev);
at the point of declaration above.
> +
> + ret = mhz19b_serdev_cmd(indio_dev, MHZ19B_READ_CO2_CMD, NULL);
> + if (ret < 0)
> + return ret;
> +
> + *val = ret;
> + return IIO_VAL_INT;
> +}
> +
> +static const struct iio_info mhz19b_info = {
> + .read_raw = mhz19b_read_raw,
> +};
> +
> +struct iio_chan_spec_ext_info mhz19b_co2_ext_info[] = {
> + {
> + .name = "zero_point",
This is custom ABI. Before we consider that in detail we
need documentation in
Documentation/ABI/testing/sysfs-bus-iio-mhz19b
It is much easier to review ABI with docs.
All 3 are direct commands to the device, so I've no idea from
what we have here on what they do.
Superficially this one looks like a calibration control.
There is existing ABI for that.
> + .write = mhz19b_zero_point_write,
> + },
> + {
> + .name = "span_point",
> + .write = mhz19b_span_point_write,
Also looks like calibration. See if you can come
up with ABI that matches with what we already have for calibration
of ADCs etc.
> + },
> + {
> + .name = "abc_logic",
Definitely not good logic. ABC is a term they made up as far
as i can tell. See if you can find existing ABI for this.
I think we have other controls for autonomous calibration cycles.
> + .write = mhz19b_abc_logic_write,
> + },
> + {}
{ }
preferred for IIO code.
> +};
> +
> +static const struct iio_chan_spec mhz19b_channels[] = {
> + {
> + .type = IIO_CONCENTRATION,
> + .channel2 = IIO_MOD_CO2,
> + .modified = 1,
> + .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
> +
> + .ext_info = mhz19b_co2_ext_info,
> + },
> +};
> +
> +static int mhz19b_core_probe(struct device *dev)
As below. This function isn't sufficiently complex to justify
a separate function.
> +{
> + int ret;
> +
> + struct serdev_device *serdev;
> + struct mhz19b_state *mhz19b;
> + struct iio_dev *indio_dev;
> +
> + indio_dev = devm_iio_device_alloc(dev, sizeof(struct mhz19b_state));
sizeof(*mhz19b));
> + if (indio_dev == NULL)
> + return ret;
> +
> + dev_set_drvdata(dev, indio_dev);
> +
> + mhz19b = iio_priv(indio_dev);
> +
> + mhz19b->buf_idx = 0;
No need to explicitly zero as it is allocated by kzalloc. Fine to
keep it though if you think it adds benefit as 'documentation'.
> + ret = devm_mutex_init(dev, &mhz19b->lock);
> + if (ret)
> + return ret;
> +
> + serdev = container_of(dev, struct serdev_device, dev);
breaking out the _core_probe() makes this more complex as in the
caller serdev is already available.
> +
> + mhz19b->serdev = serdev;
> +
> + indio_dev->name = "mh-z19b";
> + indio_dev->channels = mhz19b_channels;
> + indio_dev->num_channels = ARRAY_SIZE(mhz19b_channels);
> + indio_dev->info = &mhz19b_info;
> +
> + ret = devm_iio_device_register(dev, indio_dev);
return devm_iio_device_register()
> + if (ret)
> + return ret;
> +
> + return 0;
> +}
> +
> +static size_t mhz19b_receive_buf(struct serdev_device *serdev, const u8 *data, size_t len)
> +{
> + struct iio_dev *indio_dev;
struct iio_dev *indio_dev = dev_get_drvdata(&serdev->dev);
struct mhz19b_state *mhz19b = iio_priv(indio_dev);
to save a few lines.
> + struct mhz19b_state *mhz19b;
> +
> + indio_dev = dev_get_drvdata(&serdev->dev);
> + mhz19b = iio_priv(indio_dev);
> +
> + for (int i = 0; i < len; i++)
> + mhz19b->buf[mhz19b->buf_idx++] = data[i];
> +
> + return len;
> +}
> +
> +static void mhz19b_write_wakeup(struct serdev_device *serdev)
> +{
> + struct iio_dev *indio_dev;
> +
> + indio_dev = dev_get_drvdata(&serdev->dev);
> +
> + dev_dbg(&indio_dev->dev, "mhz19b_write_wakeup");
This doesn't do anything which makes me suspicious. Would
using serdev_device_write_wakeup() as the callback make
sense? I'm not that familiar with serial drivers but I can
see that a number of other drivers do that.
> +}
> +
> +static const struct serdev_device_ops mhz19b_ops = {
> + .receive_buf = mhz19b_receive_buf,
> + .write_wakeup = mhz19b_write_wakeup,
> +};
> +
> +static int mhz19b_probe(struct serdev_device *serdev)
> +{
> + int ret;
> +
> + struct device *dev;
> +
> + dev = &serdev->dev;
> + 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 < 0)
> + return ret;
Why check return value from this call but not the previous two?
I'm not immediately able to see a reason this is more likely to fail.
> +
> + ret = mhz19b_core_probe(dev);
Having a separate _core_probe() seems unnecessary. I'd jut have a single
probe function and put all that code inline here.
> + if (ret)
> + return ret;
> +
return mhz19b_core_probe();
> + return 0;
> +}
> +
> +static const struct of_device_id mhz19b_of_match[] = {
> + { .compatible = "winsen,mhz19b", },
> + {}
Trivial: I'm trying to standardize formatting of these in IIO
and made the random choice of
{ }
as the terminating entry style.
> +};
Similar to below, it is common practice to have no blank line
between this array of structs and the MODULE_DEVICE_TABLE
to reflect how tightly they are coupled.
> +
> +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,
> +};
> +
Typical style for these module* lines is to couple them
closely with the struct. That is done by having no blank line here.
> +module_serdev_device_driver(mhz19b_driver);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Gyeyoung Baek");
> +MODULE_DESCRIPTION("MH-Z19B CO2 sensor driver using serdev interface");
next prev parent reply other threads:[~2025-03-30 14:04 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-03-29 16:49 [PATCH 0/3] add support for winsen MHZ19B CO2 sensor Gyeyoung Baek
2025-03-29 16:49 ` [PATCH 1/3] iio: chemical: " Gyeyoung Baek
2025-03-30 14:04 ` Jonathan Cameron [this message]
2025-03-31 14:36 ` gyeyoung
2025-04-04 11:39 ` Jonathan Cameron
2025-03-29 16:49 ` [PATCH 2/3] dt-bindings: add device tree " Gyeyoung Baek
2025-03-30 9:39 ` Krzysztof Kozlowski
2025-03-31 1:11 ` gyeyoung
2025-03-30 12:50 ` Jonathan Cameron
2025-03-31 1:32 ` gyeyoung
2025-03-29 16:49 ` [PATCH 3/3] dt-bindings: add winsen to the vendor prefixes Gyeyoung Baek
2025-03-30 9:37 ` Krzysztof Kozlowski
2025-03-31 0:13 ` gyeyoung
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250330150410.23b148da@jic23-huawei \
--to=jic23@kernel.org \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=gustavograzs@gmail.com \
--cc=gye976@gmail.com \
--cc=javier.carrasco.cruz@gmail.com \
--cc=krzk+dt@kernel.org \
--cc=lars@metafoo.de \
--cc=linux-iio@vger.kernel.org \
--cc=robh@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox