Re: [PATCH v2 2/2] iio: adc: Add support for TI ADS1120
From: Ajith Anandhan
Date: Mon Dec 15 2025 - 11:13:41 EST
On 11/18/25 7:34 PM, David Lechner wrote:
On 11/9/25 8:11 AM, Ajith Anandhan wrote:
Add driver for the Texas Instruments ADS1120, a precision 16-bit...
analog-to-digital converter with an SPI interface.
+#define ADS1120_CFG0_GAIN_MASK GENMASK(3, 1)We could avoid these macros by just doing ilog2(gain).
+#define ADS1120_CFG0_GAIN_1 0
+#define ADS1120_CFG0_GAIN_2 1
+#define ADS1120_CFG0_GAIN_4 2
+#define ADS1120_CFG0_GAIN_8 3
+#define ADS1120_CFG0_GAIN_16 4
+#define ADS1120_CFG0_GAIN_32 5
+#define ADS1120_CFG0_GAIN_64 6
+#define ADS1120_CFG0_GAIN_128 7
I understand your concern about unused macros. I've kept them for documentation purposes as they map directly to the datasheet register definitions, which makes it easier to verify correctness against hardware specs also I'd prefer to keep it like this since it give more readability Shall i keep this as it is for this initial version?
+I would avoid writing macros for values we don't use yet. For example,
+#define ADS1120_CFG0_PGA_BYPASS BIT(0)
+
+/* Config Register 1 bit definitions */
+#define ADS1120_CFG1_DR_MASK GENMASK(7, 5)
+#define ADS1120_CFG1_DR_20SPS 0
+#define ADS1120_CFG1_DR_45SPS 1
+#define ADS1120_CFG1_DR_90SPS 2
+#define ADS1120_CFG1_DR_175SPS 3
+#define ADS1120_CFG1_DR_330SPS 4
+#define ADS1120_CFG1_DR_600SPS 5
+#define ADS1120_CFG1_DR_1000SPS 6
it may make more sense to have a lookup table when it comes to actually
implementing something that uses this.
Same applies to the rest below.
+Usually we don't make macros for the index. When it comes to adding
+#define ADS1120_CFG1_MODE_MASK GENMASK(4, 3)
+#define ADS1120_CFG1_MODE_NORMAL 0
+#define ADS1120_CFG1_MODE_DUTY 1
+#define ADS1120_CFG1_MODE_TURBO 2
+
+#define ADS1120_CFG1_CM_MASK BIT(2)
+#define ADS1120_CFG1_CM_SINGLE 0
+#define ADS1120_CFG1_CM_CONTINUOUS 1
+
+#define ADS1120_CFG1_TS_EN BIT(1)
+#define ADS1120_CFG1_BCS_EN BIT(0)
+
+/* Config Register 2 bit definitions */
+#define ADS1120_CFG2_VREF_MASK GENMASK(7, 6)
+#define ADS1120_CFG2_VREF_INTERNAL 0
+#define ADS1120_CFG2_VREF_EXT_REFP0 1
+#define ADS1120_CFG2_VREF_EXT_AIN0 2
+#define ADS1120_CFG2_VREF_AVDD 3
+
+#define ADS1120_CFG2_REJECT_MASK GENMASK(5, 4)
+#define ADS1120_CFG2_REJECT_OFF 0
+#define ADS1120_CFG2_REJECT_50_60 1
+#define ADS1120_CFG2_REJECT_50 2
+#define ADS1120_CFG2_REJECT_60 3
+
+#define ADS1120_CFG2_PSW_EN BIT(3)
+
+#define ADS1120_CFG2_IDAC_MASK GENMASK(2, 0)
+#define ADS1120_CFG2_IDAC_OFF 0
+#define ADS1120_CFG2_IDAC_10UA 1
+#define ADS1120_CFG2_IDAC_50UA 2
+#define ADS1120_CFG2_IDAC_100UA 3
+#define ADS1120_CFG2_IDAC_250UA 4
+#define ADS1120_CFG2_IDAC_500UA 5
+#define ADS1120_CFG2_IDAC_1000UA 6
+#define ADS1120_CFG2_IDAC_1500UA 7
+
+/* Config Register 3 bit definitions */
+#define ADS1120_CFG3_IDAC1_MASK GENMASK(7, 5)
+#define ADS1120_CFG3_IDAC1_DISABLED 0
+#define ADS1120_CFG3_IDAC1_AIN0 1
+#define ADS1120_CFG3_IDAC1_AIN1 2
+#define ADS1120_CFG3_IDAC1_AIN2 3
+#define ADS1120_CFG3_IDAC1_AIN3 4
+#define ADS1120_CFG3_IDAC1_REFP0 5
+#define ADS1120_CFG3_IDAC1_REFN0 6
+
+#define ADS1120_CFG3_IDAC2_MASK GENMASK(4, 2)
+#define ADS1120_CFG3_IDAC2_DISABLED 0
+#define ADS1120_CFG3_IDAC2_AIN0 1
+#define ADS1120_CFG3_IDAC2_AIN1 2
+#define ADS1120_CFG3_IDAC2_AIN2 3
+#define ADS1120_CFG3_IDAC2_AIN3 4
+#define ADS1120_CFG3_IDAC2_REFP0 5
+#define ADS1120_CFG3_IDAC2_REFN0 6
+
+#define ADS1120_CFG3_DRDYM_MASK BIT(1)
+#define ADS1120_CFG3_DRDYM_DRDY_ONLY 0
+#define ADS1120_CFG3_DRDYM_BOTH 1
+
+/* Conversion time for 20 SPS */
+#define ADS1120_CONV_TIME_MS 51
+
+/* Internal reference voltage in millivolts */
+#define ADS1120_VREF_INTERNAL_MV 2048
+
+
+struct ads1120_state {
+ struct spi_device *spi;
+ struct regmap *regmap;
+ /*
+ * Protects chip configuration and ADC reads to ensure
+ * consistent channel/gain settings during conversions.
+ */
+ struct mutex lock;
+
+ int vref_mv;
+
+ /* DMA-safe buffer for SPI transfers */
+ u8 data[4] __aligned(IIO_DMA_MINALIGN);
+};
+
+
+static const int ads1120_gain_values[] = { 1, 2, 4, 8, 16, 32, 64, 128 };
+static const int ads1120_low_gain_values[] = { 1, 2, 4 };
+
+/* Differential channel macro */
+#define ADS1120_DIFF_CHANNEL(index, chan1, chan2) \
+{ \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .channel = chan1, \
+ .channel2 = chan2, \
+ .differential = 1, \
+ .address = index, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_SCALE), \
+ .info_mask_separate_available = BIT(IIO_CHAN_INFO_SCALE), \
+}
+
+/* Single-ended channel macro */
+#define ADS1120_SINGLE_CHANNEL(index, chan) \
+{ \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .channel = chan, \
+ .address = index, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_SCALE), \
+ .info_mask_separate_available = BIT(IIO_CHAN_INFO_SCALE), \
+}
+
+/* Diagnostic channel macro */
+#define ADS1120_DIAG_CHANNEL(index, label) \
+{ \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .channel = index, \
+ .address = index, \
+ .extend_name = label, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_SCALE), \
+ .info_mask_separate_available = BIT(IIO_CHAN_INFO_SCALE), \
+}
+
+static const struct iio_chan_spec ads1120_channels[] = {
+ /* Differential inputs */
+ ADS1120_DIFF_CHANNEL(ADS1120_CFG0_MUX_AIN0_AIN1, 0, 1),
+ ADS1120_DIFF_CHANNEL(ADS1120_CFG0_MUX_AIN0_AIN2, 0, 2),
+ ADS1120_DIFF_CHANNEL(ADS1120_CFG0_MUX_AIN0_AIN3, 0, 3),
+ ADS1120_DIFF_CHANNEL(ADS1120_CFG0_MUX_AIN1_AIN2, 1, 2),
+ ADS1120_DIFF_CHANNEL(ADS1120_CFG0_MUX_AIN1_AIN3, 1, 3),
+ ADS1120_DIFF_CHANNEL(ADS1120_CFG0_MUX_AIN2_AIN3, 2, 3),
+ ADS1120_DIFF_CHANNEL(ADS1120_CFG0_MUX_AIN1_AIN0, 1, 0),
+ ADS1120_DIFF_CHANNEL(ADS1120_CFG0_MUX_AIN3_AIN2, 3, 2),
+ /* Single-ended inputs */
+ ADS1120_SINGLE_CHANNEL(ADS1120_CFG0_MUX_AIN0_AVSS, 0),
+ ADS1120_SINGLE_CHANNEL(ADS1120_CFG0_MUX_AIN1_AVSS, 1),
+ ADS1120_SINGLE_CHANNEL(ADS1120_CFG0_MUX_AIN2_AVSS, 2),
+ ADS1120_SINGLE_CHANNEL(ADS1120_CFG0_MUX_AIN3_AVSS, 3),
+ /* Diagnostic inputs */
+ ADS1120_DIAG_CHANNEL(ADS1120_CFG0_MUX_REFP_REFN_4, "ref_div4"),
+ ADS1120_DIAG_CHANNEL(ADS1120_CFG0_MUX_AVDD_AVSS_4, "avdd_div4"),
+ ADS1120_DIAG_CHANNEL(ADS1120_CFG0_MUX_SHORTED, "shorted"),
+};
buffer support, having the macros makes it harder to see which scan_index
belongs to which channel. And if buffer support is planned for later,
it would make sense to use .scan_index instead of .address to avoid
duplicating that info later.
+static int ads1120_set_gain(struct ads1120_state *st, int gain_val)Since there is only one gain register, we should store this in a
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(ads1120_gain_values); i++) {
+ if (ads1120_gain_values[i] == gain_val)
+ break;
+ }
+
+ if (i == ARRAY_SIZE(ads1120_gain_values))
+ return -EINVAL;
per-channel variable, then set the gain in the register in
ads1120_set_mux() instead (and it would make sense to rename
that function as well to something like ads1120_configure_channel()).
I've implemented a global gain that applies to all channels forsimplicity. I planed to add
per-channel gain storage in the later patches.
+...
+ return regmap_update_bits(st->regmap, ADS1120_REG_CONFIG0,
+ ADS1120_CFG0_GAIN_MASK,
+ FIELD_PREP(ADS1120_CFG0_GAIN_MASK, i));
+}
+
+static int ads1120_read_raw(struct iio_dev *indio_dev,I think this would need to be `*val2 = ilog2(gain) + 15`.
+ struct iio_chan_spec const *chan,
+ int *val, int *val2, long mask)
+{
+ struct ads1120_state *st = iio_priv(indio_dev);
+ int ret, gain;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_RAW:
+ guard(mutex)(&st->lock);
+ ret = ads1120_read_measurement(st, chan, val);
+ if (ret)
+ return ret;
+ return IIO_VAL_INT;
+
+ case IIO_CHAN_INFO_SCALE:
+ /*
+ * Scale = Vref / (gain * 2^15)
+ * Return in format: val / 2^val2
+ */
+ gain = ads1120_get_gain(st);
+ if (gain < 0)
+ return gain;
+
+ *val = st->vref_mv;
+ *val2 = gain * 15;
+ return IIO_VAL_FRACTIONAL_LOG2;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+This check should not be needed because of .maxregister.
+/* Regmap read function for ADS1120 */
+static int ads1120_regmap_read(void *context, const void *reg_buf,
+ size_t reg_size, void *val_buf, size_t val_size)
+{
+ struct ads1120_state *st = context;
+ u8 reg = *(u8 *)reg_buf;
+ u8 *val = val_buf;
+ int ret;
+ struct spi_transfer xfer[2] = {
+ {
+ .tx_buf = st->data,
+ .len = 1,
+ }, {
+ .rx_buf = val,
+ .len = val_size,
+ }
+ };
+
+ if (reg > ADS1120_REG_CONFIG3)
+ return -EINVAL;
+I don't see anyting unusal about these read/write functions. We should
+ /* RREG command: 0010rr00 where rr is register address */
+ st->data[0] = ADS1120_CMD_RREG | (reg << 2);
+
+ ret = spi_sync_transfer(st->spi, xfer, ARRAY_SIZE(xfer));
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+/* Regmap write function for ADS1120 */
+static int ads1120_regmap_write(void *context, const void *data, size_t count)
+{
+ struct ads1120_state *st = context;
+ const u8 *buf = data;
+
+ if (count != 2)
+ return -EINVAL;
+
+ /* WREG command: 0100rr00 where rr is register address */
+ st->data[0] = ADS1120_CMD_WREG | (buf[0] << 2);
+ st->data[1] = buf[1];
+
+ return spi_write(st->spi, st->data, 2);
+}
be able to use the existing spi_regmap with the proper configuration
instead of making a custom regmap_bus.
The ADS1120 needs register address shifted by 2 bits
in command byte (reg << 2). I couldn't find a way to do this with standard
SPI regmap. If there's a configuration I'm missing, please point me to it and I'll gladly simplify.
+It's just a few extra lines to turn on the avdd supply here before
+static const struct regmap_bus ads1120_regmap_bus = {
+ .read = ads1120_regmap_read,
+ .write = ads1120_regmap_write,
+ .reg_format_endian_default = REGMAP_ENDIAN_BIG,
+ .val_format_endian_default = REGMAP_ENDIAN_BIG,
+};
+
+static const struct regmap_config ads1120_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = ADS1120_REG_CONFIG3,
+ .cache_type = REGCACHE_FLAT,
+};
+
+static int ads1120_init(struct ads1120_state *st)
+{
+ int ret;
resettting.
+If we want to wait for a later patch to support external reference,
+ ret = ads1120_reset(st);
+ if (ret)
+ return dev_err_probe(&st->spi->dev, ret,
+ "Failed to reset device\n");
+
+ /*
+ * Configure Register 0:
+ * - Input MUX: AIN0/AVSS
+ * - Gain: 1
+ * - PGA bypass enabled. When gain is set > 4, this bit is
+ * automatically ignored by the hardware and PGA is enabled,
+ * so it's safe to leave it set.
+ */
+ ret = regmap_write(st->regmap, ADS1120_REG_CONFIG0,
+ FIELD_PREP(ADS1120_CFG0_MUX_MASK,
+ ADS1120_CFG0_MUX_AIN0_AVSS) |
+ FIELD_PREP(ADS1120_CFG0_GAIN_MASK,
+ ADS1120_CFG0_GAIN_1) |
+ ADS1120_CFG0_PGA_BYPASS);
+ if (ret)
+ return ret;
+
+ /*
+ * Configure Register 1:
+ * - Data rate: 20 SPS (for single-shot mode)
+ * - Operating mode: Normal
+ * - Conversion mode: Single-shot
+ * - Temperature sensor: Disabled
+ * - Burnout current: Disabled
+ */
+ ret = regmap_write(st->regmap, ADS1120_REG_CONFIG1,
+ FIELD_PREP(ADS1120_CFG1_DR_MASK,
+ ADS1120_CFG1_DR_20SPS) |
+ FIELD_PREP(ADS1120_CFG1_MODE_MASK,
+ ADS1120_CFG1_MODE_NORMAL) |
+ FIELD_PREP(ADS1120_CFG1_CM_MASK,
+ ADS1120_CFG1_CM_SINGLE) |
+ FIELD_PREP(ADS1120_CFG1_TS_EN, 0) |
+ FIELD_PREP(ADS1120_CFG1_BCS_EN, 0));
+ if (ret)
+ return ret;
+
+ /*
+ * Configure Register 2:
+ * - Voltage reference: Internal 2.048V
+ * - 50/60Hz rejection: Off
+ * - Power switch: Disabled
+ * - IDAC current: Off
+ */
+ ret = regmap_write(st->regmap, ADS1120_REG_CONFIG2,
+ FIELD_PREP(ADS1120_CFG2_VREF_MASK,
+ ADS1120_CFG2_VREF_INTERNAL) |
+ FIELD_PREP(ADS1120_CFG2_REJECT_MASK,
+ ADS1120_CFG2_REJECT_OFF) |
+ FIELD_PREP(ADS1120_CFG2_PSW_EN, 0) |
+ FIELD_PREP(ADS1120_CFG2_IDAC_MASK,
+ ADS1120_CFG2_IDAC_OFF));
+ if (ret)
+ return ret;
+
+ /*
+ * Configure Register 3:
+ * - IDAC1: Disabled
+ * - IDAC2: Disabled
+ * - DRDY mode: Only reflects data ready status
+ */
+ ret = regmap_write(st->regmap, ADS1120_REG_CONFIG3,
+ FIELD_PREP(ADS1120_CFG3_IDAC1_MASK,
+ ADS1120_CFG3_IDAC1_DISABLED) |
+ FIELD_PREP(ADS1120_CFG3_IDAC2_MASK,
+ ADS1120_CFG3_IDAC2_DISABLED) |
+ FIELD_PREP(ADS1120_CFG3_DRDYM_MASK,
+ ADS1120_CFG3_DRDYM_DRDY_ONLY));
+ if (ret)
+ return ret;
+
I would consider to check for the devicetree properties here and
print a warning that they aren't supported if present. Maybe others
think that is too much noise though? Especially if you plan to add
it soon.
+ st->vref_mv = ADS1120_VREF_INTERNAL_MV;
+
+ return 0;
+}
+