Re: [PATCH v4 2/2] iio: adc: ti-ads112c04: Add support for TI ADS112C04

Kyle Hsieh <[email protected]>
Newsgroups org.kernel.vger.linux-iio,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel
Message-ID <CAF7HswP7+79Y5M92PVoHJ_H_J6rU=Q0p5qWDm1gcA2mPfg0VDw@mail.gmail.com>
Hi Andy,

Thanks for the detailed review.

On Tue, Aug 11, 2026 at 5:39 PM Andy Shevchenko
<[email protected]> wrote:
>
> On Tue, Aug 11, 2026 at 10:48:38AM +0800, Kyle Hsieh wrote:
> > Add IIO driver support for the Texas Instruments ADS112C04 (16-bit)
> > delta-sigma ADCs.
> >
> > The driver implements:
> > - Single-shot conversions using the IIO raw read interface.
> > - Dynamic parsing of single-ended and differential channels from
> >   device tree child nodes.
> > - Hardware interrupt support via the DRDY pin, falling back to
> >   software polling if no IRQ is provided.
> > - Scale calculation based on the internal 2.048V reference.
> > - Reference voltage scaling via the regulator subsystem (refp-supply),
> >   falling back to the internal 2.048V reference if not specified.
> >   refn-supply is not yet supported.
> > - Hardware reset fallback using GPIO.
>
> ...
>
> > +#include <linux/bitfield.h>
> > +#include <linux/bitops.h>
> > +#include <linux/delay.h>
> > +#include <linux/err.h>
> > +#include <linux/gpio/consumer.h>
> > +#include <linux/i2c.h>
> > +#include <linux/interrupt.h>
> > +#include <linux/iopoll.h>
> > +#include <linux/jiffies.h>
> > +#include <linux/module.h>
> > +#include <linux/mutex.h>
> > +#include <linux/property.h>
> > +#include <linux/regulator/consumer.h>
> > +#include <linux/string.h>
> > +#include <linux/types.h>
> > +#include <linux/units.h>
>
> ...
>
> > +static int ads112c04_wait_for_data(struct ads112c04_state *st)
> > +{
> > +     int ret, err;
> > +     u8 val;
> > +
> > +     if (st->client->irq > 0) {
> > +             /* Timeout is 100ms (slowest data rate is 20 SPS) */
> > +             ret = wait_for_completion_timeout(&st->completion,
> > +                                               msecs_to_jiffies(100));
> > +             if (!ret)
>
> In this case semantics of ret differs, that's why it's better to write as
>
>                 if (!wait_for_completion_timeout(&st->completion, msecs_to_jiffies(100)))
>
> // and I would even dare to put on a single line.
>
> > +                     return -ETIMEDOUT;
> > +
> > +             return 0;
> > +     }
> > +
> > +     err = read_poll_timeout(ads112c04_read_reg, ret,
> > +                             (ret < 0 || (val & ADS112C04_CONFIG2_DRDY)),
> > +                             1000, 100 * USEC_PER_MSEC, false,
> > +                             st->client, ADS112C04_REG_CONFIG2, &val);
> > +
> > +     if (ret < 0)
> > +             return ret;
> > +
> > +     return err;
>
> In this piece I would swap err and ret, so the ret is outer one and err is
> the inner one. This will be consistent with other code pieces.
>
> > +}
>
> ...
>
> > +static int ads112c04_get_adc_result(struct ads112c04_state *st,
> > +                                 struct iio_chan_spec const *chan,
> > +                                 int *val)
> > +{
> > +     u8 new_config0;
> > +     int ret;
> > +
> > +     new_config0 = st->config0;
> > +     FIELD_MODIFY(ADS112C04_CONFIG0_MUX, &new_config0, chan->address);
> > +
> > +     if (st->config0 != new_config0) {
> > +             ret = ads112c04_write_reg(st->client, ADS112C04_REG_CONFIG0, new_config0);
> > +             if (ret < 0)
> > +                     return ret;
> > +             st->config0 = new_config0;
> > +     }
> > +
> > +     reinit_completion(&st->completion);
> > +
> > +     ret = ads112c04_write_cmd(st->client, ADS112C04_CMD_START_SYNC);
> > +     if (ret < 0)
> > +             return ret;
> > +
> > +     ret = ads112c04_wait_for_data(st);
> > +     if (ret < 0)
> > +             return ret;
> > +
> > +     ret = ads112c04_read_data(st, val);
>
> > +     if (st->client->irq > 0)
> > +             enable_irq(st->client->irq);
>
> Why is it fine to leave IRQ enabled even in the error case?
>
> > +     return ret;
> > +}
>
> ...
>
> > +static int ads112c04_read_raw(struct iio_dev *indio_dev,
> > +                           struct iio_chan_spec const *chan,
> > +                           int *val, int *val2, long mask)
> > +{
> > +     struct ads112c04_state *st = iio_priv(indio_dev);
> > +     int ret;
> > +
> > +     switch (mask) {
> > +     case IIO_CHAN_INFO_RAW:
> > +             mutex_lock(&st->lock);
> > +             ret = ads112c04_get_adc_result(st, chan, val);
> > +             mutex_unlock(&st->lock);
> > +
> > +             if (ret < 0)
> > +                     return ret;
>
> If IRQ is left enabled and we call it here, we end up with the unbalanced
> depth counting.
>
> > +             return IIO_VAL_INT;
> > +
> > +     case IIO_CHAN_INFO_SCALE:
> > +             *val = st->vref_mV;
> > +             *val2 = 15;
> > +             return IIO_VAL_FRACTIONAL_LOG2;
> > +
> > +     default:
> > +             return -EINVAL;
> > +     }
> > +}
>
> ...
>
> > +static irqreturn_t ads112c04_irq_handler(int irq, void *private)
> > +{
> > +     struct iio_dev *indio_dev = private;
> > +     struct ads112c04_state *st = iio_priv(indio_dev);
>
> > +     disable_irq_nosync(irq);
>
> This is unconditionally called. Where is the guarantee that it becomes enabled
> once again?
>
> > +     complete(&st->completion);
>
> > +     return IRQ_HANDLED;
> > +}
>
> ...
>
> > +static int ads112c04_parse_channels(struct iio_dev *indio_dev)
> > +{
> > +     struct device *dev = indio_dev->dev.parent;
> > +     struct ads112c04_state *st = iio_priv(indio_dev);
> > +     struct iio_chan_spec *channels;
> > +     u32 num_channels, pair[2];
>
> > +     int ret, i = 0;
>
> Why is 'i' signed? And it's better to decouple definition and assignment, so
> the assignment will happen closer to when it's really needed.
>
> ...
>
> > +             if (fwnode_property_present(child, "reference-sources")) {
> > +                     const char *ref;
> > +
> > +                     ret = fwnode_property_read_string(child, "reference-sources", &ref);
> > +                     if (ret)
> > +                             return dev_err_probe(dev, ret,
> > +                                                  "failed to read reference-sources\n");
> > +
> > +                     if ((!strcmp(ref, "external") && !st->has_refp) ||
> > +                         (!strcmp(ref, "internal") && st->has_refp))
> > +                             return dev_err_probe(dev, -EINVAL,
> > +                                                  "reference-sources does not match refp-supply\n");
> > +             }
>
> Reinvention of fwnode_property_match_property_string() ?
>
> ...
>
> > +             if (fwnode_property_present(child, "single-channel")) {
> > +                     ret = fwnode_property_read_u32(child, "single-channel", &pair[0]);
>
> I don't like the (partial) pair reuse here. It's semantically wrong. Just add
> another temporary variable and let compiler to choose what to do with a stack
> frame in such a case.
>
> > +                     if (ret)
> > +                             return dev_err_probe(dev, ret,
> > +                                                  "failed to read single-channel property\n");
> > +
> > +                     if (pair[0] > 3)
> > +                             return dev_err_probe(dev, -EINVAL,
> > +                                                  "single-channel must be 0-3\n");
> > +
> > +                     spec->channel = pair[0];
> > +                     spec->address = 0x08 + pair[0];
> > +             } else if (fwnode_property_present(child, "diff-channels")) {
> > +                     ret = fwnode_property_read_u32_array(child, "diff-channels", pair, 2);
>
> ARRAY_SIZE()
>
> > +                     if (ret)
> > +                             return dev_err_probe(dev, ret,
> > +                                                  "failed to read diff-channels property\n");
> > +
> > +                     if (pair[0] > 3 || pair[1] > 3)
> > +                             return dev_err_probe(dev, -EINVAL,
> > +                                                  "diff-channels must be 0-3\n");
> > +
> > +                     spec->channel = pair[0];
> > +                     spec->channel2 = pair[1];
> > +                     spec->differential = 1;
>
> > +                     if (pair[0] == 0 && pair[1] == 1)
> > +                             spec->address = 0x00;
> > +                     else if (pair[0] == 0 && pair[1] == 2)
> > +                             spec->address = 0x01;
> > +                     else if (pair[0] == 0 && pair[1] == 3)
> > +                             spec->address = 0x02;
> > +                     else if (pair[0] == 1 && pair[1] == 0)
> > +                             spec->address = 0x03;
> > +                     else if (pair[0] == 1 && pair[1] == 2)
> > +                             spec->address = 0x04;
> > +                     else if (pair[0] == 1 && pair[1] == 3)
> > +                             spec->address = 0x05;
> > +                     else if (pair[0] == 2 && pair[1] == 3)
> > +                             spec->address = 0x06;
> > +                     else if (pair[0] == 3 && pair[1] == 2)
> > +                             spec->address = 0x07;
>
> I would do this as a 4x4 table
>
>         -1, 0, 1, 2,
>         3, -1, 4, 5,
>         -1, -1, -1, 6,
>         -1, -1, 7, -1,
>
> With that done you can even supported the swapped cases
>
>         -1, 0, 1, 2,
>         3, -1, 4, 5,
>         1, 4, -1, 6,
>         2, 5, 7, -1,
>
> (but I haven't studied the code if it's toughly relies on the pair[0]/pair[1]
>  values to be in a strong order after the address being assigned).
I will apply your first 4x4 table for diff-channels - it maps directly to
the MUX encoding and is much more readable than the if-else chain.

I did not apply the second (swapped) table. Entries like [2][0] would
map to MUX value 1, which is AINP=AIN0/AINN=AIN2, i.e. the opposite
polarity, so it would silently return negated values. Supporting swapped
pairs properly would need a per-channel inversion flag and negation
after conversion, which I'd rather leave out of this initial minimal
implementation.
>
> > +                     else
> > +                             return dev_err_probe(dev, -EINVAL,
> > +                                                  "invalid diff-channels combination\n");
>
> > +             } else {
> > +                     return dev_err_probe(dev, -EINVAL,
> > +                                          "channel node must have single-channel or diff-channels\n");
> > +             }
> > +
> > +             i++;
> > +     }
> > +
> > +     indio_dev->channels = channels;
> > +     indio_dev->num_channels = i;
> > +
> > +     return 0;
> > +}
>
> ...
>
> > +#define ADS112C04_VREF_INTERNAL_MV   2048
>
> _mV
>
> ...
>
> > +     if (device_property_present(dev, "refp-supply")) {
>
> A dup property check. if (st->has_refp) should suffice, no?
>
> > +             ret = devm_regulator_get_enable_read_voltage(dev, "refp");
> > +             if (ret < 0)
> > +                     return dev_err_probe(dev, ret,
> > +                                          "failed to get refp voltage\n");
> > +
> > +             st->vref_mV = ret / (MICRO / MILLI);
> > +             st->config1 = 0x02;
> > +     } else {
> > +             st->vref_mV = ADS112C04_VREF_INTERNAL_MV;
> > +             st->config1 = 0x00;
> > +     }
>
> ...
>
> > +     /* Requesting OUT_HIGH asserts the active-low reset pin immediately */
> > +     reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
> > +     if (IS_ERR(reset_gpio))
> > +             return PTR_ERR(reset_gpio);
>
> Why reset-gpio driver can't be used instead?
David suggested the reset controller framework on v1, but after I moved
to gpiod in v2 he suggested GPIOD_OUT_HIGH to simplify it, so I kept
gpiod. Switching now would mean changing the binding, which already has
a Reviewed-by - happy to do it if you both prefer.

Best regards,
Kyle Hsieh
>
> > +     if (reset_gpio) {
> > +             fsleep(1000);
>
> 1 * USEC_PER_MSEC
>
> > +             gpiod_set_value_cansleep(reset_gpio, 0);
> > +     } else {
> > +             ret = ads112c04_write_cmd(client, ADS112C04_CMD_RESET);
> > +             if (ret < 0)
> > +                     return ret;
> > +     }
> > +
> > +     fsleep(1000);
>
> Ditto.
>
> --
> With Best Regards,
> Andy Shevchenko
>
>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.