Re: [PATCH 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 <CAF7HswNFg2DkD2Wif=9Hv3cyak5UVdBOUKhkH-iYBJ0NMaTxGA@mail.gmail.com>
Hi David,

Thank you for the detailed review and for pointing out the modern kernel APIs.

On Tue, Jul 28, 2026 at 11:21 PM David Lechner <[email protected]> wrote:
>
> On 7/28/26 3:01 AM, 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.
> > - MUX configuration for single-ended and differential channels.
> > - Hardware interrupt support via the DRDY pin, falling back to
> >   software polling of the configuration register if no IRQ is provided.
> > - Scale calculation based on the internal 2.048V reference.
> > - Dynamic reference voltage scaling via the regulator subsystem
> >   (vref-supply), falling back to the internal 2.048V reference
> >   if not specified.
>
> Nice to see it starting with a minimal feature set like this. Makes it easy
> to review.
>
> In the cover letter, it would be nice to know if this is all the features
> you need or if you plan to add more soon.
I will clarify this in the next version cover letter. This minimal set
covers our
current use case now.
>
> >
> > Signed-off-by: Kyle Hsieh <[email protected]>
> > ---
> >  drivers/iio/adc/Kconfig        |  10 ++
> >  drivers/iio/adc/Makefile       |   1 +
> >  drivers/iio/adc/ti-ads112c04.c | 317 +++++++++++++++++++++++++++++++++++++++++
> >  3 files changed, 328 insertions(+)
> >
> > diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
> > index 3755a81c1efd..402e841bc083 100644
> > --- a/drivers/iio/adc/Kconfig
> > +++ b/drivers/iio/adc/Kconfig
> > @@ -1789,6 +1789,16 @@ config TI_ADS1119
> >           This driver can also be built as a module. If so, the module will be
> >           called ti-ads1119.
> >
> > +config TI_ADS112C04
> > +     tristate "Texas Instruments ADS112C04 ADC"
> > +     depends on I2C
> > +     help
> > +       If you say yes here you get support for Texas Instruments
> > +       ADS112C04 (16-bit) I2C analog to digital converters.
> > +
> > +       This driver can also be built as a module. If so, the module will be
> > +       called ti-ads112c04.
> > +
> >  config TI_ADS124S08
> >       tristate "Texas Instruments ADS124S08"
> >       depends on SPI
> > diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile
> > index 707dd708912f..ebf9d4047a5a 100644
> > --- a/drivers/iio/adc/Makefile
> > +++ b/drivers/iio/adc/Makefile
> > @@ -153,6 +153,7 @@ obj-$(CONFIG_TI_ADS1015) += ti-ads1015.o
> >  obj-$(CONFIG_TI_ADS1018) += ti-ads1018.o
> >  obj-$(CONFIG_TI_ADS1100) += ti-ads1100.o
> >  obj-$(CONFIG_TI_ADS1119) += ti-ads1119.o
> > +obj-$(CONFIG_TI_ADS112C04)   += ti-ads112c04.o
> >  obj-$(CONFIG_TI_ADS124S08) += ti-ads124s08.o
> >  obj-$(CONFIG_TI_ADS1298) += ti-ads1298.o
> >  obj-$(CONFIG_TI_ADS131E08) += ti-ads131e08.o
> > diff --git a/drivers/iio/adc/ti-ads112c04.c b/drivers/iio/adc/ti-ads112c04.c
> > new file mode 100644
> > index 000000000000..0f1d415e3f21
> > --- /dev/null
> > +++ b/drivers/iio/adc/ti-ads112c04.c
> > @@ -0,0 +1,317 @@
> > +// SPDX-License-Identifier: GPL-2.0
>
> Prefer either GPL-2.0-only or GPL-2.0-or-later (your choice).
>
> > +/*
> > + * Texas Instruments ADS112C04 16-bit I2C ADC driver
> > + * Based on TI Reference Code and standard Linux IIO framework.
> > + */
> > +
> > +#include <linux/module.h>
> > +#include <linux/i2c.h>
> > +#include <linux/delay.h>
> > +#include <linux/interrupt.h>
> > +#include <linux/mutex.h>
> > +#include <linux/iio/iio.h>
> > +#include <linux/iio/sysfs.h>
> > +#include <linux/regulator/consumer.h>
> > +
> > +/* ADS112C04 Commands */
> > +#define ADS112C04_CMD_RESET         0x06
> > +#define ADS112C04_CMD_START_SYNC    0x08
> > +#define ADS112C04_CMD_POWERDOWN     0x02
> > +#define ADS112C04_CMD_RDATA         0x10
> > +#define ADS112C04_CMD_RREG(reg)     (0x20 | ((reg) << 2))
> > +#define ADS112C04_CMD_WREG(reg)     (0x40 | ((reg) << 2))
> > +
> > +/* Registers */
> > +#define ADS112C04_REG_CONFIG0       0x00
> > +#define ADS112C04_REG_CONFIG1       0x01
> > +#define ADS112C04_REG_CONFIG2       0x02
> > +#define ADS112C04_REG_CONFIG3       0x03
> > +
> > +/* Config2 Masks */
> > +#define ADS112C04_DRDY_MASK         BIT(7)
> > +
> > +struct ads112c04_state {
> > +     struct i2c_client *client;
> > +     /* Protects concurrent ADC reads and device configuration */
> > +     struct mutex lock;
> > +     struct completion completion;
> > +     struct regulator *vref_reg;
> > +     int vref_mv;
> > +     u8 config0;
> > +     u8 config1;
> > +};
> > +
> > +static int ads112c04_write_cmd(struct i2c_client *client, u8 cmd)
> > +{
> > +     return i2c_master_send(client, &cmd, 1);
> > +}
> > +
> > +static int ads112c04_read_reg(struct i2c_client *client, u8 reg, u8 *val)
> > +{
> > +     u8 cmd = ADS112C04_CMD_RREG(reg);
> > +     int ret;
> > +
> > +     ret = i2c_master_send(client, &cmd, 1);
> > +     if (ret < 0)
> > +             return ret;
> > +
> > +     ret = i2c_master_recv(client, val, 1);
> > +     return ret < 0 ? ret : 0;
> > +}
> > +
> > +static int ads112c04_write_reg(struct i2c_client *client, u8 reg, u8 val)
> > +{
> > +     u8 buf[2] = { ADS112C04_CMD_WREG(reg), val };
> > +     int ret;
> > +
> > +     ret = i2c_master_send(client, buf, 2);
> > +     return ret < 0 ? ret : 0;
> > +}
> > +
> > +static int ads112c04_wait_for_data(struct ads112c04_state *st)
> > +{
> > +     int ret, timeout = 50;
>
> Why is poll timeout 50x longer than interrupt timeout?
>
> > +     u8 val;
> > +
> > +     /* If IRQ is available, wait for DRDY interrupt completion */
> > +     if (st->client->irq > 0) {
> > +             ret = wait_for_completion_timeout(&st->completion, msecs_to_jiffies(1000));
> > +             if (!ret)
> > +                     return -ETIMEDOUT;
> > +             return 0;
> > +     }
> > +
> > +     /* Fallback: Poll DRDY bit in CONFIG2 */
>
> Use read_poll_timeout() or if you convert to use regmap, regmap_read_poll_timeout().
I will switch to `read_poll_timeout()` and align the timeout duration properly.
>
> > +     while (timeout--) {
> > +             ret = ads112c04_read_reg(st->client, ADS112C04_REG_CONFIG2, &val);
> > +             if (ret < 0)
> > +                     return ret;
> > +             if (val & ADS112C04_DRDY_MASK)
> > +                     return 0;
> > +             usleep_range(1000, 2000);
> > +     }
> > +
> > +     return -ETIMEDOUT;
> > +}
> > +
> > +static int ads112c04_read_data(struct ads112c04_state *st, int *val)
> > +{
> > +     u8 cmd = ADS112C04_CMD_RDATA;
> > +     u8 buf[2];
>
> Use __be16 buf;
>
> > +     int ret;
> > +
> > +     ret = i2c_master_send(st->client, &cmd, 1);
> > +     if (ret < 0)
> > +             return ret;
> > +
> > +     ret = i2c_master_recv(st->client, buf, 2);
> > +     if (ret < 0)
> > +             return ret;
>
> Can be simplified by using smbus APIs. It would just be one call.
>
> > +
> > +     /* 16-bit 2's complement conversion */
> > +     *val = (s16)((buf[0] << 8) | buf[1]);
>
> And here use be16_to_cpu() and sign_extend32(). Then no comments needed.
Thank you for the suggestion. I will update the data reading logic to use
`__be16`, `be16_to_cpu()`, and `sign_extend32()` for a much cleaner
implementation.
>
> > +     return 0;
> > +}
> > +
> > +static int ads112c04_get_adc_result(struct ads112c04_state *st,
> > +                                 struct iio_chan_spec const *chan,
> > +                                 int *val)
> > +{
> > +     int ret;
> > +     u8 mux;
> > +
> > +     mux = (chan->address << 4) & 0xF0;
>
> Use FIELD_PREP().
Got it.
>
> > +     if ((st->config0 & 0xF0) != mux) {
>
> Another option would be to use regmap with cache so that we don't
> have to keep track of the current config ourselves.
>
> > +             st->config0 = (st->config0 & 0x0F) | mux;
> > +             ads112c04_write_reg(st->client, ADS112C04_REG_CONFIG0, st->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;
> > +
> > +     return ads112c04_read_data(st, val);
> > +}
> > +
> > +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, raw;
> > +
> > +     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;
> > +             return IIO_VAL_INT;
> > +
> > +     case IIO_CHAN_INFO_SCALE:
> > +             *val = st->vref_mv;
> > +             *val2 = 15; /* 2^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);
> > +
> > +     complete(&st->completion);
>
> nit: add blank line here.
Will fix.
>
> > +     return IRQ_HANDLED;
> > +}
> > +
> > +#define ADS112C04_V_CHAN(_chan, _addr) {                     \
> > +     .type = IIO_VOLTAGE,                                    \
> > +     .indexed = 1,                                           \
> > +     .channel = _chan,                                       \
> > +     .address = _addr,                                       \
> > +     .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),           \
> > +     .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),   \
> > +}
> > +
> > +#define ADS112C04_V_DIFF_CHAN(_chan, _chan2, _addr) {                \
> > +     .type = IIO_VOLTAGE,                                    \
> > +     .differential = 1,                                      \
> > +     .indexed = 1,                                           \
> > +     .channel = _chan,                                       \
> > +     .channel2 = _chan2,                                     \
> > +     .address = _addr,                                       \
> > +     .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),           \
> > +     .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),   \
> > +}
> > +
> > +static const struct iio_chan_spec ads112c04_channels[] = {
> > +     ADS112C04_V_DIFF_CHAN(0, 1, 0x00),
> > +     ADS112C04_V_DIFF_CHAN(0, 2, 0x01),
> > +     ADS112C04_V_DIFF_CHAN(0, 3, 0x02),
> > +     ADS112C04_V_DIFF_CHAN(1, 0, 0x03),
> > +     ADS112C04_V_DIFF_CHAN(1, 2, 0x04),
> > +     ADS112C04_V_DIFF_CHAN(1, 3, 0x05),
> > +     ADS112C04_V_DIFF_CHAN(2, 3, 0x06),
> > +     ADS112C04_V_DIFF_CHAN(3, 2, 0x07),
> > +     ADS112C04_V_CHAN(0, 0x08),
> > +     ADS112C04_V_CHAN(1, 0x09),
> > +     ADS112C04_V_CHAN(2, 0x0A),
> > +     ADS112C04_V_CHAN(3, 0x0B),
> > +};
> > +
> > +static const struct iio_info ads112c04_info = {
> > +     .read_raw = ads112c04_read_raw,
> > +};
> > +
> > +static void ads112c04_regulator_disable(void *data)
> > +{
> > +     regulator_disable(data);
> > +}
> > +
> > +static int ads112c04_probe(struct i2c_client *client)
> > +{
> > +     struct iio_dev *indio_dev;
> > +     struct ads112c04_state *st;
> > +     int ret;
> > +
> > +     indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*st));
> > +     if (!indio_dev)
> > +             return -ENOMEM;
> > +
> > +     st = iio_priv(indio_dev);
> > +     st->client = client;
> > +     mutex_init(&st->lock);
>
>         ret = devm_mutex_init(&st->lock);
>         if (ret)
>                 return ret;
>
> > +     init_completion(&st->completion);
> > +
> > +     indio_dev->name = "ads112c04";
> > +     indio_dev->modes = INDIO_DIRECT_MODE;
> > +     indio_dev->info = &ads112c04_info;
> > +     indio_dev->channels = ads112c04_channels;
> > +     indio_dev->num_channels = ARRAY_SIZE(ads112c04_channels);
>
> We need more information to know how to configure each channel, so we
> probably won't be able to use fixed channels like this.
I will drop the fixed array and refactor the driver to parse the
channel configurations
dynamically from the devicetree child nodes, matching the dt-bindings update.
>
> > +
> > +     st->vref_reg = devm_regulator_get_optional(&client->dev, "vref");
>
> This is gonig to be more compilicated. See ti-ads112c14 driver.

>
> > +     if (IS_ERR(st->vref_reg)) {
> > +             ret = PTR_ERR(st->vref_reg);
> > +             if (ret == -ENODEV) {
> > +                     st->vref_mv = 2048;
> > +                     st->config1 = 0x00; /* VREF[1:0] = 00 */
> > +             } else {
> > +                     return ret;
> > +             }
> > +     } else {
> > +             ret = regulator_enable(st->vref_reg);
> > +             if (ret)
> > +                     return ret;
> > +
> > +             ret = devm_add_action_or_reset(&client->dev, ads112c04_regulator_disable,
> > +                                            st->vref_reg);
> > +             if (ret)
> > +                     return ret;
> > +
> > +             ret = regulator_get_voltage(st->vref_reg);
> > +             if (ret < 0)
> > +                     return ret;
> > +
> > +             st->vref_mv = ret / 1000;
> > +             st->config1 = 0x02;
> > +     }
> > +
> > +     /* Reset device to defaults */
>
> There is a hardware reset, so the way we usually do that is to use
> devm_reset_control_get_optional_exclusive() to see if the gpio is
> wired up. If it is, use that to do the reset, otherwise fall back
> to the software reset.
I will implement the hardware reset fallback logic using
`devm_gpiod_get_optional()` as you suggested.
>
> > +     ads112c04_write_cmd(client, ADS112C04_CMD_RESET);
> > +     usleep_range(1000, 2000);
>
>         fsleep(1000);
>
> > +
> > +     /* Set defaults: PGA Disabled (For single-ended safety), Gain=1 */
> > +     st->config0 = 0x01;
> > +
> > +     ads112c04_write_reg(client, ADS112C04_REG_CONFIG1, st->config1);
>
> Check return value.
Will fix.
>
> > +
> > +     if (client->irq > 0) {
> > +             ret = devm_request_irq(&client->dev, client->irq,
> > +                                    ads112c04_irq_handler,
> > +                                    IRQF_TRIGGER_FALLING,
>
> This flag already comes from the devicetree and should not be hard-coded
> here.
I will change this to `0` to inherit the trigger type from the devicetree.
>
> > +                                    indio_dev->name, indio_dev);
> > +             if (ret) {
> > +                     dev_err(&client->dev, "Failed to request DRDY IRQ\n");
> > +                     return ret;
> > +             }
> > +     }
> > +
> > +     return devm_iio_device_register(&client->dev, indio_dev);
> > +}
> > +
> > +static const struct i2c_device_id ads112c04_id[] = {
> > +     { "ads112c04", 0 },
>
>         { .name = "ads112c04" },
>
Will update to use the named initializer.
Thanks again for your time and the fantastic guidance!
> > +     { }
> > +};
> > +MODULE_DEVICE_TABLE(i2c, ads112c04_id);
> > +
> > +static const struct of_device_id ads112c04_of_match[] = {
> > +     { .compatible = "ti,ads112c04" },
> > +     { }
> > +};
> > +MODULE_DEVICE_TABLE(of, ads112c04_of_match);
> > +
> > +static struct i2c_driver ads112c04_driver = {
> > +     .driver = {
> > +             .name = "ads112c04",
> > +             .of_match_table = ads112c04_of_match,
> > +     },
> > +     .probe = ads112c04_probe,
> > +     .id_table = ads112c04_id,
> > +};
> > +module_i2c_driver(ads112c04_driver);
> > +
> > +MODULE_AUTHOR("Kyle Hsieh <[email protected]>");
> > +MODULE_DESCRIPTION("Texas Instruments ADS112C04 ADC driver");
> > +MODULE_LICENSE("GPL");
> >
>
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.