Re: [PATCH 2/2] iio: adc: Add TI ADS1220 driver

From: Andy Shevchenko

Date: Thu Jun 11 2026 - 12:03:52 EST


On Wed, Jun 10, 2026 at 10:13:42PM +0700, Nguyen Minh Tien wrote:
> Add an IIO driver for the Texas Instruments ADS1220 24-bit delta-sigma
> SPI ADC. The driver supports single-ended and differential voltage
> channels described as device-tree child nodes, per-channel programmable
> gain (exposed through scale) and data rate (exposed through sampling
> frequency), the internal 2.048V reference, an external reference via a
> regulator, or the analog supply (AVDD) as a ratiometric reference,
> single-shot conversions and a DRDY-interrupt-driven triggered buffer.
> Conversions are gated either on the DRDY interrupt or, when no interrupt
> is wired, on a data-rate-derived delay. Runtime PM powers the device down
> between conversions.

...

+ array_size.h

> +#include <linux/bitfield.h>

> +#include <linux/bits.h>

Instead you should use bitops.h due to sign_extend32() and other calls.

> +#include <linux/completion.h>
> +#include <linux/delay.h>
> +#include <linux/err.h>
> +#include <linux/interrupt.h>

> +#include <linux/log2.h>

+ math.h // DIV_ROUND_UP()

> +#include <linux/mod_devicetable.h>
> +#include <linux/module.h>
> +#include <linux/property.h>
> +#include <linux/regulator/consumer.h>
> +#include <linux/pm_runtime.h>
> +#include <linux/spi/spi.h>

+ types.h // bool, uXX

> +#include <linux/units.h>
> +#include <linux/unaligned.h>

...

> +/* Worst-case single conversion: 20 SPS => 50 ms, plus margin. */
> +#define ADS1220_CONV_TIMEOUT_MS 100
> +#define ADS1220_CONV_MARGIN_US 2000

2 * USEC_PER_MSEC (will need time.h to be included).

...


> +#define ADS1220_SUSPEND_DELAY_MS 2000

2 * MSEC_PER_SEC respectively.

...

> +static int ads1220_write_reg(struct ads1220_state *st, u8 reg, u8 val)
> +{
> + st->tx[0] = ADS1220_CMD_WREG_REG(reg);
> + st->tx[1] = val;
> +
> + return spi_write(st->spi, st->tx, 2);

sizeof(st->tx) ?

> +}

...

> +static unsigned int ads1220_datarate_to_code(unsigned int datarate)

unsigned? See below why.

> +{
> + int i;
> +
> + for (i = 0; i < ARRAY_SIZE(ads1220_datarates); i++)

for (unsigned int i = 0; i < ARRAY_SIZE(ads1220_datarates); i++)


> + if (ads1220_datarates[i] == datarate)
> + return i;
> +
> + return 0;

This is wrong. It should return -ENOENT or so to be distinct with the 0 index.

> +}

...

> +static int ads1220_read_sample(struct ads1220_state *st, unsigned int datarate,
> + int *val)
> +{
> + int ret;
> +
> + if (st->spi->irq) {
> + unsigned long timeout = msecs_to_jiffies(ADS1220_CONV_TIMEOUT_MS);
> +
> + if (!wait_for_completion_timeout(&st->completion, timeout))
> + return -ETIMEDOUT;
> + } else {
> + /*
> + * No DRDY interrupt: wait for the conversion to finish. In
> + * single-shot mode the result stays latched until the next
> + * START, so waiting longer than one conversion is harmless;
> + * wait two periods plus a margin to comfortably cover the
> + * oscillator start-up and its tolerance.
> + */
> + fsleep(2 * DIV_ROUND_UP(MICRO, datarate) + ADS1220_CONV_MARGIN_US);

USEC_PER_SEC instead of MICRO?

> + }
> +
> + /*
> + * Once DRDY is low the result can be clocked out directly, MSB first,
> + * without an RDATA command (datasheet section 8.5.4).
> + */
> + ret = spi_read(st->spi, st->rx, ADS1220_DATA_BYTES);
> + if (ret)
> + return ret;
> +
> + *val = sign_extend32(get_unaligned_be24(st->rx), ADS1220_DATA_BITS - 1);
> +
> + return 0;
> +}

...

> +static int ads1220_single_conversion(struct ads1220_state *st,
> + const struct iio_chan_spec *chan,
> + int *val, bool calib_offset)
> +{
> + struct device *dev = &st->spi->dev;
> + struct ads1220_channel_config *cfg = &st->channels_cfg[chan->address];
> + unsigned int mux = cfg->mux;
> + bool single_ended = cfg->single_ended;
> + int ret;
> +
> + if (calib_offset) {
> + mux = ADS1220_MUX_SHORTED;
> + single_ended = false;
> + }
> +
> + ret = pm_runtime_resume_and_get(dev);
> + if (ret)
> + return ret;

Use PM_ACQUIRE_*() macros instead.


> + ret = ads1220_configure(st, mux, cfg->gain, cfg->datarate,
> + single_ended, false);
> + if (ret)
> + goto out;
> +
> + if (st->spi->irq)
> + reinit_completion(&st->completion);
> +
> + ret = ads1220_command(st, ADS1220_CMD_START);
> + if (ret)
> + goto out;
> +
> + ret = ads1220_read_sample(st, cfg->datarate, val);
> + if (ret)
> + goto out;
> +
> + ret = IIO_VAL_INT;
> +out:

> + pm_runtime_mark_last_busy(dev);

Even without above this is not needed, it's implied by the below call.

> + pm_runtime_put_autosuspend(dev);
> +
> + return ret;
> +}

...

> +static int ads1220_write_raw(struct iio_dev *indio_dev,
> + const struct iio_chan_spec *chan,
> + int val, int val2, long mask)
> +{
> + struct ads1220_state *st = iio_priv(indio_dev);
> + struct ads1220_channel_config *cfg = &st->channels_cfg[chan->address];
> + unsigned int gain;


> + int i;

Why signed? And why not to make it local to the loop?

> + switch (mask) {
> + case IIO_CHAN_INFO_SCALE:
> + /* The available scales are the gain reciprocals (e.g. 1/4). */
> + if (val == 0 && val2 == 0)
> + return -EINVAL;
> +
> + gain = MICRO / (val * MICRO + val2);
> + if (!is_power_of_2(gain) || gain > BIT(ADS1220_NUM_GAINS - 1))
> + return -EINVAL;
> + if (cfg->single_ended && gain > ADS1220_MAX_SE_GAIN)
> + return -EINVAL;
> +
> + cfg->gain = gain;
> + return 0;
> + case IIO_CHAN_INFO_SAMP_FREQ:

> + for (i = 0; i < ARRAY_SIZE(ads1220_datarates); i++) {
> + if (ads1220_datarates[i] == val) {
> + cfg->datarate = val;
> + return 0;
> + }
> + }

Can't ads1220_datarate_to_code() be used?

> + return -EINVAL;
> + default:
> + return -EINVAL;
> + }
> +}

...

> +static int ads1220_debugfs_reg_access(struct iio_dev *indio_dev,
> + unsigned int reg, unsigned int writeval,
> + unsigned int *readval)
> +{
> + struct ads1220_state *st = iio_priv(indio_dev);
> + u8 val;
> + int ret;

> + if (reg > ADS1220_MAX_REG)
> + return -EINVAL;

How is this not a dead code? Forgot to set .max_register in regmap configuration?

> + if (readval) {
> + ret = ads1220_read_reg(st, reg, &val);
> + if (ret)
> + return ret;
> + *readval = val;
> + return 0;
> + }
> +
> + return ads1220_write_reg(st, reg, writeval);
> +}

...

> +static int ads1220_buffer_preenable(struct iio_dev *indio_dev)
> +{
> + struct ads1220_state *st = iio_priv(indio_dev);
> + struct device *dev = &st->spi->dev;
> + struct ads1220_channel_config *cfg;
> + unsigned int index;
> + int ret;
> +
> + index = find_first_bit(indio_dev->active_scan_mask,
> + iio_get_masklength(indio_dev));
> + cfg = &st->channels_cfg[index];

> + ret = pm_runtime_resume_and_get(dev);
> + if (ret)
> + return ret;

> + ret = ads1220_configure(st, cfg->mux, cfg->gain, cfg->datarate,
> + cfg->single_ended, true);
> + if (ret)
> + goto err;
> +
> + ret = ads1220_command(st, ADS1220_CMD_START);
> + if (ret)
> + goto err;
> +
> + return 0;
> +err:

> + pm_runtime_mark_last_busy(dev);

Dup.

> + pm_runtime_put_autosuspend(dev);
> + return ret;
> +}

...

> +static int ads1220_buffer_postdisable(struct iio_dev *indio_dev)
> +{
> + struct ads1220_state *st = iio_priv(indio_dev);
> + struct device *dev = &st->spi->dev;

> + pm_runtime_mark_last_busy(dev);

Dup.

> + pm_runtime_put_autosuspend(dev);
> +
> + return 0;
> +}

...

> + static const u8 diff_mux[ADS1220_MAX_AIN][ADS1220_MAX_AIN] = {
> + [0][1] = 0x0, [0][2] = 0x1, [0][3] = 0x2,
> + [1][2] = 0x3, [1][3] = 0x4, [1][0] = 0x6,
> + [2][3] = 0x5,
> + [3][2] = 0x7,

Please, fill all the gaps to make this look nice tabulator.
Also you may drop the first _MAX_AIN, compiler will be able to get this.

> + };

...

> + const struct iio_chan_spec ads1220_channel = {

Why is this not static?

> + .type = IIO_VOLTAGE,
> + .indexed = 1,
> + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
> + BIT(IIO_CHAN_INFO_SCALE) |
> + BIT(IIO_CHAN_INFO_OFFSET) |
> + BIT(IIO_CHAN_INFO_SAMP_FREQ),
> + .info_mask_separate_available = BIT(IIO_CHAN_INFO_SCALE) |
> + BIT(IIO_CHAN_INFO_SAMP_FREQ),
> + .scan_type = {
> + .sign = 's',
> + .realbits = ADS1220_DATA_BITS,
> + .storagebits = 32,
> + .endianness = IIO_CPU,
> + },
> + };

...

> + unsigned int num_channels, i = 0;

Split assignment and definition to avoid subtle mistakes in the future.
Move the assignment closer to it's first user
(device_for_each_child_node_scoped() loop AFAICS).

> + int ret;
> +
> + st->num_channels_cfg = device_get_child_node_count(dev);
> + if (st->num_channels_cfg == 0 ||
> + st->num_channels_cfg > ADS1220_MAX_CHANNELS)

in_range() ?

> + return dev_err_probe(dev, -EINVAL,
> + "Invalid channel count %u (max %u)\n",
> + st->num_channels_cfg, ADS1220_MAX_CHANNELS);

> + /* One extra channel for the timestamp. */
> + num_channels = st->num_channels_cfg + 1;

> + channels = devm_kcalloc(dev, num_channels, sizeof(*channels),
> + GFP_KERNEL);

I would leave it on a single line.

> + if (!channels)
> + return -ENOMEM;
> +
> + device_for_each_child_node_scoped(dev, child) {
> + struct ads1220_channel_config *cfg = &st->channels_cfg[i];
> + bool differential;
> + u32 ain[2];
> +
> + differential = fwnode_property_present(child, "diff-channels");
> + if (differential)
> + ret = fwnode_property_read_u32_array(child,
> + "diff-channels",
> + ain, 2);

ARRAY_SIZE()

> + else
> + ret = fwnode_property_read_u32(child, "single-channel",
> + &ain[0]);
> + if (ret)
> + return dev_err_probe(dev, ret,
> + "Failed to read channel property\n");

> + ret = ads1220_map_mux(dev, ain[0], ain[1], differential,

ain[1] may be uninitialised here. Why not supply the pointer to the array since
you anyway supply 'differential'?

> + &cfg->mux, &cfg->single_ended);
> + if (ret)
> + return dev_err_probe(dev, ret,
> + "Invalid input combination\n");
> +
> + cfg->gain = 1;
> + cfg->datarate = ads1220_datarates[0];
> +
> + chan = &channels[i];
> + *chan = ads1220_channel;
> + chan->channel = ain[0];
> + chan->address = i;
> + chan->scan_index = i;
> + if (differential) {
> + chan->channel2 = ain[1];
> + chan->differential = 1;
> + }
> +
> + i++;
> + }

...

> + st->vref_uV = devm_regulator_get_enable_read_voltage(dev, "vref");
> + if (st->vref_uV >= 0) {

Does Vref == 0 make any sense?

> + st->vref_source = ADS1220_VREF_REFP0_REFN0;
> + } else if (st->vref_uV != -ENODEV) {
> + return dev_err_probe(dev, st->vref_uV, "Failed to get vref\n");
> + } else if (device_property_read_bool(dev, "ti,vref-avdd")) {
> + st->vref_source = ADS1220_VREF_AVDD;
> + st->vref_uV = avdd_uV;
> + } else {
> + st->vref_source = ADS1220_VREF_INTERNAL;
> + st->vref_uV = ADS1220_INTERNAL_VREF_uV;
> + }

...

> + if (spi->irq > 0) {
> + ret = devm_request_irq(dev, spi->irq, ads1220_irq_handler,
> + IRQF_NO_THREAD, "ads1220", indio_dev);
> + if (ret)

> + return dev_err_probe(dev, ret,
> + "Failed to request irq\n");

No dup messages, just

return ret;

here.

> + st->trig = devm_iio_trigger_alloc(dev, "%s-dev%d",
> + indio_dev->name,
> + iio_device_id(indio_dev));
> + if (!st->trig)
> + return -ENOMEM;
> +
> + st->trig->ops = &ads1220_trigger_ops;
> + iio_trigger_set_drvdata(st->trig, indio_dev);
> +
> + ret = devm_iio_trigger_register(dev, st->trig);
> + if (ret)
> + return dev_err_probe(dev, ret,
> + "Failed to register trigger\n");
> + }

...

> +static const struct spi_device_id ads1220_id[] = {
> + { "ads1220" },

Use C99 initialisers.

> + { }
> +};

--
With Best Regards,
Andy Shevchenko