* [PATCH v9 0/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC
@ 2026-04-20 10:12 Antoniu Miclaus
2026-04-20 10:12 ` [PATCH v9 1/3] iio: backend: add devm_iio_backend_get_by_index() Antoniu Miclaus
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Antoniu Miclaus @ 2026-04-20 10:12 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Antoniu Miclaus,
Jonathan Cameron, David Lechner, Nuno Sá, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Olivier Moysan, linux-iio,
devicetree, linux-kernel
Add support for the AD4880, a dual-channel 20-bit 40MSPS SAR ADC with
integrated fully differential amplifiers (FDA).
Architecture notes:
The AD4880 is modeled as a single IIO device rather than two independent
devices because the channels share power supplies, a voltage reference,
the CNV conversion clock, and a single interleaved data output stream.
Splitting them into separate IIO devices would make synchronized
dual-channel capture impossible from userspace.
An MFD approach does not apply here either - the channels are not
functionally distinct sub-devices but identical ADC paths sharing a
common data interface.
Each channel has fully independent configuration registers accessible
through separate SPI chip selects, so per-channel regmaps are used with
no locking between them. The data path has no software involvement at
runtime: the CNV clock triggers simultaneous conversions and the device
outputs an interleaved bitstream captured directly by the IIO backend
(FPGA). spi_new_ancillary_device() handles the configuration path;
the IIO backend handles the data path.
The debugfs_reg_access callback is not exposed for the dual-channel
variant since the IIO framework provides a single (reg, val) interface
with no channel parameter, and exposing only one channel would be
misleading.
The AD4880 is a fairly unique part - having separate SPI config
interfaces per channel with a shared interleaved data output is not
a common pattern.
NOTE: The AD4880 driver has a cross-tree dependency on two SPI patches
that are queued in spi/for-7.1:
- ffef4123043c ("spi: allow ancillary devices to share parent's chip selects")
- 463279e58811 ("spi: add devm_spi_new_ancillary_device()")
Changes in v9:
- Rebase on jic23/togreg
- Add Conor's ack on dt-bindings patch
Antoniu Miclaus (3):
iio: backend: add devm_iio_backend_get_by_index()
dt-bindings: iio: adc: ad4080: add AD4880 support
iio: adc: ad4080: add support for AD4880 dual-channel ADC
.../bindings/iio/adc/adi,ad4080.yaml | 53 +++-
drivers/iio/adc/ad4080.c | 257 +++++++++++++-----
drivers/iio/industrialio-backend.c | 53 +++-
include/linux/iio/backend.h | 1 +
4 files changed, 285 insertions(+), 79 deletions(-)
base-commit: d2a4ec19d2a2e54c23b5180e939994d3da4a6b91
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v9 1/3] iio: backend: add devm_iio_backend_get_by_index()
2026-04-20 10:12 [PATCH v9 0/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC Antoniu Miclaus
@ 2026-04-20 10:12 ` Antoniu Miclaus
2026-04-20 10:12 ` [PATCH v9 2/3] dt-bindings: iio: adc: ad4080: add AD4880 support Antoniu Miclaus
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Antoniu Miclaus @ 2026-04-20 10:12 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Antoniu Miclaus,
Jonathan Cameron, David Lechner, Nuno Sá, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Olivier Moysan, linux-iio,
devicetree, linux-kernel
Add a new function to get an IIO backend by its index in the
io-backends device tree property. This is useful for multi-channel
devices that have multiple backends, where looking up by index is
more straightforward than using named backends.
Extract __devm_iio_backend_fwnode_get_by_index() from the existing
__devm_iio_backend_fwnode_get(), taking the index directly as a
parameter. The new public API devm_iio_backend_get_by_index() uses
the index to find the backend reference in the io-backends property,
avoiding the need for io-backend-names.
Reviewed-by: David Lechner <dlechner@baylibre.com>
Reviewed-by: Nuno Sá <nuno.sa@analog.com>
Signed-off-by: Antoniu Miclaus <antoniu.miclaus@analog.com>
---
Changes in v9:
- No changes
drivers/iio/industrialio-backend.c | 53 +++++++++++++++++++++---------
include/linux/iio/backend.h | 1 +
2 files changed, 39 insertions(+), 15 deletions(-)
diff --git a/drivers/iio/industrialio-backend.c b/drivers/iio/industrialio-backend.c
index 10e689f49441..138ebebc9c0d 100644
--- a/drivers/iio/industrialio-backend.c
+++ b/drivers/iio/industrialio-backend.c
@@ -964,23 +964,13 @@ int iio_backend_data_transfer_addr(struct iio_backend *back, u32 address)
}
EXPORT_SYMBOL_NS_GPL(iio_backend_data_transfer_addr, "IIO_BACKEND");
-static struct iio_backend *__devm_iio_backend_fwnode_get(struct device *dev, const char *name,
- struct fwnode_handle *fwnode)
+static struct iio_backend *__devm_iio_backend_fwnode_get_by_index(struct device *dev,
+ struct fwnode_handle *fwnode,
+ unsigned int index)
{
struct iio_backend *back;
- unsigned int index;
int ret;
- if (name) {
- ret = device_property_match_string(dev, "io-backend-names",
- name);
- if (ret < 0)
- return ERR_PTR(ret);
- index = ret;
- } else {
- index = 0;
- }
-
struct fwnode_handle *fwnode_back __free(fwnode_handle) =
fwnode_find_reference(fwnode, "io-backends", index);
if (IS_ERR(fwnode_back))
@@ -996,8 +986,7 @@ static struct iio_backend *__devm_iio_backend_fwnode_get(struct device *dev, con
if (ret)
return ERR_PTR(ret);
- if (name)
- back->idx = index;
+ back->idx = index;
return back;
}
@@ -1005,6 +994,24 @@ static struct iio_backend *__devm_iio_backend_fwnode_get(struct device *dev, con
return ERR_PTR(-EPROBE_DEFER);
}
+static struct iio_backend *__devm_iio_backend_fwnode_get(struct device *dev, const char *name,
+ struct fwnode_handle *fwnode)
+{
+ unsigned int index;
+ int ret;
+
+ if (name) {
+ ret = device_property_match_string(dev, "io-backend-names", name);
+ if (ret < 0)
+ return ERR_PTR(ret);
+ index = ret;
+ } else {
+ index = 0;
+ }
+
+ return __devm_iio_backend_fwnode_get_by_index(dev, fwnode, index);
+}
+
/**
* devm_iio_backend_get - Device managed backend device get
* @dev: Consumer device for the backend
@@ -1021,6 +1028,22 @@ struct iio_backend *devm_iio_backend_get(struct device *dev, const char *name)
}
EXPORT_SYMBOL_NS_GPL(devm_iio_backend_get, "IIO_BACKEND");
+/**
+ * devm_iio_backend_get_by_index - Device managed backend device get by index
+ * @dev: Consumer device for the backend
+ * @index: Index of the backend in the io-backends property
+ *
+ * Gets the backend at @index associated with @dev.
+ *
+ * RETURNS:
+ * A backend pointer, negative error pointer otherwise.
+ */
+struct iio_backend *devm_iio_backend_get_by_index(struct device *dev, unsigned int index)
+{
+ return __devm_iio_backend_fwnode_get_by_index(dev, dev_fwnode(dev), index);
+}
+EXPORT_SYMBOL_NS_GPL(devm_iio_backend_get_by_index, "IIO_BACKEND");
+
/**
* devm_iio_backend_fwnode_get - Device managed backend firmware node get
* @dev: Consumer device for the backend
diff --git a/include/linux/iio/backend.h b/include/linux/iio/backend.h
index 4d15c2a9802c..3f95ed1fdf9e 100644
--- a/include/linux/iio/backend.h
+++ b/include/linux/iio/backend.h
@@ -261,6 +261,7 @@ int iio_backend_extend_chan_spec(struct iio_backend *back,
bool iio_backend_has_caps(struct iio_backend *back, u32 caps);
void *iio_backend_get_priv(const struct iio_backend *conv);
struct iio_backend *devm_iio_backend_get(struct device *dev, const char *name);
+struct iio_backend *devm_iio_backend_get_by_index(struct device *dev, unsigned int index);
struct iio_backend *devm_iio_backend_fwnode_get(struct device *dev,
const char *name,
struct fwnode_handle *fwnode);
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v9 2/3] dt-bindings: iio: adc: ad4080: add AD4880 support
2026-04-20 10:12 [PATCH v9 0/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC Antoniu Miclaus
2026-04-20 10:12 ` [PATCH v9 1/3] iio: backend: add devm_iio_backend_get_by_index() Antoniu Miclaus
@ 2026-04-20 10:12 ` Antoniu Miclaus
2026-04-20 10:12 ` [PATCH v9 3/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC Antoniu Miclaus
2026-04-25 18:37 ` [PATCH v9 0/3] " Jonathan Cameron
3 siblings, 0 replies; 5+ messages in thread
From: Antoniu Miclaus @ 2026-04-20 10:12 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Antoniu Miclaus,
Jonathan Cameron, David Lechner, Nuno Sá, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Olivier Moysan, linux-iio,
devicetree, linux-kernel
Cc: Conor Dooley
Add support for the AD4880, a dual-channel 20-bit 40MSPS SAR ADC
with integrated fully differential amplifiers (FDA).
The AD4880 has two independent ADC channels, each with its own SPI
configuration interface. This requires:
- Two entries in reg property for primary and secondary channel
chip selects
- Two io-backends entries for the two data channels
Acked-by: Conor Dooley <conor.dooley@microchip.com>
Reviewed-by: David Lechner <dlechner@baylibre.com>
Signed-off-by: Antoniu Miclaus <antoniu.miclaus@analog.com>
---
Changes in v9:
- Add Conor's ack
.../bindings/iio/adc/adi,ad4080.yaml | 53 ++++++++++++++++++-
1 file changed, 51 insertions(+), 2 deletions(-)
diff --git a/Documentation/devicetree/bindings/iio/adc/adi,ad4080.yaml b/Documentation/devicetree/bindings/iio/adc/adi,ad4080.yaml
index 79df2696ef24..9c6a56c7c8ef 100644
--- a/Documentation/devicetree/bindings/iio/adc/adi,ad4080.yaml
+++ b/Documentation/devicetree/bindings/iio/adc/adi,ad4080.yaml
@@ -18,7 +18,11 @@ description: |
service a wide variety of precision, wide bandwidth data acquisition
applications.
+ The AD4880 is a dual-channel variant with two independent ADC channels,
+ each with its own SPI configuration interface.
+
https://www.analog.com/media/en/technical-documentation/data-sheets/ad4080.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/ad4880.pdf
$ref: /schemas/spi/spi-peripheral-props.yaml#
@@ -34,9 +38,15 @@ properties:
- adi,ad4086
- adi,ad4087
- adi,ad4088
+ - adi,ad4880
reg:
- maxItems: 1
+ minItems: 1
+ maxItems: 2
+ description:
+ SPI chip select(s). For single-channel devices, one chip select.
+ For multi-channel devices like AD4880, two chip selects are required
+ as each channel has its own SPI configuration interface.
spi-max-frequency:
description: Configuration of the SPI bus.
@@ -60,7 +70,10 @@ properties:
vrefin-supply: true
io-backends:
- maxItems: 1
+ minItems: 1
+ items:
+ - description: Backend for channel A (primary)
+ - description: Backend for channel B (secondary)
adi,lvds-cnv-enable:
description: Enable the LVDS signal type on the CNV pin. Default is CMOS.
@@ -81,6 +94,25 @@ required:
- vdd33-supply
- vrefin-supply
+allOf:
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: adi,ad4880
+ then:
+ properties:
+ reg:
+ minItems: 2
+ io-backends:
+ minItems: 2
+ else:
+ properties:
+ reg:
+ maxItems: 1
+ io-backends:
+ maxItems: 1
+
additionalProperties: false
examples:
@@ -101,4 +133,21 @@ examples:
io-backends = <&iio_backend>;
};
};
+ - |
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ adc@0 {
+ compatible = "adi,ad4880";
+ reg = <0>, <1>;
+ spi-max-frequency = <10000000>;
+ vdd33-supply = <&vdd33>;
+ vddldo-supply = <&vddldo>;
+ vrefin-supply = <&vrefin>;
+ clocks = <&cnv>;
+ clock-names = "cnv";
+ io-backends = <&iio_backend_cha>, <&iio_backend_chb>;
+ };
+ };
...
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v9 3/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC
2026-04-20 10:12 [PATCH v9 0/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC Antoniu Miclaus
2026-04-20 10:12 ` [PATCH v9 1/3] iio: backend: add devm_iio_backend_get_by_index() Antoniu Miclaus
2026-04-20 10:12 ` [PATCH v9 2/3] dt-bindings: iio: adc: ad4080: add AD4880 support Antoniu Miclaus
@ 2026-04-20 10:12 ` Antoniu Miclaus
2026-04-25 18:37 ` [PATCH v9 0/3] " Jonathan Cameron
3 siblings, 0 replies; 5+ messages in thread
From: Antoniu Miclaus @ 2026-04-20 10:12 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Antoniu Miclaus,
Jonathan Cameron, David Lechner, Nuno Sá, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Olivier Moysan, linux-iio,
devicetree, linux-kernel
Add support for the AD4880, a dual-channel 20-bit 40MSPS SAR ADC with
integrated fully differential amplifiers (FDA).
The AD4880 has two independent ADC channels, each with its own SPI
configuration interface. The driver uses spi_new_ancillary_device() to
create an additional SPI device for the second channel, allowing both
channels to share the same SPI bus with different chip selects.
Reviewed-by: David Lechner <dlechner@baylibre.com>
Reviewed-by: Nuno Sá <nuno.sa@analog.com>
Signed-off-by: Antoniu Miclaus <antoniu.miclaus@analog.com>
---
Changes in v9:
- No changes
drivers/iio/adc/ad4080.c | 257 +++++++++++++++++++++++++++++----------
1 file changed, 195 insertions(+), 62 deletions(-)
diff --git a/drivers/iio/adc/ad4080.c b/drivers/iio/adc/ad4080.c
index 204ad198342b..265d85ac171a 100644
--- a/drivers/iio/adc/ad4080.c
+++ b/drivers/iio/adc/ad4080.c
@@ -16,6 +16,7 @@
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/mutex.h>
+#include <linux/property.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
#include <linux/spi/spi.h>
@@ -134,6 +135,9 @@
#define AD4086_CHIP_ID 0x0056
#define AD4087_CHIP_ID 0x0057
#define AD4088_CHIP_ID 0x0058
+#define AD4880_CHIP_ID 0x0750
+
+#define AD4080_MAX_CHANNELS 2
#define AD4080_LVDS_CNV_CLK_CNT_MAX 7
@@ -179,8 +183,9 @@ struct ad4080_chip_info {
};
struct ad4080_state {
- struct regmap *regmap;
- struct iio_backend *back;
+ struct spi_device *spi[AD4080_MAX_CHANNELS];
+ struct regmap *regmap[AD4080_MAX_CHANNELS];
+ struct iio_backend *back[AD4080_MAX_CHANNELS];
const struct ad4080_chip_info *info;
/*
* Synchronize access to members the of driver state, and ensure
@@ -189,7 +194,7 @@ struct ad4080_state {
struct mutex lock;
unsigned int num_lanes;
unsigned long clk_rate;
- enum ad4080_filter_type filter_type;
+ enum ad4080_filter_type filter_type[AD4080_MAX_CHANNELS];
bool lvds_cnv_en;
};
@@ -206,9 +211,9 @@ static int ad4080_reg_access(struct iio_dev *indio_dev, unsigned int reg,
struct ad4080_state *st = iio_priv(indio_dev);
if (readval)
- return regmap_read(st->regmap, reg, readval);
+ return regmap_read(st->regmap[0], reg, readval);
- return regmap_write(st->regmap, reg, writeval);
+ return regmap_write(st->regmap[0], reg, writeval);
}
static int ad4080_get_scale(struct ad4080_state *st, int *val, int *val2)
@@ -229,8 +234,9 @@ static unsigned int ad4080_get_dec_rate(struct iio_dev *dev,
struct ad4080_state *st = iio_priv(dev);
int ret;
unsigned int data;
+ unsigned int ch = chan->channel;
- ret = regmap_read(st->regmap, AD4080_REG_FILTER_CONFIG, &data);
+ ret = regmap_read(st->regmap[ch], AD4080_REG_FILTER_CONFIG, &data);
if (ret)
return ret;
@@ -242,13 +248,14 @@ static int ad4080_set_dec_rate(struct iio_dev *dev,
unsigned int mode)
{
struct ad4080_state *st = iio_priv(dev);
+ unsigned int ch = chan->channel;
guard(mutex)(&st->lock);
- if ((st->filter_type >= SINC_5 && mode >= 512) || mode < 2)
+ if ((st->filter_type[ch] >= SINC_5 && mode >= 512) || mode < 2)
return -EINVAL;
- return regmap_update_bits(st->regmap, AD4080_REG_FILTER_CONFIG,
+ return regmap_update_bits(st->regmap[ch], AD4080_REG_FILTER_CONFIG,
AD4080_FILTER_CONFIG_SINC_DEC_RATE_MSK,
FIELD_PREP(AD4080_FILTER_CONFIG_SINC_DEC_RATE_MSK,
(ilog2(mode) - 1)));
@@ -268,15 +275,15 @@ static int ad4080_read_raw(struct iio_dev *indio_dev,
dec_rate = ad4080_get_dec_rate(indio_dev, chan);
if (dec_rate < 0)
return dec_rate;
- if (st->filter_type == SINC_5_COMP)
+ if (st->filter_type[chan->channel] == SINC_5_COMP)
dec_rate *= 2;
- if (st->filter_type)
+ if (st->filter_type[chan->channel])
*val = DIV_ROUND_CLOSEST(st->clk_rate, dec_rate);
else
*val = st->clk_rate;
return IIO_VAL_INT;
case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
- if (st->filter_type == FILTER_NONE) {
+ if (st->filter_type[chan->channel] == FILTER_NONE) {
*val = 1;
} else {
*val = ad4080_get_dec_rate(indio_dev, chan);
@@ -297,7 +304,7 @@ static int ad4080_write_raw(struct iio_dev *indio_dev,
switch (mask) {
case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
- if (st->filter_type == FILTER_NONE && val > 1)
+ if (st->filter_type[chan->channel] == FILTER_NONE && val > 1)
return -EINVAL;
return ad4080_set_dec_rate(indio_dev, chan, val);
@@ -306,23 +313,23 @@ static int ad4080_write_raw(struct iio_dev *indio_dev,
}
}
-static int ad4080_lvds_sync_write(struct ad4080_state *st)
+static int ad4080_lvds_sync_write(struct ad4080_state *st, unsigned int ch)
{
- struct device *dev = regmap_get_device(st->regmap);
+ struct device *dev = regmap_get_device(st->regmap[ch]);
int ret;
- ret = regmap_set_bits(st->regmap, AD4080_REG_ADC_DATA_INTF_CONFIG_A,
+ ret = regmap_set_bits(st->regmap[ch], AD4080_REG_ADC_DATA_INTF_CONFIG_A,
AD4080_ADC_DATA_INTF_CONFIG_A_INTF_CHK_EN);
if (ret)
return ret;
- ret = iio_backend_interface_data_align(st->back, 10000);
+ ret = iio_backend_interface_data_align(st->back[ch], 10000);
if (ret)
return dev_err_probe(dev, ret,
"Data alignment process failed\n");
dev_dbg(dev, "Success: Pattern correct and Locked!\n");
- return regmap_clear_bits(st->regmap, AD4080_REG_ADC_DATA_INTF_CONFIG_A,
+ return regmap_clear_bits(st->regmap[ch], AD4080_REG_ADC_DATA_INTF_CONFIG_A,
AD4080_ADC_DATA_INTF_CONFIG_A_INTF_CHK_EN);
}
@@ -331,9 +338,10 @@ static int ad4080_get_filter_type(struct iio_dev *dev,
{
struct ad4080_state *st = iio_priv(dev);
unsigned int data;
+ unsigned int ch = chan->channel;
int ret;
- ret = regmap_read(st->regmap, AD4080_REG_FILTER_CONFIG, &data);
+ ret = regmap_read(st->regmap[ch], AD4080_REG_FILTER_CONFIG, &data);
if (ret)
return ret;
@@ -345,6 +353,7 @@ static int ad4080_set_filter_type(struct iio_dev *dev,
unsigned int mode)
{
struct ad4080_state *st = iio_priv(dev);
+ unsigned int ch = chan->channel;
int dec_rate;
int ret;
@@ -357,18 +366,18 @@ static int ad4080_set_filter_type(struct iio_dev *dev,
if (mode >= SINC_5 && dec_rate >= 512)
return -EINVAL;
- ret = iio_backend_filter_type_set(st->back, mode);
+ ret = iio_backend_filter_type_set(st->back[ch], mode);
if (ret)
return ret;
- ret = regmap_update_bits(st->regmap, AD4080_REG_FILTER_CONFIG,
+ ret = regmap_update_bits(st->regmap[ch], AD4080_REG_FILTER_CONFIG,
AD4080_FILTER_CONFIG_FILTER_SEL_MSK,
FIELD_PREP(AD4080_FILTER_CONFIG_FILTER_SEL_MSK,
mode));
if (ret)
return ret;
- st->filter_type = mode;
+ st->filter_type[ch] = mode;
return 0;
}
@@ -382,14 +391,14 @@ static int ad4080_read_avail(struct iio_dev *indio_dev,
switch (mask) {
case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
- switch (st->filter_type) {
+ switch (st->filter_type[chan->channel]) {
case FILTER_NONE:
*vals = ad4080_dec_rate_none;
*length = ARRAY_SIZE(ad4080_dec_rate_none);
break;
default:
*vals = ad4080_dec_rate_avail;
- *length = st->filter_type >= SINC_5 ?
+ *length = st->filter_type[chan->channel] >= SINC_5 ?
(ARRAY_SIZE(ad4080_dec_rate_avail) - 2) :
ARRAY_SIZE(ad4080_dec_rate_avail);
break;
@@ -401,6 +410,28 @@ static int ad4080_read_avail(struct iio_dev *indio_dev,
}
}
+static int ad4880_update_scan_mode(struct iio_dev *indio_dev,
+ const unsigned long *scan_mask)
+{
+ struct ad4080_state *st = iio_priv(indio_dev);
+ int ret;
+
+ for (unsigned int ch = 0; ch < st->info->num_channels; ch++) {
+ /*
+ * Each backend has a single channel (channel 0 from the
+ * backend's perspective), so always use channel index 0.
+ */
+ if (test_bit(ch, scan_mask))
+ ret = iio_backend_chan_enable(st->back[ch], 0);
+ else
+ ret = iio_backend_chan_disable(st->back[ch], 0);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
static const struct iio_info ad4080_iio_info = {
.debugfs_reg_access = ad4080_reg_access,
.read_raw = ad4080_read_raw,
@@ -408,6 +439,19 @@ static const struct iio_info ad4080_iio_info = {
.read_avail = ad4080_read_avail,
};
+/*
+ * AD4880 needs update_scan_mode to enable/disable individual backend channels.
+ * Single-channel devices don't need this as their backends may not implement
+ * chan_enable/chan_disable operations.
+ */
+static const struct iio_info ad4880_iio_info = {
+ .debugfs_reg_access = ad4080_reg_access,
+ .read_raw = ad4080_read_raw,
+ .write_raw = ad4080_write_raw,
+ .read_avail = ad4080_read_avail,
+ .update_scan_mode = ad4880_update_scan_mode,
+};
+
static const struct iio_enum ad4080_filter_type_enum = {
.items = ad4080_filter_type_iio_enum,
.num_items = ARRAY_SIZE(ad4080_filter_type_iio_enum),
@@ -422,17 +466,28 @@ static struct iio_chan_spec_ext_info ad4080_ext_info[] = {
{ }
};
-#define AD4080_CHANNEL_DEFINE(bits, storage) { \
+/*
+ * AD4880 needs per-channel filter configuration since each channel has
+ * its own independent ADC with separate SPI interface.
+ */
+static struct iio_chan_spec_ext_info ad4880_ext_info[] = {
+ IIO_ENUM("filter_type", IIO_SEPARATE, &ad4080_filter_type_enum),
+ IIO_ENUM_AVAILABLE("filter_type", IIO_SEPARATE,
+ &ad4080_filter_type_enum),
+ { }
+};
+
+#define AD4080_CHANNEL_DEFINE(bits, storage, idx) { \
.type = IIO_VOLTAGE, \
.indexed = 1, \
- .channel = 0, \
+ .channel = (idx), \
.info_mask_separate = BIT(IIO_CHAN_INFO_SCALE), \
.info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SAMP_FREQ) | \
BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \
.info_mask_shared_by_all_available = \
BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \
.ext_info = ad4080_ext_info, \
- .scan_index = 0, \
+ .scan_index = (idx), \
.scan_type = { \
.sign = 's', \
.realbits = (bits), \
@@ -440,23 +495,51 @@ static struct iio_chan_spec_ext_info ad4080_ext_info[] = {
}, \
}
-static const struct iio_chan_spec ad4080_channel = AD4080_CHANNEL_DEFINE(20, 32);
+/*
+ * AD4880 has per-channel attributes (filter_type, oversampling_ratio,
+ * sampling_frequency) since each channel has its own independent ADC
+ * with separate SPI configuration interface.
+ */
+#define AD4880_CHANNEL_DEFINE(bits, storage, idx) { \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .channel = (idx), \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_SCALE) | \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ) | \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \
+ .info_mask_separate_available = \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \
+ .ext_info = ad4880_ext_info, \
+ .scan_index = (idx), \
+ .scan_type = { \
+ .sign = 's', \
+ .realbits = (bits), \
+ .storagebits = (storage), \
+ }, \
+}
-static const struct iio_chan_spec ad4081_channel = AD4080_CHANNEL_DEFINE(20, 32);
+static const struct iio_chan_spec ad4080_channel = AD4080_CHANNEL_DEFINE(20, 32, 0);
-static const struct iio_chan_spec ad4082_channel = AD4080_CHANNEL_DEFINE(20, 32);
+static const struct iio_chan_spec ad4081_channel = AD4080_CHANNEL_DEFINE(20, 32, 0);
-static const struct iio_chan_spec ad4083_channel = AD4080_CHANNEL_DEFINE(16, 16);
+static const struct iio_chan_spec ad4082_channel = AD4080_CHANNEL_DEFINE(20, 32, 0);
-static const struct iio_chan_spec ad4084_channel = AD4080_CHANNEL_DEFINE(16, 16);
+static const struct iio_chan_spec ad4083_channel = AD4080_CHANNEL_DEFINE(16, 16, 0);
-static const struct iio_chan_spec ad4085_channel = AD4080_CHANNEL_DEFINE(16, 16);
+static const struct iio_chan_spec ad4084_channel = AD4080_CHANNEL_DEFINE(16, 16, 0);
-static const struct iio_chan_spec ad4086_channel = AD4080_CHANNEL_DEFINE(14, 16);
+static const struct iio_chan_spec ad4085_channel = AD4080_CHANNEL_DEFINE(16, 16, 0);
-static const struct iio_chan_spec ad4087_channel = AD4080_CHANNEL_DEFINE(14, 16);
+static const struct iio_chan_spec ad4086_channel = AD4080_CHANNEL_DEFINE(14, 16, 0);
-static const struct iio_chan_spec ad4088_channel = AD4080_CHANNEL_DEFINE(14, 16);
+static const struct iio_chan_spec ad4087_channel = AD4080_CHANNEL_DEFINE(14, 16, 0);
+
+static const struct iio_chan_spec ad4088_channel = AD4080_CHANNEL_DEFINE(14, 16, 0);
+
+static const struct iio_chan_spec ad4880_channels[] = {
+ AD4880_CHANNEL_DEFINE(20, 32, 0),
+ AD4880_CHANNEL_DEFINE(20, 32, 1),
+};
static const struct ad4080_chip_info ad4080_chip_info = {
.name = "ad4080",
@@ -548,25 +631,34 @@ static const struct ad4080_chip_info ad4088_chip_info = {
.lvds_cnv_clk_cnt_max = 8,
};
-static int ad4080_setup(struct iio_dev *indio_dev)
+static const struct ad4080_chip_info ad4880_chip_info = {
+ .name = "ad4880",
+ .product_id = AD4880_CHIP_ID,
+ .scale_table = ad4080_scale_table,
+ .num_scales = ARRAY_SIZE(ad4080_scale_table),
+ .num_channels = 2,
+ .channels = ad4880_channels,
+ .lvds_cnv_clk_cnt_max = AD4080_LVDS_CNV_CLK_CNT_MAX,
+};
+
+static int ad4080_setup_channel(struct ad4080_state *st, unsigned int ch)
{
- struct ad4080_state *st = iio_priv(indio_dev);
- struct device *dev = regmap_get_device(st->regmap);
+ struct device *dev = regmap_get_device(st->regmap[ch]);
__le16 id_le;
u16 id;
int ret;
- ret = regmap_write(st->regmap, AD4080_REG_INTERFACE_CONFIG_A,
+ ret = regmap_write(st->regmap[ch], AD4080_REG_INTERFACE_CONFIG_A,
AD4080_INTERFACE_CONFIG_A_SW_RESET);
if (ret)
return ret;
- ret = regmap_write(st->regmap, AD4080_REG_INTERFACE_CONFIG_A,
+ ret = regmap_write(st->regmap[ch], AD4080_REG_INTERFACE_CONFIG_A,
AD4080_INTERFACE_CONFIG_A_SDO_ENABLE);
if (ret)
return ret;
- ret = regmap_bulk_read(st->regmap, AD4080_REG_PRODUCT_ID_L, &id_le,
+ ret = regmap_bulk_read(st->regmap[ch], AD4080_REG_PRODUCT_ID_L, &id_le,
sizeof(id_le));
if (ret)
return ret;
@@ -575,18 +667,18 @@ static int ad4080_setup(struct iio_dev *indio_dev)
if (id != st->info->product_id)
dev_info(dev, "Unrecognized CHIP_ID 0x%X\n", id);
- ret = regmap_set_bits(st->regmap, AD4080_REG_GPIO_CONFIG_A,
+ ret = regmap_set_bits(st->regmap[ch], AD4080_REG_GPIO_CONFIG_A,
AD4080_GPIO_CONFIG_A_GPO_1_EN);
if (ret)
return ret;
- ret = regmap_write(st->regmap, AD4080_REG_GPIO_CONFIG_B,
+ ret = regmap_write(st->regmap[ch], AD4080_REG_GPIO_CONFIG_B,
FIELD_PREP(AD4080_GPIO_CONFIG_B_GPIO_1_SEL_MSK,
AD4080_GPIO_CONFIG_B_GPIO_FILTER_RES_RDY));
if (ret)
return ret;
- ret = iio_backend_num_lanes_set(st->back, st->num_lanes);
+ ret = iio_backend_num_lanes_set(st->back[ch], st->num_lanes);
if (ret)
return ret;
@@ -594,7 +686,7 @@ static int ad4080_setup(struct iio_dev *indio_dev)
return 0;
/* Set maximum LVDS Data Transfer Latency */
- ret = regmap_update_bits(st->regmap,
+ ret = regmap_update_bits(st->regmap[ch],
AD4080_REG_ADC_DATA_INTF_CONFIG_B,
AD4080_ADC_DATA_INTF_CONFIG_B_LVDS_CNV_CLK_CNT_MSK,
FIELD_PREP(AD4080_ADC_DATA_INTF_CONFIG_B_LVDS_CNV_CLK_CNT_MSK,
@@ -603,24 +695,38 @@ static int ad4080_setup(struct iio_dev *indio_dev)
return ret;
if (st->num_lanes > 1) {
- ret = regmap_set_bits(st->regmap, AD4080_REG_ADC_DATA_INTF_CONFIG_A,
+ ret = regmap_set_bits(st->regmap[ch], AD4080_REG_ADC_DATA_INTF_CONFIG_A,
AD4080_ADC_DATA_INTF_CONFIG_A_SPI_LVDS_LANES);
if (ret)
return ret;
}
- ret = regmap_set_bits(st->regmap,
+ ret = regmap_set_bits(st->regmap[ch],
AD4080_REG_ADC_DATA_INTF_CONFIG_B,
AD4080_ADC_DATA_INTF_CONFIG_B_LVDS_CNV_EN);
if (ret)
return ret;
- return ad4080_lvds_sync_write(st);
+ return ad4080_lvds_sync_write(st, ch);
}
-static int ad4080_properties_parse(struct ad4080_state *st)
+static int ad4080_setup(struct iio_dev *indio_dev)
+{
+ struct ad4080_state *st = iio_priv(indio_dev);
+ int ret;
+
+ for (unsigned int ch = 0; ch < st->info->num_channels; ch++) {
+ ret = ad4080_setup_channel(st, ch);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ad4080_properties_parse(struct ad4080_state *st,
+ struct device *dev)
{
- struct device *dev = regmap_get_device(st->regmap);
st->lvds_cnv_en = device_property_read_bool(dev, "adi,lvds-cnv-enable");
@@ -655,14 +761,28 @@ static int ad4080_probe(struct spi_device *spi)
return dev_err_probe(dev, ret,
"failed to get and enable supplies\n");
- st->regmap = devm_regmap_init_spi(spi, &ad4080_regmap_config);
- if (IS_ERR(st->regmap))
- return PTR_ERR(st->regmap);
+ /* Setup primary SPI device (channel 0) */
+ st->spi[0] = spi;
+ st->regmap[0] = devm_regmap_init_spi(spi, &ad4080_regmap_config);
+ if (IS_ERR(st->regmap[0]))
+ return PTR_ERR(st->regmap[0]);
st->info = spi_get_device_match_data(spi);
if (!st->info)
return -ENODEV;
+ /* Setup ancillary SPI devices for additional channels */
+ for (unsigned int ch = 1; ch < st->info->num_channels; ch++) {
+ st->spi[ch] = devm_spi_new_ancillary_device(spi, spi_get_chipselect(spi, ch));
+ if (IS_ERR(st->spi[ch]))
+ return dev_err_probe(dev, PTR_ERR(st->spi[ch]),
+ "failed to register ancillary device\n");
+
+ st->regmap[ch] = devm_regmap_init_spi(st->spi[ch], &ad4080_regmap_config);
+ if (IS_ERR(st->regmap[ch]))
+ return PTR_ERR(st->regmap[ch]);
+ }
+
ret = devm_mutex_init(dev, &st->lock);
if (ret)
return ret;
@@ -670,9 +790,10 @@ static int ad4080_probe(struct spi_device *spi)
indio_dev->name = st->info->name;
indio_dev->channels = st->info->channels;
indio_dev->num_channels = st->info->num_channels;
- indio_dev->info = &ad4080_iio_info;
+ indio_dev->info = st->info->num_channels > 1 ?
+ &ad4880_iio_info : &ad4080_iio_info;
- ret = ad4080_properties_parse(st);
+ ret = ad4080_properties_parse(st, dev);
if (ret)
return ret;
@@ -682,15 +803,25 @@ static int ad4080_probe(struct spi_device *spi)
st->clk_rate = clk_get_rate(clk);
- st->back = devm_iio_backend_get(dev, NULL);
- if (IS_ERR(st->back))
- return PTR_ERR(st->back);
+ /* Get backends for all channels */
+ for (unsigned int ch = 0; ch < st->info->num_channels; ch++) {
+ st->back[ch] = devm_iio_backend_get_by_index(dev, ch);
+ if (IS_ERR(st->back[ch]))
+ return PTR_ERR(st->back[ch]);
- ret = devm_iio_backend_request_buffer(dev, st->back, indio_dev);
- if (ret)
- return ret;
+ ret = devm_iio_backend_enable(dev, st->back[ch]);
+ if (ret)
+ return ret;
+ }
- ret = devm_iio_backend_enable(dev, st->back);
+ /*
+ * Request buffer from the first backend only. For multi-channel
+ * devices (e.g., AD4880), the FPGA uses two axi_ad408x IP instances
+ * (one per ADC channel) whose outputs are combined by a packer block
+ * that interleaves all channel data into a single DMA stream routed
+ * through the first backend's clock domain.
+ */
+ ret = devm_iio_backend_request_buffer(dev, st->back[0], indio_dev);
if (ret)
return ret;
@@ -711,6 +842,7 @@ static const struct spi_device_id ad4080_id[] = {
{ "ad4086", (kernel_ulong_t)&ad4086_chip_info },
{ "ad4087", (kernel_ulong_t)&ad4087_chip_info },
{ "ad4088", (kernel_ulong_t)&ad4088_chip_info },
+ { "ad4880", (kernel_ulong_t)&ad4880_chip_info },
{ }
};
MODULE_DEVICE_TABLE(spi, ad4080_id);
@@ -725,6 +857,7 @@ static const struct of_device_id ad4080_of_match[] = {
{ .compatible = "adi,ad4086", &ad4086_chip_info },
{ .compatible = "adi,ad4087", &ad4087_chip_info },
{ .compatible = "adi,ad4088", &ad4088_chip_info },
+ { .compatible = "adi,ad4880", &ad4880_chip_info },
{ }
};
MODULE_DEVICE_TABLE(of, ad4080_of_match);
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH v9 0/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC
2026-04-20 10:12 [PATCH v9 0/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC Antoniu Miclaus
` (2 preceding siblings ...)
2026-04-20 10:12 ` [PATCH v9 3/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC Antoniu Miclaus
@ 2026-04-25 18:37 ` Jonathan Cameron
3 siblings, 0 replies; 5+ messages in thread
From: Jonathan Cameron @ 2026-04-25 18:37 UTC (permalink / raw)
To: Antoniu Miclaus
Cc: Lars-Peter Clausen, Michael Hennerich, David Lechner,
Nuno Sá, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Olivier Moysan, linux-iio, devicetree, linux-kernel
On Mon, 20 Apr 2026 13:12:22 +0300
Antoniu Miclaus <antoniu.miclaus@analog.com> wrote:
> Add support for the AD4880, a dual-channel 20-bit 40MSPS SAR ADC with
> integrated fully differential amplifiers (FDA).
>
> Architecture notes:
>
> The AD4880 is modeled as a single IIO device rather than two independent
> devices because the channels share power supplies, a voltage reference,
> the CNV conversion clock, and a single interleaved data output stream.
> Splitting them into separate IIO devices would make synchronized
> dual-channel capture impossible from userspace.
>
> An MFD approach does not apply here either - the channels are not
> functionally distinct sub-devices but identical ADC paths sharing a
> common data interface.
>
> Each channel has fully independent configuration registers accessible
> through separate SPI chip selects, so per-channel regmaps are used with
> no locking between them. The data path has no software involvement at
> runtime: the CNV clock triggers simultaneous conversions and the device
> outputs an interleaved bitstream captured directly by the IIO backend
> (FPGA). spi_new_ancillary_device() handles the configuration path;
> the IIO backend handles the data path.
>
> The debugfs_reg_access callback is not exposed for the dual-channel
> variant since the IIO framework provides a single (reg, val) interface
> with no channel parameter, and exposing only one channel would be
> misleading.
>
> The AD4880 is a fairly unique part - having separate SPI config
> interfaces per channel with a shared interleaved data output is not
> a common pattern.
>
> NOTE: The AD4880 driver has a cross-tree dependency on two SPI patches
> that are queued in spi/for-7.1:
>
> - ffef4123043c ("spi: allow ancillary devices to share parent's chip selects")
> - 463279e58811 ("spi: add devm_spi_new_ancillary_device()")
>
Applied to the testing branch of iio.git - char-misc pull requests were picked
up so I've rebased on top of that and the SPI patches are therefore available.
I'll rebase again on rc1 once available then push out as togreg for linux-next
to pick up.
thanks,
Jonathan
> Changes in v9:
> - Rebase on jic23/togreg
> - Add Conor's ack on dt-bindings patch
>
> Antoniu Miclaus (3):
> iio: backend: add devm_iio_backend_get_by_index()
> dt-bindings: iio: adc: ad4080: add AD4880 support
> iio: adc: ad4080: add support for AD4880 dual-channel ADC
>
> .../bindings/iio/adc/adi,ad4080.yaml | 53 +++-
> drivers/iio/adc/ad4080.c | 257 +++++++++++++-----
> drivers/iio/industrialio-backend.c | 53 +++-
> include/linux/iio/backend.h | 1 +
> 4 files changed, 285 insertions(+), 79 deletions(-)
>
>
> base-commit: d2a4ec19d2a2e54c23b5180e939994d3da4a6b91
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-04-25 18:37 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-20 10:12 [PATCH v9 0/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC Antoniu Miclaus
2026-04-20 10:12 ` [PATCH v9 1/3] iio: backend: add devm_iio_backend_get_by_index() Antoniu Miclaus
2026-04-20 10:12 ` [PATCH v9 2/3] dt-bindings: iio: adc: ad4080: add AD4880 support Antoniu Miclaus
2026-04-20 10:12 ` [PATCH v9 3/3] iio: adc: ad4080: add support for AD4880 dual-channel ADC Antoniu Miclaus
2026-04-25 18:37 ` [PATCH v9 0/3] " Jonathan Cameron
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox