* [PATCH v2 1/7] dt-bindings: iio: adc: Add TI ADS126x ADC family
2026-06-28 5:36 [PATCH v2 0/7] iio: adc: Add TI ADS126X ADC family support Kurt Borja
@ 2026-06-28 5:36 ` Kurt Borja
2026-06-28 15:45 ` David Lechner
2026-06-28 5:36 ` [PATCH v2 2/7] iio: adc: Add ti-ads1262 driver Kurt Borja
` (5 subsequent siblings)
6 siblings, 1 reply; 14+ messages in thread
From: Kurt Borja @ 2026-06-28 5:36 UTC (permalink / raw)
To: Kurt Borja, Jonathan Cameron, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, David Lechner
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel, Jonathan Cameron
The ADS1262 and ADS1263 are 32-bit, 38.4-kSPS delta-sigma ADCs with an
integrated PGA, internal reference, excitation and burn-out current
sources for sensor biasing and diagnostics. The ADS1263 adds a second,
24-bit delta-sigma ADC (ADC2) for background measurements.
Each can configure it's own voltage reference source, the two excitation
current sources (IDAC), plus input and excitation channels rotation for
offset and IDAC mismatch cancellation. This lets the device drive and
ratiometrically measure RTDs and other resistive sensors.
Signed-off-by: Kurt Borja <kuurtb@gmail.com>
---
.../devicetree/bindings/iio/adc/ti,ads1262.yaml | 309 +++++++++++++++++++++
MAINTAINERS | 6 +
2 files changed, 315 insertions(+)
diff --git a/Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml b/Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
new file mode 100644
index 0000000000000000..2f4e812ae2af135a
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
@@ -0,0 +1,309 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/iio/adc/ti,ads1262.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: TI ADS1262/ADS1263 analog to digital converter
+
+maintainers:
+ - Kurt Borja <kuurtb@gmail.com>
+
+description: |
+ The ADS1262 and ADS1263 are 38.4-kSPS, delta-sigma (ΔΣ) ADCs with an
+ integrated PGA, reference, and internal fault monitors. The ADS1263 integrates
+ an auxiliary, 24-bit, ΔΣ ADC intended for background measurements.
+
+ Datasheets:
+ - ADS126x: https://www.ti.com/lit/ds/symlink/ads1262.pdf
+
+properties:
+ compatible:
+ oneOf:
+ - const: ti,ads1262
+ - items:
+ - const: ti,ads1263
+ - const: ti,ads1262
+
+ reg:
+ maxItems: 1
+
+ '#address-cells':
+ const: 1
+
+ '#size-cells':
+ const: 0
+
+ spi-max-frequency:
+ maximum: 8000000
+
+ spi-cpha: true
+
+ interrupts:
+ description: Data ready (DRDY) interrupt line.
+ maxItems: 1
+
+ start-gpios:
+ description: Start conversion control.
+ maxItems: 1
+
+ reset-gpios:
+ maxItems: 1
+
+ dvdd-supply:
+ description: Digital power supply.
+
+ avdd-supply:
+ description: Analog power supply.
+
+ refp-supply:
+ description: External positive voltage reference.
+
+ refn-supply:
+ description: External negative voltage reference.
+
+ ti,vbias:
+ $ref: /schemas/types.yaml#/definitions/flag
+ description: Enables the level-shift voltage on the AINCOM pin.
+
+ clocks:
+ maxItems: 1
+
+ '#io-channel-cells':
+ minimum: 1
+ maximum: 2
+
+ '#gpio-cells':
+ const: 2
+
+ gpio-controller: true
+
+patternProperties:
+ "^channel@[0-9]+$":
+ $ref: /schemas/iio/adc/adc.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ reg:
+ maxItems: 1
+
+ diff-channels:
+ description: |
+ Selects the analog input configuration for this channel. The first
+ value is the positive input and the second is the negative input.
+ The following values are available:
+ 0: AIN0 pin
+ 1: AIN1 pin
+ 2: AIN2 pin
+ 3: AIN3 pin
+ 4: AIN4 pin
+ 5: AIN5 pin
+ 6: AIN6 pin
+ 7: AIN7 pin
+ 8: AIN8 pin
+ 9: AIN9 pin
+ 10: AINCOM pin
+ 11: Temperature sensor monitor
+ 12: Analog power supply monitor
+ 13: Digital power supply monitor
+ 14: TDAC test signal
+ 15: Float (open connection)
+ items:
+ minimum: 0
+ maximum: 15
+
+ reference-sources:
+ minItems: 2
+ description:
+ Indicates the reference sources for this channel. The first and second
+ items are the positive and negative sources of the main ADC (ADC1).
+ The third item is the reference source of the secondary ADC (ADC2).
+ items:
+ - enum: [internal, ain0, ain2, ain4, avdd]
+ - enum: [internal, ain1, ain3, ain5, avss]
+ - enum: [internal, ain0-ain1, ain2-ain3, ain4-ain5, avdd-avss]
+
+ excitation-channels:
+ $ref: /schemas/types.yaml#/definitions/uint32-array
+ minItems: 2
+ maxItems: 2
+ description: |
+ Selects pins for the IDAC sources from the following options:
+ 0: AIN0
+ 1: AIN1
+ 2: AIN2
+ 3: AIN3
+ 4: AIN4
+ 5: AIN5
+ 6: AIN6
+ 7: AIN7
+ 8: AIN8
+ 9: AIN9
+ 10: AINCOM
+ 11: No Connection
+ The first value corresponds to IDAC1 and the second to IDAC2.
+ items:
+ minimum: 0
+ maximum: 11
+
+ excitation-current-nanoamp:
+ minItems: 2
+ maxItems: 2
+ description:
+ The first value corresponds to IDAC1 and the second to IDAC2.
+ items:
+ enum: [0, 50000, 100000, 250000, 500000, 750000, 1000000, 1500000,
+ 2000000, 2500000, 3000000]
+
+ burn-out-current-nanoamp:
+ description:
+ The ADC incorporates a sensor bias current source that can be used to
+ apply a small test current to diagnose broken sensor leads or problems
+ existing in the sensor.
+ enum: [0, 500, 2000, 10000, 50000, 200000]
+
+ ti,burn-out-resistor:
+ $ref: /schemas/types.yaml#/definitions/flag
+ description: |
+ Instead of a fixed current, the sensor bias (burn-out) current source
+ can be pulled using an internal 10 MΩ resistor.
+
+ ti,burn-out-polarity:
+ $ref: /schemas/types.yaml#/definitions/string
+ description:
+ The sensor bias can be configured to either pull-up or pull-down mode.
+ In pull-up mode, the current flows into the positive input and flows
+ out of the negative input. In pull-down mode, the polarities are
+ reversed.
+ enum: [pull-up, pull-down]
+
+ input-chopping:
+ $ref: /schemas/types.yaml#/definitions/flag
+ description:
+ When enabled, the ADC performs two internal conversions to cancel the
+ input offset voltage. The first conversion is taken with normal input
+ polarity. The ADC reverses the internal input polarity for the second
+ conversion. The difference of the two conversions is computed to yield
+ the final corrected result with the offset voltage removed.
+
+ ti,idac-chopping:
+ $ref: /schemas/types.yaml#/definitions/flag
+ description:
+ Automatically swap the IDAC1 and IDAC2 connections of alternate
+ conversions. The ADC averages the alternate conversions to eliminate
+ IDAC mismatch.
+
+ ti,pga-bypass:
+ $ref: /schemas/types.yaml#/definitions/flag
+ description: Bypass the Programmable Gain Amplifier (PGA).
+
+ dependencies:
+ excitation-channels: [excitation-current-nanoamp]
+ excitation-current-nanoamp: [excitation-channels]
+ burn-out-current-nanoamp:
+ not:
+ required:
+ - ti,burn-out-resistor
+
+ required:
+ - reg
+
+dependencies:
+ refn-supply: [refp-supply]
+
+required:
+ - compatible
+ - reg
+ - avdd-supply
+ - dvdd-supply
+ - '#address-cells'
+ - '#size-cells'
+
+allOf:
+ - $ref: /schemas/spi/spi-peripheral-props.yaml#
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: ti,ads1263
+ then:
+ properties:
+ '#io-channel-cells':
+ const: 2
+ patternProperties:
+ "^channel@[0-9]+$":
+ properties:
+ reference-sources:
+ minItems: 3
+ else:
+ properties:
+ '#io-channel-cells':
+ const: 1
+ patternProperties:
+ "^channel@[0-9]+$":
+ properties:
+ reference-sources:
+ maxItems: 2
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ adc@0 {
+ compatible = "ti,ads1262";
+ reg = <0>;
+ spi-max-frequency = <8000000>;
+ spi-cpha;
+ avdd-supply = <&avdd>;
+ dvdd-supply = <&dvdd>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ reset-gpios = <&gpio 18 GPIO_ACTIVE_LOW>;
+ interrupts-extended = <&gpio 10 IRQ_TYPE_EDGE_FALLING>;
+
+ channel@0 {
+ reg = <0>;
+ diff-channels = <0x0 0xA>;
+ };
+ };
+ };
+
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ adc@0 {
+ compatible = "ti,ads1263", "ti,ads1262";
+ reg = <0>;
+ spi-max-frequency = <8000000>;
+ spi-cpha;
+ avdd-supply = <&avdd>;
+ dvdd-supply = <&dvdd>;
+ refp-supply = <&refp>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ reset-gpios = <&gpio 18 GPIO_ACTIVE_LOW>;
+ interrupts-extended = <&gpio 10 IRQ_TYPE_EDGE_FALLING>;
+
+ channel@0 {
+ reg = <0>;
+ diff-channels = <0x4 0x5>;
+ reference-sources = "ain2", "ain3", "ain2-ain3";
+ excitation-channels = <0x1 0x6>;
+ excitation-current-nanoamp = <500000 500000>;
+ };
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 6c0471487974f145..9b83d294734b574d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -26923,6 +26923,12 @@ S: Maintained
F: Documentation/devicetree/bindings/iio/adc/ti,ads1018.yaml
F: drivers/iio/adc/ti-ads1018.c
+TI ADS1262 ADC DRIVER
+M: Kurt Borja <kuurtb@gmail.com>
+L: linux-iio@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
+
TI ADS7924 ADC DRIVER
M: Hugo Villeneuve <hvilleneuve@dimonoff.com>
L: linux-iio@vger.kernel.org
--
2.54.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* Re: [PATCH v2 1/7] dt-bindings: iio: adc: Add TI ADS126x ADC family
2026-06-28 5:36 ` [PATCH v2 1/7] dt-bindings: iio: adc: Add TI ADS126x ADC family Kurt Borja
@ 2026-06-28 15:45 ` David Lechner
2026-06-28 19:12 ` Kurt Borja
0 siblings, 1 reply; 14+ messages in thread
From: David Lechner @ 2026-06-28 15:45 UTC (permalink / raw)
To: Kurt Borja, Jonathan Cameron, Rob Herring, Krzysztof Kozlowski,
Conor Dooley
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel
On 6/28/26 12:36 AM, Kurt Borja wrote:
> The ADS1262 and ADS1263 are 32-bit, 38.4-kSPS delta-sigma ADCs with an
> integrated PGA, internal reference, excitation and burn-out current
> sources for sensor biasing and diagnostics. The ADS1263 adds a second,
> 24-bit delta-sigma ADC (ADC2) for background measurements.
>
> Each can configure it's own voltage reference source, the two excitation
> current sources (IDAC), plus input and excitation channels rotation for
> offset and IDAC mismatch cancellation. This lets the device drive and
> ratiometrically measure RTDs and other resistive sensors.
>
> Signed-off-by: Kurt Borja <kuurtb@gmail.com>
> ---
> .../devicetree/bindings/iio/adc/ti,ads1262.yaml | 309 +++++++++++++++++++++
> MAINTAINERS | 6 +
> 2 files changed, 315 insertions(+)
>
> diff --git a/Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml b/Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
> new file mode 100644
> index 0000000000000000..2f4e812ae2af135a
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
> @@ -0,0 +1,309 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/iio/adc/ti,ads1262.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: TI ADS1262/ADS1263 analog to digital converter
> +
> +maintainers:
> + - Kurt Borja <kuurtb@gmail.com>
> +
> +description: |
> + The ADS1262 and ADS1263 are 38.4-kSPS, delta-sigma (ΔΣ) ADCs with an
> + integrated PGA, reference, and internal fault monitors. The ADS1263 integrates
> + an auxiliary, 24-bit, ΔΣ ADC intended for background measurements.
> +
> + Datasheets:
> + - ADS126x: https://www.ti.com/lit/ds/symlink/ads1262.pdf
> +
> +properties:
> + compatible:
> + oneOf:
> + - const: ti,ads1262
> + - items:
> + - const: ti,ads1263
> + - const: ti,ads1262
> +
> + reg:
> + maxItems: 1
> +
> + '#address-cells':
> + const: 1
> +
> + '#size-cells':
> + const: 0
> +
> + spi-max-frequency:
> + maximum: 8000000
> +
> + spi-cpha: true
> +
> + interrupts:
> + description: Data ready (DRDY) interrupt line.
> + maxItems: 1
Technically, there are two pins with the DRDY signal, so we should have
two interrupts in order to be able to tell which one is wired up.
> +
> + start-gpios:
> + description: Start conversion control.
> + maxItems: 1
> +
> + reset-gpios:
> + maxItems: 1
> +
> + dvdd-supply:
> + description: Digital power supply.
> +
> + avdd-supply:
> + description: Analog power supply.
> +
> + refp-supply:
> + description: External positive voltage reference.
> +
> + refn-supply:
> + description: External negative voltage reference.
> +
Which pins are these? I see 4 possible external reference sources,
but all go through the AINx pins. So I would expect:
refp1-supply, refn1-supply, refp2-supply, refn2-supply,
refp3-supply, refn3-supply, refp4-supply, refn4-supply
Also, similar to the chip I am working on, I expect that these pins
could be connected to a resistor rather than a voltage source, so
could use additional bindings for that.
> + ti,vbias:
> + $ref: /schemas/types.yaml#/definitions/flag
> + description: Enables the level-shift voltage on the AINCOM pin.
VBIAS is a voltage source, so I would expect that to be modeled
as a regulator provider. (If we do that REFOUT should be included
as well.)
> +
> + clocks:
> + maxItems: 1
> +
> + '#io-channel-cells':
> + minimum: 1
> + maximum: 2
> +
> + '#gpio-cells':
> + const: 2
> +
> + gpio-controller: true
> +
> +patternProperties:
> + "^channel@[0-9]+$":
> + $ref: /schemas/iio/adc/adc.yaml#
> + unevaluatedProperties: false
> +
> + properties:
> + reg:
> + maxItems: 1
> +
If we want to allow single-ended/pseudo-differential inputs, then we should
also allow single-channel (positive pin) and common-mode-channel (negative
pin) properties.
This will also require additional common-mode-<N>-supply properties to allow
for the negative pin connected to something other than GND.
> + diff-channels:
> + description: |
> + Selects the analog input configuration for this channel. The first
> + value is the positive input and the second is the negative input.
> + The following values are available:
> + 0: AIN0 pin
> + 1: AIN1 pin
> + 2: AIN2 pin
> + 3: AIN3 pin
> + 4: AIN4 pin
> + 5: AIN5 pin
> + 6: AIN6 pin
> + 7: AIN7 pin
> + 8: AIN8 pin
> + 9: AIN9 pin
> + 10: AINCOM pin
> + 11: Temperature sensor monitor
> + 12: Analog power supply monitor
> + 13: Digital power supply monitor
> + 14: TDAC test signal
These are all internal signals, so not sure it makes sense to have
them in the devicetree. It would make more sense to have fixed
channels defined in the driver for these since they are always there.
We probably also need a separate property (a bool/flag?) to say that
this channel is a TDAC output rather than an analog input. Although
that is for testing, so maybe something to omit for now until we
actually have an application that uses it (to make sure we get it
right)?
> + 15: Float (open connection)
How could we have a differential input with one or both pins open?
Likely this will just be the setting for pins not specified as something
else in the devicetree.
> + items:
> + minimum: 0
> + maximum: 15
> +
> + reference-sources:
> + minItems: 2
> + description:
> + Indicates the reference sources for this channel. The first and second
> + items are the positive and negative sources of the main ADC (ADC1).
> + The third item is the reference source of the secondary ADC (ADC2).
> + items:
> + - enum: [internal, ain0, ain2, ain4, avdd]
> + - enum: [internal, ain1, ain3, ain5, avss]
> + - enum: [internal, ain0-ain1, ain2-ain3, ain4-ain5, avdd-avss]
> +
> + excitation-channels:
> + $ref: /schemas/types.yaml#/definitions/uint32-array
> + minItems: 2
minItems should be 1 since there are applications that only use one
current source.
> + maxItems: 2
> + description: |
> + Selects pins for the IDAC sources from the following options:
> + 0: AIN0
> + 1: AIN1
> + 2: AIN2
> + 3: AIN3
> + 4: AIN4
> + 5: AIN5
> + 6: AIN6
> + 7: AIN7
> + 8: AIN8
> + 9: AIN9
> + 10: AINCOM
> + 11: No Connection
Having a value for "no connection" doesn't make sense. We would just omit the
property or only have one item in the array.
> + The first value corresponds to IDAC1 and the second to IDAC2.
> + items:
> + minimum: 0
> + maximum: 11
> +
> + excitation-current-nanoamp:
> + minItems: 2
> + maxItems: 2
> + description:
> + The first value corresponds to IDAC1 and the second to IDAC2.
> + items:
> + enum: [0, 50000, 100000, 250000, 500000, 750000, 1000000, 1500000,
> + 2000000, 2500000, 3000000]
In the chip I am working on, I left out 0 with the intention that we
would just omit the property in that case. If we do include 0, then we
should also have `default: 0`. Not sure which way would be preferred by
others though.
> +
> + burn-out-current-nanoamp:
> + description:
> + The ADC incorporates a sensor bias current source that can be used to
> + apply a small test current to diagnose broken sensor leads or problems
> + existing in the sensor.
This description doesn't add anything that adc.yaml doesn't already say, so can
be omitted.
> + enum: [0, 500, 2000, 10000, 50000, 200000]
Same thing here about 0 value.
> +
> + ti,burn-out-resistor:
> + $ref: /schemas/types.yaml#/definitions/flag
> + description: |
Don't see a reason to need | here.
> + Instead of a fixed current, the sensor bias (burn-out) current source
> + can be pulled using an internal 10 MΩ resistor.
> +
> + ti,burn-out-polarity:
> + $ref: /schemas/types.yaml#/definitions/string
> + description:
> + The sensor bias can be configured to either pull-up or pull-down mode.
> + In pull-up mode, the current flows into the positive input and flows
> + out of the negative input. In pull-down mode, the polarities are
> + reversed.
> + enum: [pull-up, pull-down]
Needs a default.
> +
> + input-chopping:
> + $ref: /schemas/types.yaml#/definitions/flag
> + description:
> + When enabled, the ADC performs two internal conversions to cancel the
> + input offset voltage. The first conversion is taken with normal input
> + polarity. The ADC reverses the internal input polarity for the second
> + conversion. The difference of the two conversions is computed to yield
> + the final corrected result with the offset voltage removed.
> +
> + ti,idac-chopping:
I would call this ti,excitation-channel-chopping to match the excitation-channel
property. Or since this isn't a generic property, call it ti,idac-rotation to
match the datasheet.
> + $ref: /schemas/types.yaml#/definitions/flag
> + description:
> + Automatically swap the IDAC1 and IDAC2 connections of alternate
> + conversions. The ADC averages the alternate conversions to eliminate
> + IDAC mismatch.
> +
> + ti,pga-bypass:
> + $ref: /schemas/types.yaml#/definitions/flag
> + description: Bypass the Programmable Gain Amplifier (PGA).
Why would this need to be a DT property? I didn't read this datasheet
too much, but in other chips I have seen there are usually rules that
PGA has to be bypassed under certain conditions, but not others, so
this seems like something for the driver to handle rather than the
devicetree.
> +
> + dependencies:
> + excitation-channels: [excitation-current-nanoamp]
> + excitation-current-nanoamp: [excitation-channels]
> + burn-out-current-nanoamp:
> + not:
> + required:
> + - ti,burn-out-resistor
> +
> + required:
> + - reg
> +
> +dependencies:
> + refn-supply: [refp-supply]
> +
> +required:
> + - compatible
> + - reg
> + - avdd-supply
> + - dvdd-supply
> + - '#address-cells'
> + - '#size-cells'
> +
> +allOf:
> + - $ref: /schemas/spi/spi-peripheral-props.yaml#
> + - if:
> + properties:
> + compatible:
> + contains:
> + const: ti,ads1263
> + then:
> + properties:
> + '#io-channel-cells':
> + const: 2
> + patternProperties:
> + "^channel@[0-9]+$":
> + properties:
> + reference-sources:
> + minItems: 3
> + else:
> + properties:
> + '#io-channel-cells':
> + const: 1
> + patternProperties:
> + "^channel@[0-9]+$":
> + properties:
> + reference-sources:
> + maxItems: 2
> +
> +unevaluatedProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/gpio/gpio.h>
> + #include <dt-bindings/interrupt-controller/irq.h>
> +
> + spi {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + adc@0 {
> + compatible = "ti,ads1262";
> + reg = <0>;
> + spi-max-frequency = <8000000>;
> + spi-cpha;
> + avdd-supply = <&avdd>;
> + dvdd-supply = <&dvdd>;
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + reset-gpios = <&gpio 18 GPIO_ACTIVE_LOW>;
> + interrupts-extended = <&gpio 10 IRQ_TYPE_EDGE_FALLING>;
> +
> + channel@0 {
> + reg = <0>;
> + diff-channels = <0x0 0xA>;
I would just use decimal instead of hex for these. That is how they are
listed in the description anyway.
> + };
> + };
> + };
> +
> + - |
> + #include <dt-bindings/gpio/gpio.h>
> + #include <dt-bindings/interrupt-controller/irq.h>
> +
> + spi {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + adc@0 {
> + compatible = "ti,ads1263", "ti,ads1262";
> + reg = <0>;
> + spi-max-frequency = <8000000>;
> + spi-cpha;
> + avdd-supply = <&avdd>;
> + dvdd-supply = <&dvdd>;
> + refp-supply = <&refp>;
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + reset-gpios = <&gpio 18 GPIO_ACTIVE_LOW>;
> + interrupts-extended = <&gpio 10 IRQ_TYPE_EDGE_FALLING>;
> +
> + channel@0 {
> + reg = <0>;
> + diff-channels = <0x4 0x5>;
> + reference-sources = "ain2", "ain3", "ain2-ain3";
> + excitation-channels = <0x1 0x6>;
> + excitation-current-nanoamp = <500000 500000>;
> + };
> + };
> + };
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 6c0471487974f145..9b83d294734b574d 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -26923,6 +26923,12 @@ S: Maintained
> F: Documentation/devicetree/bindings/iio/adc/ti,ads1018.yaml
> F: drivers/iio/adc/ti-ads1018.c
>
> +TI ADS1262 ADC DRIVER
> +M: Kurt Borja <kuurtb@gmail.com>
> +L: linux-iio@vger.kernel.org
> +S: Maintained
> +F: Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
> +
> TI ADS7924 ADC DRIVER
> M: Hugo Villeneuve <hvilleneuve@dimonoff.com>
> L: linux-iio@vger.kernel.org
>
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH v2 1/7] dt-bindings: iio: adc: Add TI ADS126x ADC family
2026-06-28 15:45 ` David Lechner
@ 2026-06-28 19:12 ` Kurt Borja
0 siblings, 0 replies; 14+ messages in thread
From: Kurt Borja @ 2026-06-28 19:12 UTC (permalink / raw)
To: David Lechner, Kurt Borja, Jonathan Cameron, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel
On Sun Jun 28, 2026 at 10:45 AM -05, David Lechner wrote:
> On 6/28/26 12:36 AM, Kurt Borja wrote:
>> The ADS1262 and ADS1263 are 32-bit, 38.4-kSPS delta-sigma ADCs with an
>> integrated PGA, internal reference, excitation and burn-out current
>> sources for sensor biasing and diagnostics. The ADS1263 adds a second,
>> 24-bit delta-sigma ADC (ADC2) for background measurements.
>>
>> Each can configure it's own voltage reference source, the two excitation
>> current sources (IDAC), plus input and excitation channels rotation for
>> offset and IDAC mismatch cancellation. This lets the device drive and
>> ratiometrically measure RTDs and other resistive sensors.
>>
>> Signed-off-by: Kurt Borja <kuurtb@gmail.com>
>> ---
>> .../devicetree/bindings/iio/adc/ti,ads1262.yaml | 309 +++++++++++++++++++++
>> MAINTAINERS | 6 +
>> 2 files changed, 315 insertions(+)
>>
>> diff --git a/Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml b/Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
>> new file mode 100644
>> index 0000000000000000..2f4e812ae2af135a
>> --- /dev/null
>> +++ b/Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
>> @@ -0,0 +1,309 @@
>> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
>> +%YAML 1.2
>> +---
>> +$id: http://devicetree.org/schemas/iio/adc/ti,ads1262.yaml#
>> +$schema: http://devicetree.org/meta-schemas/core.yaml#
>> +
>> +title: TI ADS1262/ADS1263 analog to digital converter
>> +
>> +maintainers:
>> + - Kurt Borja <kuurtb@gmail.com>
>> +
>> +description: |
>> + The ADS1262 and ADS1263 are 38.4-kSPS, delta-sigma (ΔΣ) ADCs with an
>> + integrated PGA, reference, and internal fault monitors. The ADS1263 integrates
>> + an auxiliary, 24-bit, ΔΣ ADC intended for background measurements.
>> +
>> + Datasheets:
>> + - ADS126x: https://www.ti.com/lit/ds/symlink/ads1262.pdf
>> +
>> +properties:
>> + compatible:
>> + oneOf:
>> + - const: ti,ads1262
>> + - items:
>> + - const: ti,ads1263
>> + - const: ti,ads1262
>> +
>> + reg:
>> + maxItems: 1
>> +
>> + '#address-cells':
>> + const: 1
>> +
>> + '#size-cells':
>> + const: 0
>> +
>> + spi-max-frequency:
>> + maximum: 8000000
>> +
>> + spi-cpha: true
>> +
>> + interrupts:
>> + description: Data ready (DRDY) interrupt line.
>> + maxItems: 1
>
> Technically, there are two pins with the DRDY signal, so we should have
> two interrupts in order to be able to tell which one is wired up.
Oh you're right. I'll describe both here. I may not add support for it
though, at least until I complete everything else.
>
>> +
>> + start-gpios:
>> + description: Start conversion control.
>> + maxItems: 1
>> +
>> + reset-gpios:
>> + maxItems: 1
>> +
>> + dvdd-supply:
>> + description: Digital power supply.
>> +
>> + avdd-supply:
>> + description: Analog power supply.
>> +
>> + refp-supply:
>> + description: External positive voltage reference.
>> +
>> + refn-supply:
>> + description: External negative voltage reference.
>> +
>
> Which pins are these? I see 4 possible external reference sources,
> but all go through the AINx pins. So I would expect:
>
> refp1-supply, refn1-supply, refp2-supply, refn2-supply,
> refp3-supply, refn3-supply, refp4-supply, refn4-supply
I tried to go for a simpler route, but I agree with this.
>
> Also, similar to the chip I am working on, I expect that these pins
> could be connected to a resistor rather than a voltage source, so
> could use additional bindings for that.
Sure!
>
>
>> + ti,vbias:
>> + $ref: /schemas/types.yaml#/definitions/flag
>> + description: Enables the level-shift voltage on the AINCOM pin.
>
> VBIAS is a voltage source, so I would expect that to be modeled
> as a regulator provider. (If we do that REFOUT should be included
> as well.)
I didn't think about that, I agree.
>
>> +
>> + clocks:
>> + maxItems: 1
>> +
>> + '#io-channel-cells':
>> + minimum: 1
>> + maximum: 2
>> +
>> + '#gpio-cells':
>> + const: 2
>> +
>> + gpio-controller: true
>> +
>> +patternProperties:
>> + "^channel@[0-9]+$":
>> + $ref: /schemas/iio/adc/adc.yaml#
>> + unevaluatedProperties: false
>> +
>> + properties:
>> + reg:
>> + maxItems: 1
>> +
>
> If we want to allow single-ended/pseudo-differential inputs, then we should
> also allow single-channel (positive pin) and common-mode-channel (negative
> pin) properties.
>
> This will also require additional common-mode-<N>-supply properties to allow
> for the negative pin connected to something other than GND.
Ah interesting. Why the N though? wouldn't a single supply connected to
AINCOM be enough here?
>
>> + diff-channels:
>> + description: |
>> + Selects the analog input configuration for this channel. The first
>> + value is the positive input and the second is the negative input.
>> + The following values are available:
>> + 0: AIN0 pin
>> + 1: AIN1 pin
>> + 2: AIN2 pin
>> + 3: AIN3 pin
>> + 4: AIN4 pin
>> + 5: AIN5 pin
>> + 6: AIN6 pin
>> + 7: AIN7 pin
>> + 8: AIN8 pin
>> + 9: AIN9 pin
>> + 10: AINCOM pin
>
>> + 11: Temperature sensor monitor
>> + 12: Analog power supply monitor
>> + 13: Digital power supply monitor
>> + 14: TDAC test signal
>
> These are all internal signals, so not sure it makes sense to have
> them in the devicetree. It would make more sense to have fixed
> channels defined in the driver for these since they are always there.
Similar to the approach you took.
>
> We probably also need a separate property (a bool/flag?) to say that
> this channel is a TDAC output rather than an analog input. Although
> that is for testing, so maybe something to omit for now until we
> actually have an application that uses it (to make sure we get it
> right)?
Yes, I will add the monitor channel for this too. The users can adjust
voltage from debugfs. IMO that should be enough.
>
>
>> + 15: Float (open connection)
>
> How could we have a differential input with one or both pins open?
> Likely this will just be the setting for pins not specified as something
> else in the devicetree.
I should remove this too. Leaving the pins floating is necessary when
calibrating. I will add full automatic calibration on probe right after
this series.
[...]
>> + ti,idac-chopping:
>
> I would call this ti,excitation-channel-chopping to match the excitation-channel
> property. Or since this isn't a generic property, call it ti,idac-rotation to
> match the datasheet.
Both are fine by me. I went with chopping based on what you said about
the term in your series.
>
>> + $ref: /schemas/types.yaml#/definitions/flag
>> + description:
>> + Automatically swap the IDAC1 and IDAC2 connections of alternate
>> + conversions. The ADC averages the alternate conversions to eliminate
>> + IDAC mismatch.
>> +
>> + ti,pga-bypass:
>> + $ref: /schemas/types.yaml#/definitions/flag
>> + description: Bypass the Programmable Gain Amplifier (PGA).
>
> Why would this need to be a DT property? I didn't read this datasheet
> too much, but in other chips I have seen there are usually rules that
> PGA has to be bypassed under certain conditions, but not others, so
> this seems like something for the driver to handle rather than the
> devicetree.
To be honest, I don't know what would be the application for this. AFAIK
when the PGA is bypassed the analog inputs are read unbuffered (?) In
that case shouldn't this belong DT?
[...]
--
Thanks,
~ Kurt
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v2 2/7] iio: adc: Add ti-ads1262 driver
2026-06-28 5:36 [PATCH v2 0/7] iio: adc: Add TI ADS126X ADC family support Kurt Borja
2026-06-28 5:36 ` [PATCH v2 1/7] dt-bindings: iio: adc: Add TI ADS126x ADC family Kurt Borja
@ 2026-06-28 5:36 ` Kurt Borja
2026-06-28 17:15 ` David Lechner
2026-06-28 5:36 ` [PATCH v2 3/7] iio: adc: ti-ads1262: Add channel filter support Kurt Borja
` (4 subsequent siblings)
6 siblings, 1 reply; 14+ messages in thread
From: Kurt Borja @ 2026-06-28 5:36 UTC (permalink / raw)
To: Kurt Borja, Jonathan Cameron, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, David Lechner
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel, Jonathan Cameron
Add the ti-ads1262 driver with initial support for the primary ADC
(ADC1). The ADS1263 auxiliary ADC (ADC2) is handled by a separate driver
and interoperability considerations were taken into account.
Signed-off-by: Kurt Borja <kuurtb@gmail.com>
---
MAINTAINERS | 1 +
drivers/iio/adc/Kconfig | 13 +
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ti-ads1262.c | 1206 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 1221 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 9b83d294734b574d..d868b25f2c65bcd9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -26928,6 +26928,7 @@ M: Kurt Borja <kuurtb@gmail.com>
L: linux-iio@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
+F: drivers/iio/adc/ti-ads1262.c
TI ADS7924 ADC DRIVER
M: Hugo Villeneuve <hvilleneuve@dimonoff.com>
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 5845db2011fb5be1..6051092c20b96731 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -1811,6 +1811,19 @@ config TI_ADS124S08
This driver can also be built as a module. If so, the module will be
called ti-ads124s08.
+config TI_ADS1262
+ tristate "Texas Instruments ADS1262"
+ depends on SPI
+ select REGMAP
+ select IIO_BUFFER
+ select IIO_TRIGGERED_BUFFER
+ help
+ If you say yes here you get support for Texas Instruments ADS1262 and
+ ADS1263 ADC chips.
+
+ This driver can also be built as a module. If so, the module will be
+ called ti-ads1262.
+
config TI_ADS1298
tristate "Texas Instruments ADS1298"
depends on SPI
diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile
index 72f4c6b33ca87b3f..4b1f89a2317a35f7 100644
--- a/drivers/iio/adc/Makefile
+++ b/drivers/iio/adc/Makefile
@@ -155,6 +155,7 @@ obj-$(CONFIG_TI_ADS1100) += ti-ads1100.o
obj-$(CONFIG_TI_ADS1119) += ti-ads1119.o
obj-$(CONFIG_TI_ADS112C14) += ti-ads112c14.o
obj-$(CONFIG_TI_ADS124S08) += ti-ads124s08.o
+obj-$(CONFIG_TI_ADS1262) += ti-ads1262.o
obj-$(CONFIG_TI_ADS1298) += ti-ads1298.o
obj-$(CONFIG_TI_ADS131E08) += ti-ads131e08.o
obj-$(CONFIG_TI_ADS131M02) += ti-ads131m02.o
diff --git a/drivers/iio/adc/ti-ads1262.c b/drivers/iio/adc/ti-ads1262.c
new file mode 100644
index 0000000000000000..6103cf5a2d1624a9
--- /dev/null
+++ b/drivers/iio/adc/ti-ads1262.c
@@ -0,0 +1,1206 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Texas Instruments ADS1262 ADC driver
+ *
+ * Copyright (C) 2025 Kurt Borja <kuurtb@gmail.com>
+ */
+
+#include <linux/array_size.h>
+#include <linux/align.h>
+#include <linux/bitfield.h>
+#include <linux/bitmap.h>
+#include <linux/bitops.h>
+#include <linux/cleanup.h>
+#include <linux/clk.h>
+#include <linux/completion.h>
+#include <linux/compiler_attributes.h>
+#include <linux/compiler_types.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/interrupt.h>
+#include <linux/lockdep.h>
+#include <linux/math.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/mod_devicetable.h>
+#include <linux/mutex.h>
+#include <linux/overflow.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/spi/spi.h>
+#include <linux/string.h>
+#include <linux/time.h>
+#include <linux/types.h>
+#include <linux/units.h>
+
+#include <asm/byteorder.h>
+
+#include <linux/iio/iio.h>
+
+#define ADS1262_OPCODE_NOP 0x00
+#define ADS1262_OPCODE_RESET 0x06
+#define ADS1262_OPCODE_START1 0x08
+#define ADS1262_OPCODE_STOP1 0x0A
+#define ADS1262_OPCODE_START2 0x0C
+#define ADS1262_OPCODE_STOP2 0x0E
+#define ADS1262_OPCODE_RDATA1 0x12
+#define ADS1262_OPCODE_RDATA2 0x14
+#define ADS1262_OPCODE_SYOCAL1 0x16
+#define ADS1262_OPCODE_SYGCAL1 0x17
+#define ADS1262_OPCODE_SFOCAL1 0x19
+#define ADS1262_OPCODE_SYOCAL2 0x1B
+#define ADS1262_OPCODE_SYGCAL2 0x1C
+#define ADS1262_OPCODE_SFOCAL2 0x1E
+#define ADS1262_OPCODE_RREG 0x20
+#define ADS1262_OPCODE_WREG 0x40
+
+#define ADS1262_ID_REG 0x00
+#define ADS1262_DEV_ID_MASK GENMASK(7, 5)
+#define ADS1262_REV_ID_MASK GENMASK(4, 0)
+
+#define ADS1262_POWER_REG 0x01
+#define ADS1262_POWER_RESET_MASK BIT(4)
+#define ADS1262_POWER_VBIAS_MASK BIT(1)
+#define ADS1262_POWER_INTREF_MASK BIT(0)
+
+#define ADS1262_INTERFACE_REG 0x02
+#define ADS1262_INTERFACE_TIMEOUT_MASK BIT(3)
+#define ADS1262_INTERFACE_STATUS_MASK BIT(2)
+#define ADS1262_INTERFACE_CRC_MASK GENMASK(1, 0)
+
+#define ADS1262_MODE0_REG 0x03
+#define ADS1262_MODE0_REFREV_MASK BIT(7)
+#define ADS1262_MODE0_RUNMODE_MASK BIT(6)
+#define ADS1262_MODE0_IDAC_CHOP_MASK BIT(5)
+#define ADS1262_MODE0_INPUT_CHOP_MASK BIT(4)
+#define ADS1262_MODE0_DELAY_MASK GENMASK(3, 0)
+
+#define ADS1262_MODE1_REG 0x04
+#define ADS1262_MODE1_FILTER_MASK GENMASK(7, 5)
+#define ADS1262_MODE1_SBADC_MASK BIT(4)
+#define ADS1262_MODE1_SBPOL_MASK BIT(3)
+#define ADS1262_MODE1_SBMAG_MASK GENMASK(2, 0)
+
+#define ADS1262_MODE2_REG 0x05
+#define ADS1262_MODE2_BYPASS_MASK BIT(7)
+#define ADS1262_MODE2_GAIN_MASK GENMASK(6, 4)
+#define ADS1262_MODE2_DR_MASK GENMASK(3, 0)
+
+#define ADS1262_INPMUX_REG 0x06
+#define ADS1262_INPMUX_MUXP_MASK GENMASK(7, 4)
+#define ADS1262_INPMUX_MUXN_MASK GENMASK(3, 0)
+
+#define ADS1262_OFCAL0_REG 0x07
+#define ADS1262_OFCAL1_REG 0x08
+#define ADS1262_OFCAL2_REG 0x09
+#define ADS1262_FSCAL0_REG 0x0A
+#define ADS1262_FSCAL1_REG 0x0B
+#define ADS1262_FSCAL2_REG 0x0C
+
+#define ADS1262_IDACMUX_REG 0x0D
+#define ADS1262_IDACMUX_MUX2_MASK GENMASK(7, 4)
+#define ADS1262_IDACMUX_MUX1_MASK GENMASK(3, 0)
+
+#define ADS1262_IDACMAG_REG 0x0E
+#define ADS1262_IDACMAG_MAG2_MASK GENMASK(7, 4)
+#define ADS1262_IDACMAG_MAG1_MASK GENMASK(3, 0)
+
+#define ADS1262_REFMUX_REG 0x0F
+#define ADS1262_REFMUX_RMUXP_MASK GENMASK(5, 3)
+#define ADS1262_REFMUX_RMUXN_MASK GENMASK(2, 0)
+
+#define ADS1262_TDACP_REG 0x10
+#define ADS1262_TDACP_OUTP_MASK BIT(7)
+#define ADS1262_TDACP_MAGP_MASK GENMASK(4, 0)
+
+#define ADS1262_TDACN_REG 0x11
+#define ADS1262_TDACN_OUTN_MASK BIT(7)
+#define ADS1262_TDACN_MAGN_MASK GENMASK(4, 0)
+
+#define ADS1262_GPIOCON_REG 0x12
+#define ADS1262_GPIODIR_REG 0x13
+#define ADS1262_GPIODAT_REG 0x14
+
+#define ADS1262_ADC2CFG_REG 0x15
+#define ADS1262_ADC2CFG_DR2_MASK GENMASK(7, 6)
+#define ADS1262_ADC2CFG_REF2_MASK GENMASK(5, 3)
+#define ADS1262_ADC2CFG_GAIN2_MASK GENMASK(2, 0)
+
+#define ADS1262_ADC2MUX_REG 0x16
+#define ADS1262_ADC2MUX_MUXP2_MASK GENMASK(7, 4)
+#define ADS1262_ADC2MUX_MUXN2_MASK GENMASK(3, 0)
+
+#define ADS1262_ADC2OFC0_REG 0x17
+#define ADS1262_ADC2OFC1_REG 0x18
+#define ADS1262_ADC2FSC0_REG 0x19
+#define ADS1262_ADC2FSC1_REG 0x1A
+#define ADS1262_REG_COUNT 0x1B
+
+#define ADS1262_MAX_CHANNEL_COUNT 16
+#define ADS1262_XFER_BUFFER_SZ 11
+
+enum {
+ ADS1262_RUNMODE_CONTINUOUS,
+ ADS1262_RUNMODE_PULSE,
+};
+
+enum {
+ ADS1262_DR_2_5_SPS,
+ ADS1262_DR_5_SPS,
+ ADS1262_DR_10_SPS,
+ ADS1262_DR_16_6_SPS,
+ ADS1262_DR_20_SPS,
+ ADS1262_DR_50_SPS,
+ ADS1262_DR_60_SPS,
+ ADS1262_DR_100_SPS,
+ ADS1262_DR_400_SPS,
+ ADS1262_DR_1200_SPS,
+ ADS1262_DR_2400_SPS,
+ ADS1262_DR_4800_SPS,
+ ADS1262_DR_7200_SPS,
+ ADS1262_DR_14400_SPS,
+ ADS1262_DR_19200_SPS,
+ ADS1262_DR_38400_SPS,
+};
+
+enum {
+ ADS1262_INPMUX_AIN0,
+ ADS1262_INPMUX_AIN1,
+ ADS1262_INPMUX_AIN2,
+ ADS1262_INPMUX_AIN3,
+ ADS1262_INPMUX_AIN4,
+ ADS1262_INPMUX_AIN5,
+ ADS1262_INPMUX_AIN6,
+ ADS1262_INPMUX_AIN7,
+ ADS1262_INPMUX_AIN8,
+ ADS1262_INPMUX_AIN9,
+ ADS1262_INPMUX_AINCOM,
+ ADS1262_INPMUX_TEMP,
+ ADS1262_INPMUX_AVDD,
+ ADS1262_INPMUX_DVDD,
+ ADS1262_INPMUX_TDAC,
+ ADS1262_INPMUX_FLOAT,
+ ADS1262_INPMUX_LAST
+};
+
+enum {
+ ADS1262_REFMUX_INTERNAL,
+ ADS1262_REFMUX_AIN0_AIN1,
+ ADS1262_REFMUX_AIN2_AIN3,
+ ADS1262_REFMUX_AIN4_AIN5,
+ ADS1262_REFMUX_AVDD_AVSS,
+ ADS1262_REFMUX_LAST
+};
+
+struct ads1262_chip_info {
+ const char *name;
+ bool has_aux_adc;
+};
+
+struct ads1262_channel {
+ u8 input[2];
+ u8 gain;
+ u8 data_rate;
+ u8 reference[3];
+ u8 pga_bypass:1;
+ u8 ref_reversal:1;
+ u8 input_chop:1;
+ u8 idac_chop:1;
+};
+
+struct ads1262 {
+ struct spi_device *spi;
+ const struct ads1262_chip_info *info;
+ struct regmap *regmap;
+ struct iio_dev *indio_dev;
+ struct gpio_desc *reset_gpiod;
+ struct gpio_desc *start_gpiod;
+
+ /* Protects channel state */
+ struct mutex chan_lock;
+ unsigned int num_channels;
+ struct ads1262_channel *channels;
+ struct completion drdy;
+
+ int refp_uV;
+ int refn_uV;
+ int avdd_uV;
+
+ /* Protects transfer buffers and concurrent SPI transfers */
+ struct mutex xfer_lock;
+
+ u8 tx[ADS1262_XFER_BUFFER_SZ] __aligned(IIO_DMA_MINALIGN);
+ u8 rx[ADS1262_XFER_BUFFER_SZ] __aligned(IIO_DMA_MINALIGN);
+};
+
+static const int ads1262_data_rate_avail[][2] = {
+ [ADS1262_DR_2_5_SPS] = { 2, 500000 },
+ [ADS1262_DR_5_SPS] = { 5, 0 },
+ [ADS1262_DR_10_SPS] = { 10, 0 },
+ [ADS1262_DR_16_6_SPS] = { 16, 666667 },
+ [ADS1262_DR_20_SPS] = { 20, 0 },
+ [ADS1262_DR_50_SPS] = { 50, 0 },
+ [ADS1262_DR_60_SPS] = { 60, 0 },
+ [ADS1262_DR_100_SPS] = { 100, 0 },
+ [ADS1262_DR_400_SPS] = { 400, 0 },
+ [ADS1262_DR_1200_SPS] = { 1200, 0 },
+ [ADS1262_DR_2400_SPS] = { 2400, 0 },
+ [ADS1262_DR_4800_SPS] = { 4800, 0 },
+ [ADS1262_DR_7200_SPS] = { 7200, 0 },
+ [ADS1262_DR_14400_SPS] = { 14400, 0 },
+ [ADS1262_DR_19200_SPS] = { 19200, 0 },
+ [ADS1262_DR_38400_SPS] = { 38400, 0 },
+};
+
+static const int ads1262_pga_gain_avail[] = {
+ 1, 2, 4, 8, 16, 32
+};
+
+#define ads1262_find_one(_arr, _val) \
+({ \
+ int _i; \
+ for (_i = 0; _i < ARRAY_SIZE(_arr); _i++) { \
+ if ((_val) == _arr[_i]) \
+ break; \
+ } \
+ if (_i == ARRAY_SIZE(_arr)) \
+ _i = -EINVAL; \
+ _i; \
+})
+
+#define ads1262_find_two(_arr, _val, _val2) \
+({ \
+ int _i; \
+ for (_i = 0; _i < ARRAY_SIZE(_arr); _i++) { \
+ if ((_val) == _arr[_i][0] && (_val2) == _arr[_i][1]) \
+ break; \
+ } \
+ if (_i == ARRAY_SIZE(_arr)) \
+ _i = -EINVAL; \
+ _i; \
+})
+
+#define ads1262_find_string(_arr, _str) \
+({ \
+ int _i; \
+ for (_i = 0; _i < ARRAY_SIZE(_arr); _i++) { \
+ if (!strcmp(_arr[_i], _str)) \
+ break; \
+ } \
+ if (_i == ARRAY_SIZE(_arr)) \
+ _i = -EINVAL; \
+ _i; \
+})
+
+static int ads1262_dev_cmd(struct ads1262 *st, u8 opcode)
+{
+ guard(mutex)(&st->xfer_lock);
+
+ return spi_write_then_read(st->spi, &opcode, sizeof(opcode), NULL, 0);
+}
+
+static int ads1262_dev_reset(struct ads1262 *st)
+{
+ int ret;
+
+ ret = ads1262_dev_cmd(st, ADS1262_OPCODE_RESET);
+ if (ret)
+ return ret;
+
+ /* RESET command timing requirement is 1.085 usecs (Table 9-32) */
+ fsleep(2);
+
+ return ret;
+}
+
+static int ads1262_dev_start(struct ads1262 *st)
+{
+ int ret;
+
+ if (st->start_gpiod)
+ ret = gpiod_set_value_cansleep(st->start_gpiod, 1);
+ else
+ ret = ads1262_dev_cmd(st, ADS1262_OPCODE_START1);
+
+ return ret;
+}
+
+static int ads1262_dev_stop(struct ads1262 *st)
+{
+ int ret;
+
+ if (st->start_gpiod)
+ ret = gpiod_set_value_cansleep(st->start_gpiod, 0);
+ else
+ ret = ads1262_dev_cmd(st, ADS1262_OPCODE_STOP1);
+
+ return ret;
+}
+
+static int ads1262_dev_start_one(struct ads1262 *st, u8 runmode)
+{
+ int ret;
+
+ ret = ads1262_dev_start(st);
+ if (ret)
+ return ret;
+
+ if (runmode == ADS1262_RUNMODE_CONTINUOUS)
+ return ads1262_dev_stop(st);
+
+ return 0;
+}
+
+static int ads1262_calculate_scale(struct ads1262 *st, u8 realbits, u8 gain,
+ u8 pos_ref, u8 neg_ref, int *val, int *val2)
+{
+ u64 divd, divr, tmp, rem;
+ int pos_uV, neg_uV;
+
+ switch (pos_ref) {
+ case ADS1262_REFMUX_INTERNAL:
+ /* Internal voltage reference is 2.5 V */
+ pos_uV = 2500000;
+ break;
+ case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
+ pos_uV = st->refp_uV;
+ break;
+ case ADS1262_REFMUX_AVDD_AVSS:
+ pos_uV = st->avdd_uV;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (neg_ref) {
+ case ADS1262_REFMUX_INTERNAL:
+ neg_uV = 0;
+ break;
+ case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
+ neg_uV = st->refn_uV == -ENODEV ? 0 : st->refn_uV;
+ break;
+ case ADS1262_REFMUX_AVDD_AVSS:
+ neg_uV = 0;
+ break;
+
+ default:
+ return -EINVAL;
+ }
+
+ if (pos_uV < neg_uV)
+ divd = neg_uV - pos_uV;
+ else
+ divd = pos_uV - neg_uV;
+ divr = BIT_ULL(gain + realbits - 1) * 1000;
+ tmp = div64_u64(divd * NANO, divr);
+
+ *val = div64_u64_rem(tmp, NANO, &rem);
+ *val2 = rem;
+
+ return 0;
+}
+
+static int ads1262_channel_get_scale(struct ads1262 *st,
+ struct iio_chan_spec const *chan, int *val,
+ int *val2)
+{
+ struct ads1262_channel *chan_data = &st->channels[chan->scan_index];
+
+ return ads1262_calculate_scale(st, chan->scan_type.realbits, chan_data->gain,
+ chan_data->reference[0], chan_data->reference[1],
+ val, val2);
+}
+
+static long ads1262_wait_for_conversion(struct ads1262 *st)
+{
+ /*
+ * The first conversion latency is affected by the channel's data rate,
+ * filter, the configurable conversion delay and whether chop mode
+ * and/or IDAC rotation mode are enabled.
+ *
+ * The worst possible latency is calculated by taking the lowest data
+ * rate (2.5 SPS) and the sinc4 filter. This gives a latency of 1600 ms
+ * (Table 9-13). Then add the slowest configurable conversion delay
+ * (9 ms) and multiply by 4 to account for chop and IDAC rotation modes
+ * (Equation 20).
+ *
+ * Final result is 4 * (1600 ms + 9 ms) = 6436.
+ */
+ return wait_for_completion_interruptible_timeout(&st->drdy, msecs_to_jiffies(6436));
+}
+
+static int ads1262_dev_read_by_cmd(struct ads1262 *st, u8 cmd, __be32 *val)
+{
+ guard(mutex)(&st->xfer_lock);
+
+ return spi_write_then_read(st->spi, &cmd, sizeof(cmd), val, sizeof(*val));
+}
+
+static int ads1262_channel_enable(struct ads1262 *st,
+ struct ads1262_channel *chan)
+{
+ u8 mode0, mode2, inpmux, refmux;
+ int ret;
+
+ /* Avoid using guard() here to mitigate AB/BA deadlock warning */
+ mutex_lock(&st->chan_lock);
+ mode0 = FIELD_PREP(ADS1262_MODE0_INPUT_CHOP_MASK, chan->input_chop) |
+ FIELD_PREP(ADS1262_MODE0_IDAC_CHOP_MASK, chan->idac_chop) |
+ FIELD_PREP(ADS1262_MODE0_REFREV_MASK, chan->ref_reversal);
+ mode2 = FIELD_PREP(ADS1262_MODE2_DR_MASK, chan->data_rate) |
+ FIELD_PREP(ADS1262_MODE2_GAIN_MASK, chan->gain) |
+ FIELD_PREP(ADS1262_MODE2_BYPASS_MASK, chan->pga_bypass);
+ inpmux = FIELD_PREP(ADS1262_INPMUX_MUXN_MASK, chan->input[1]) |
+ FIELD_PREP(ADS1262_INPMUX_MUXP_MASK, chan->input[0]);
+ refmux = FIELD_PREP(ADS1262_REFMUX_RMUXN_MASK, chan->reference[1]) |
+ FIELD_PREP(ADS1262_REFMUX_RMUXP_MASK, chan->reference[0]);
+ mutex_unlock(&st->chan_lock);
+
+ ret = regmap_update_bits(st->regmap, ADS1262_MODE0_REG,
+ ADS1262_MODE0_INPUT_CHOP_MASK |
+ ADS1262_MODE0_IDAC_CHOP_MASK |
+ ADS1262_MODE0_REFREV_MASK, mode0);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(st->regmap, ADS1262_MODE2_REG,
+ ADS1262_MODE2_DR_MASK |
+ ADS1262_MODE2_GAIN_MASK |
+ ADS1262_MODE2_BYPASS_MASK, mode2);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(st->regmap, ADS1262_INPMUX_REG,
+ ADS1262_INPMUX_MUXN_MASK |
+ ADS1262_INPMUX_MUXP_MASK, inpmux);
+ if (ret)
+ return ret;
+
+ return regmap_update_bits(st->regmap, ADS1262_REFMUX_REG,
+ ADS1262_REFMUX_RMUXN_MASK |
+ ADS1262_REFMUX_RMUXP_MASK, refmux);
+}
+
+static int ads1262_set_runmode(struct ads1262 *st, u8 runmode)
+{
+ return regmap_update_bits(st->regmap, ADS1262_MODE0_REG,
+ ADS1262_MODE0_RUNMODE_MASK,
+ FIELD_PREP(ADS1262_MODE0_RUNMODE_MASK, runmode));
+}
+
+static int ads1262_channel_read(struct ads1262 *st,
+ struct ads1262_channel *chan_data, __be32 *val)
+{
+ u8 runmode;
+ int ret;
+
+ IIO_DEV_ACQUIRE_DIRECT_MODE(st->indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ /*
+ * When a channel has chop mode or IDAC rotation mode, the first
+ * conversion is always withheld so the datasheet suggests using the
+ * CONTINUOUS mode and briefly starting and stopping conversions to
+ * achieve the same effect (Section 9.4.1.2).
+ */
+ if (chan_data->input_chop || chan_data->idac_chop)
+ runmode = ADS1262_RUNMODE_CONTINUOUS;
+ else
+ runmode = ADS1262_RUNMODE_PULSE;
+
+ ret = ads1262_set_runmode(st, runmode);
+ if (ret)
+ return ret;
+
+ ret = ads1262_channel_enable(st, chan_data);
+ if (ret)
+ return ret;
+
+ reinit_completion(&st->drdy);
+
+ ret = ads1262_dev_start_one(st, runmode);
+ if (ret)
+ return ret;
+
+ ads1262_wait_for_conversion(st);
+
+ return ads1262_dev_read_by_cmd(st, ADS1262_OPCODE_RDATA1, val);
+}
+
+static int ads1262_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int *val,
+ int *val2, long mask)
+{
+ struct ads1262 *st = iio_priv(indio_dev);
+ struct ads1262_channel *chan_data = &st->channels[chan->scan_index];
+ u8 realbits = chan->scan_type.realbits;
+ __be32 raw;
+ int ret;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_RAW:
+ ret = ads1262_channel_read(st, chan_data, &raw);
+ if (ret)
+ return ret;
+ *val = sign_extend32(be32_to_cpu(raw), realbits - 1);
+
+ return IIO_VAL_INT;
+
+ case IIO_CHAN_INFO_SCALE: {
+ guard(mutex)(&st->chan_lock);
+
+ ret = ads1262_channel_get_scale(st, chan, val, val2);
+ if (ret)
+ return ret;
+
+ return IIO_VAL_INT_PLUS_NANO;
+ }
+
+ case IIO_CHAN_INFO_HARDWAREGAIN: {
+ guard(mutex)(&st->chan_lock);
+
+ *val = ads1262_pga_gain_avail[chan_data->gain];
+
+ return IIO_VAL_INT;
+ }
+
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ guard(mutex)(&st->chan_lock);
+
+ *val = ads1262_data_rate_avail[chan_data->data_rate][0];
+ *val2 = ads1262_data_rate_avail[chan_data->data_rate][1];
+
+ return IIO_VAL_INT_PLUS_MICRO;
+ }
+
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ads1262_read_avail(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, const int **vals,
+ int *type, int *length, long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ *type = IIO_VAL_INT_PLUS_MICRO;
+ *vals = (const int *)ads1262_data_rate_avail;
+ *length = ARRAY_SIZE(ads1262_data_rate_avail) * 2;
+ return IIO_AVAIL_LIST;
+
+ case IIO_CHAN_INFO_HARDWAREGAIN:
+ *type = IIO_VAL_INT;
+ *vals = ads1262_pga_gain_avail;
+ *length = ARRAY_SIZE(ads1262_pga_gain_avail);
+ return IIO_AVAIL_LIST;
+
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ads1262_write_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int val,
+ int val2, long mask)
+{
+ struct ads1262 *st = iio_priv(indio_dev);
+ struct ads1262_channel *chan_data;
+ int i;
+
+ chan_data = &st->channels[chan->scan_index];
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ i = ads1262_find_two(ads1262_data_rate_avail, val, val2);
+ if (i < 0)
+ return i;
+
+ guard(mutex)(&st->chan_lock);
+ chan_data->data_rate = i;
+
+ break;
+ }
+
+ case IIO_CHAN_INFO_HARDWAREGAIN: {
+ i = ads1262_find_one(ads1262_pga_gain_avail, val);
+ if (i < 0)
+ return i;
+
+ guard(mutex)(&st->chan_lock);
+ chan_data->gain = i;
+
+ break;
+ }
+
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ return 0;
+}
+
+static int ads1262_write_raw_get_fmt(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_CONVDELAY:
+ return IIO_VAL_INT_PLUS_NANO;
+ default:
+ return IIO_VAL_INT_PLUS_MICRO;
+ }
+}
+
+static int ads1262_debugfs_reg_access(struct iio_dev *indio_dev, unsigned int reg,
+ unsigned int writeval, unsigned int *readval)
+{
+ struct ads1262 *st = iio_priv(indio_dev);
+
+ if (readval)
+ return regmap_read_bypassed(st->regmap, reg, readval);
+
+ return regmap_write(st->regmap, reg, writeval);
+}
+
+static const struct iio_info ads1262_iio_info = {
+ .read_raw = ads1262_read_raw,
+ .read_avail = ads1262_read_avail,
+ .write_raw = ads1262_write_raw,
+ .write_raw_get_fmt = ads1262_write_raw_get_fmt,
+ .debugfs_reg_access = ads1262_debugfs_reg_access,
+};
+
+static irqreturn_t ads1262_irq_handler(int irq, void *dev_id)
+{
+ struct ads1262 *st = dev_id;
+
+ complete(&st->drdy);
+
+ return IRQ_HANDLED;
+}
+
+static int ads1262_alloc_channels(struct ads1262 *st,
+ struct iio_chan_spec **channels)
+{
+ struct device *dev = &st->spi->dev;
+ struct ads1262_channel *chan_data;
+ struct iio_chan_spec *chans;
+ unsigned int i, num_channels;
+
+ /* Account for the timestamp channel */
+ num_channels = st->num_channels + 1;
+ chans = devm_kcalloc(dev, num_channels, sizeof(*chans), GFP_KERNEL);
+ if (!chans)
+ return -ENOMEM;
+
+ for (i = 0; i < st->num_channels; i++) {
+ chan_data = &st->channels[i];
+ chans[i] = (struct iio_chan_spec) {
+ .type = IIO_VOLTAGE,
+ .channel = chan_data->input[0],
+ .channel2 = chan_data->input[1],
+ .scan_index = i,
+ .scan_type = {
+ .format = IIO_SCAN_FORMAT_SIGNED_INT,
+ .realbits = 32,
+ .storagebits = 32,
+ .endianness = IIO_BE,
+ },
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE) |
+ BIT(IIO_CHAN_INFO_HARDWAREGAIN) |
+ BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ .info_mask_shared_by_type_available =
+ BIT(IIO_CHAN_INFO_HARDWAREGAIN) |
+ BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ .indexed = true,
+ .differential = true,
+ };
+ }
+
+ chans[i] = IIO_CHAN_SOFT_TIMESTAMP(i);
+
+ *channels = chans;
+
+ return num_channels;
+}
+
+static int ads1262_dev_configure(struct ads1262 *st)
+{
+ struct device *dev = &st->spi->dev;
+ int ret;
+
+ ret = ads1262_dev_reset(st);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to reset device\n");
+
+ ret = regmap_clear_bits(st->regmap, ADS1262_POWER_REG,
+ ADS1262_POWER_RESET_MASK);
+ if (ret)
+ return ret;
+
+ ret = regmap_clear_bits(st->regmap, ADS1262_INTERFACE_REG,
+ ADS1262_INTERFACE_STATUS_MASK |
+ ADS1262_INTERFACE_CRC_MASK);
+ if (ret)
+ return ret;
+
+ if (device_property_present(dev, "ti,vbias")) {
+ ret = regmap_set_bits(st->regmap, ADS1262_POWER_REG,
+ ADS1262_POWER_VBIAS_MASK);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static const struct regmap_range ads1262_read_write_range[] = {
+ regmap_reg_range(ADS1262_ID_REG, ADS1262_ADC2FSC1_REG),
+};
+
+static const struct regmap_range ads1262_read_only_range[] = {
+ regmap_reg_range(ADS1262_ID_REG, ADS1262_ID_REG),
+};
+
+static const struct regmap_access_table ads1262_wr_table = {
+ .yes_ranges = ads1262_read_write_range,
+ .n_yes_ranges = ARRAY_SIZE(ads1262_read_write_range),
+ .no_ranges = ads1262_read_only_range,
+ .n_no_ranges = ARRAY_SIZE(ads1262_read_only_range),
+};
+
+static const struct regmap_access_table ads1262_rd_table = {
+ .yes_ranges = ads1262_read_write_range,
+ .n_yes_ranges = ARRAY_SIZE(ads1262_read_write_range),
+};
+
+static const struct reg_default ads1262_reg_defaults[] = {
+ { ADS1262_POWER_REG, 0x11 },
+ { ADS1262_INTERFACE_REG, 0x05 },
+ { ADS1262_MODE0_REG, 0x00 },
+ { ADS1262_MODE1_REG, 0x80 },
+ { ADS1262_MODE2_REG, 0x04 },
+ { ADS1262_INPMUX_REG, 0x01 },
+ { ADS1262_OFCAL0_REG, 0x00 },
+ { ADS1262_OFCAL1_REG, 0x00 },
+ { ADS1262_OFCAL2_REG, 0x00 },
+ { ADS1262_FSCAL0_REG, 0x00 },
+ { ADS1262_FSCAL1_REG, 0x00 },
+ { ADS1262_FSCAL2_REG, 0x40 },
+ { ADS1262_IDACMUX_REG, 0xBB },
+ { ADS1262_IDACMAG_REG, 0x00 },
+ { ADS1262_REFMUX_REG, 0x00 },
+ { ADS1262_TDACP_REG, 0x00 },
+ { ADS1262_TDACN_REG, 0x00 },
+ { ADS1262_GPIOCON_REG, 0x00 },
+ { ADS1262_GPIODIR_REG, 0x00 },
+ { ADS1262_GPIODAT_REG, 0x00 },
+ { ADS1262_ADC2CFG_REG, 0x00 },
+ { ADS1262_ADC2MUX_REG, 0x01 },
+ { ADS1262_ADC2OFC0_REG, 0x00 },
+ { ADS1262_ADC2OFC1_REG, 0x00 },
+ { ADS1262_ADC2FSC0_REG, 0x00 },
+ { ADS1262_ADC2FSC1_REG, 0x40 },
+};
+
+static const struct regmap_config ads1262_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .wr_table = &ads1262_wr_table,
+ .rd_table = &ads1262_rd_table,
+ .reg_defaults = ads1262_reg_defaults,
+ .num_reg_defaults = ARRAY_SIZE(ads1262_reg_defaults),
+ .max_register = ADS1262_ADC2FSC1_REG,
+ .read_flag_mask = ADS1262_OPCODE_RREG,
+ .write_flag_mask = ADS1262_OPCODE_WREG,
+ .can_sleep = true,
+ .cache_type = REGCACHE_MAPLE,
+};
+
+static int ads1262_regmap_read(void *context, const void *reg_buf,
+ size_t reg_size, void *val_buf, size_t val_size)
+{
+ struct ads1262 *st = context;
+ struct spi_transfer xfer = {
+ .tx_buf = st->tx,
+ .rx_buf = st->rx,
+ .len = reg_size + 1 + val_size,
+ };
+ int ret;
+
+ guard(mutex)(&st->xfer_lock);
+
+ memset(st->tx, 0, reg_size + 1 + val_size);
+
+ memcpy(&st->tx[0], reg_buf, 1);
+ st->tx[1] = val_size - 1;
+
+ ret = spi_sync_transfer(st->spi, &xfer, 1);
+ if (ret)
+ return ret;
+
+ memcpy(val_buf, &st->rx[2], val_size);
+
+ return 0;
+}
+
+static int ads1262_regmap_gather_write(void *context, const void *reg_buf,
+ size_t reg_size, const void *val_buf,
+ size_t val_size)
+{
+ struct ads1262 *st = context;
+ struct spi_transfer xfer = {
+ .tx_buf = st->tx,
+ .rx_buf = st->rx,
+ .len = reg_size + 1 + val_size,
+ };
+
+ guard(mutex)(&st->xfer_lock);
+
+ memset(st->tx, 0, reg_size + 1 + val_size);
+
+ memcpy(&st->tx[0], reg_buf, 1);
+ st->tx[1] = val_size - 1;
+ memcpy(&st->tx[2], val_buf, val_size);
+
+ return spi_sync_transfer(st->spi, &xfer, 1);
+}
+
+static int ads1262_regmap_write(void *context, const void *data, size_t count)
+{
+ return ads1262_regmap_gather_write(context, data, 1, data + 1,
+ count - 1);
+}
+
+static const struct regmap_bus ads1262_regmap_bus = {
+ .read = ads1262_regmap_read,
+ .gather_write = ads1262_regmap_gather_write,
+ .write = ads1262_regmap_write,
+ .reg_format_endian_default = REGMAP_ENDIAN_BIG,
+ .val_format_endian_default = REGMAP_ENDIAN_BIG,
+ /* The first two bytes of the buffer are reserved for the protocol */
+ .max_raw_read = ADS1262_XFER_BUFFER_SZ - 2,
+ .max_raw_write = ADS1262_XFER_BUFFER_SZ - 2,
+};
+
+static int ads1262_channel_sanity_check(struct ads1262 *st,
+ struct ads1262_channel *chan)
+{
+ struct device *dev = &st->spi->dev;
+ int pos_uV, neg_uV;
+
+ /* Positive reference */
+ switch (chan->reference[0]) {
+ case ADS1262_REFMUX_INTERNAL:
+ /* Internal voltage reference is 2.5 V */
+ pos_uV = 2500000;
+ break;
+ case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
+ if (st->refp_uV == -ENODEV)
+ return dev_err_probe(dev, -ENODEV, "refp-supply not found\n");
+ pos_uV = st->refp_uV;
+ break;
+ case ADS1262_REFMUX_AVDD_AVSS:
+ pos_uV = st->avdd_uV;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* Negative reference */
+ switch (chan->reference[1]) {
+ case ADS1262_REFMUX_INTERNAL:
+ neg_uV = 0;
+ break;
+ case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
+ neg_uV = st->refn_uV == -ENODEV ? 0 : st->refn_uV;
+ break;
+ case ADS1262_REFMUX_AVDD_AVSS:
+ neg_uV = 0;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* If the positive reference is bellow the negative, reverse it */
+ if (pos_uV < neg_uV)
+ chan->ref_reversal = 1;
+
+ /* ADC2 reference */
+ switch (chan->reference[2]) {
+ case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
+ if (st->refp_uV == -ENODEV)
+ return dev_err_probe(dev, -ENODEV, "refp-supply not found\n");
+ pos_uV = st->refp_uV;
+ neg_uV = st->refn_uV == -ENODEV ? 0 : st->refn_uV;
+
+ /* We can't reverse ADC2 reference */
+ if (pos_uV < neg_uV && st->info->has_aux_adc)
+ return dev_err_probe(dev, -EINVAL,
+ "ADC2 doesn't support negative reference voltage\n");
+
+ break;
+ }
+
+ return 0;
+}
+
+static const char * const ads1262_ref_sources_positive[] = {
+ [ADS1262_REFMUX_INTERNAL] = "internal",
+ [ADS1262_REFMUX_AIN0_AIN1] = "ain0",
+ [ADS1262_REFMUX_AIN2_AIN3] = "ain2",
+ [ADS1262_REFMUX_AIN4_AIN5] = "ain4",
+ [ADS1262_REFMUX_AVDD_AVSS] = "avdd"
+};
+
+static const char * const ads1262_ref_sources_negative[] = {
+ [ADS1262_REFMUX_INTERNAL] = "internal",
+ [ADS1262_REFMUX_AIN0_AIN1] = "ain1",
+ [ADS1262_REFMUX_AIN2_AIN3] = "ain3",
+ [ADS1262_REFMUX_AIN4_AIN5] = "ain5",
+ [ADS1262_REFMUX_AVDD_AVSS] = "avss"
+};
+
+static const char * const ads1262_ref_sources_adc2[] = {
+ [ADS1262_REFMUX_INTERNAL] = "internal",
+ [ADS1262_REFMUX_AIN0_AIN1] = "ain0-ain1",
+ [ADS1262_REFMUX_AIN2_AIN3] = "ain2-ain3",
+ [ADS1262_REFMUX_AIN4_AIN5] = "ain4-ain5",
+ [ADS1262_REFMUX_AVDD_AVSS] = "avdd-avss"
+};
+
+static int ads1262_parse_channel_node(struct ads1262 *st,
+ struct ads1262_channel *chan,
+ struct fwnode_handle *node)
+{
+ struct device *dev = &st->spi->dev;
+ const char *ref_sources[3] = {};
+ u32 pins[2];
+ int ret;
+
+ /* Write non-zero default configuration values */
+ chan->data_rate = ADS1262_DR_20_SPS;
+
+ ret = fwnode_property_read_u32_array(node, "diff-channels", pins, ARRAY_SIZE(pins));
+ if (ret)
+ return dev_err_probe(dev, ret, "%s: Failed to read diff-channels\n",
+ fwnode_get_name(node));
+ if (pins[0] >= ADS1262_INPMUX_LAST || pins[1] >= ADS1262_INPMUX_LAST)
+ return dev_err_probe(dev, -EINVAL, "%s: input channels not in range\n",
+ fwnode_get_name(node));
+ chan->input[0] = pins[0];
+ chan->input[1] = pins[1];
+
+ if (fwnode_property_present(node, "reference-sources")) {
+ ret = fwnode_property_read_string_array(node, "reference-sources", ref_sources,
+ st->info->has_aux_adc ? 3 : 2);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "%s: Failed to read reference-sources\n",
+ fwnode_get_name(node));
+
+ ret = ads1262_find_string(ads1262_ref_sources_positive, ref_sources[0]);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "%s: Invalid positive reference\n",
+ fwnode_get_name(node));
+ chan->reference[0] = ret;
+
+ ret = ads1262_find_string(ads1262_ref_sources_negative, ref_sources[1]);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "%s: Invalid negative reference\n",
+ fwnode_get_name(node));
+ chan->reference[1] = ret;
+
+ if (st->info->has_aux_adc) {
+ ret = ads1262_find_string(ads1262_ref_sources_adc2, ref_sources[2]);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "%s: Invalid ADC2 reference\n",
+ fwnode_get_name(node));
+ chan->reference[2] = ret;
+ }
+ }
+
+ if (fwnode_property_present(node, "ti,pga-bypass"))
+ chan->pga_bypass = 1;
+
+ if (fwnode_property_present(node, "input-chopping"))
+ chan->input_chop = 1;
+
+ if (fwnode_property_present(node, "ti,idac-chopping"))
+ chan->idac_chop = 1;
+
+ return ads1262_channel_sanity_check(st, chan);
+}
+
+static int ads1262_parse_firmware(struct ads1262 *st)
+{
+ struct device *dev = &st->spi->dev;
+ struct clk *clk;
+ u32 reg;
+ int ret;
+
+ /* Set the nominal clock frequency */
+ clk = devm_clk_get_optional_enabled_with_rate(dev, NULL, 7372800);
+ if (IS_ERR(clk))
+ return dev_err_probe(dev, PTR_ERR(clk),
+ "Failed to get external clock\n");
+
+ ret = devm_regulator_get_enable(dev, "dvdd");
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to get dvdd regulator\n");
+
+ st->avdd_uV = devm_regulator_get_enable_read_voltage(dev, "avdd");
+ if (st->avdd_uV < 0)
+ return dev_err_probe(dev, st->avdd_uV, "Failed to get avdd regulator\n");
+
+ st->refp_uV = devm_regulator_get_enable_read_voltage(dev, "refp");
+ if (st->refp_uV < 0 && st->refp_uV != -ENODEV)
+ return dev_err_probe(dev, st->refp_uV, "Failed to get refp regulator\n");
+
+ st->refn_uV = devm_regulator_get_enable_read_voltage(dev, "refn");
+ if (st->refn_uV < 0 && st->refn_uV != -ENODEV)
+ return dev_err_probe(dev, st->refn_uV, "Failed to get refn regulator\n");
+
+ st->start_gpiod = devm_gpiod_get_optional(dev, "start", GPIOD_OUT_LOW);
+ if (IS_ERR(st->start_gpiod))
+ return dev_err_probe(dev, PTR_ERR(st->start_gpiod),
+ "Failed to get start GPIO\n");
+
+ st->reset_gpiod = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
+ if (IS_ERR(st->reset_gpiod))
+ return dev_err_probe(dev, PTR_ERR(st->reset_gpiod),
+ "Failed to get reset GPIO\n");
+
+ st->num_channels = device_get_named_child_node_count(dev, "channel");
+ if (!st->num_channels)
+ return dev_err_probe(dev, -ENXIO,
+ "No 'channel' nodes configured\n");
+ if (st->num_channels > ADS1262_MAX_CHANNEL_COUNT)
+ return dev_err_probe(dev, -EINVAL, "Too many channels\n");
+
+ st->channels = devm_kcalloc(dev, st->num_channels, sizeof(*st->channels),
+ GFP_KERNEL);
+ if (!st->channels)
+ return -ENOMEM;
+
+ device_for_each_named_child_node_scoped(dev, node, "channel") {
+ ret = fwnode_property_read_u32(node, "reg", ®);
+ if (ret)
+ return dev_err_probe(dev, ret, "%s: Failed to read channel reg\n",
+ fwnode_get_name(node));
+ if (reg >= st->num_channels)
+ return dev_err_probe(dev, -EINVAL, "%s: reg out of range\n",
+ fwnode_get_name(node));
+
+ ret = ads1262_parse_channel_node(st, &st->channels[reg], node);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ads1262_spi_probe(struct spi_device *spi)
+{
+ const struct ads1262_chip_info *info;
+ struct iio_chan_spec *channels;
+ struct device *dev = &spi->dev;
+ struct iio_dev *indio_dev;
+ struct ads1262 *st;
+ int num_channels;
+ int ret;
+
+ info = spi_get_device_match_data(spi);
+ if (!info)
+ return -EINVAL;
+
+ indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ st = iio_priv(indio_dev);
+ st->spi = spi;
+ st->indio_dev = indio_dev;
+ st->info = info;
+ init_completion(&st->drdy);
+ dev_set_drvdata(dev, st);
+
+ ret = devm_mutex_init(dev, &st->chan_lock);
+ if (ret)
+ return ret;
+ ret = devm_mutex_init(dev, &st->xfer_lock);
+ if (ret)
+ return ret;
+
+ ret = ads1262_parse_firmware(st);
+ if (ret)
+ return ret;
+
+ st->regmap = devm_regmap_init(dev, &ads1262_regmap_bus, st,
+ &ads1262_regmap_config);
+ if (IS_ERR(st->regmap))
+ return PTR_ERR(st->regmap);
+
+ ret = ads1262_dev_configure(st);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to configure device\n");
+
+ num_channels = ads1262_alloc_channels(st, &channels);
+ if (num_channels < 0)
+ return num_channels;
+ indio_dev->name = info->name;
+ indio_dev->modes = INDIO_DIRECT_MODE;
+ indio_dev->info = &ads1262_iio_info;
+ indio_dev->channels = channels;
+ indio_dev->num_channels = num_channels;
+
+ if (spi->irq > 0) {
+ ret = devm_request_irq(dev, spi->irq, ads1262_irq_handler,
+ IRQF_NO_THREAD, info->name, st);
+ if (ret)
+ return ret;
+ }
+
+ return devm_iio_device_register(dev, indio_dev);
+}
+
+static const struct ads1262_chip_info ads1262_chip_info = {
+ .name = "ads1262",
+ .has_aux_adc = false,
+};
+
+static const struct ads1262_chip_info ads1263_chip_info = {
+ .name = "ads1263",
+ .has_aux_adc = true,
+};
+
+static const struct of_device_id ads1262_of_match[] = {
+ { .compatible = "ti,ads1263", .data = &ads1263_chip_info },
+ { .compatible = "ti,ads1262", .data = &ads1262_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ads1262_of_match);
+
+static const struct spi_device_id ads1262_spi_match[] = {
+ { .name = "ads1263", .driver_data = (kernel_ulong_t)&ads1263_chip_info },
+ { .name = "ads1262", .driver_data = (kernel_ulong_t)&ads1262_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(spi, ads1262_spi_match);
+
+static struct spi_driver ads1262_spi_driver = {
+ .driver = {
+ .name = "ads1262",
+ .of_match_table = ads1262_of_match,
+ },
+ .probe = ads1262_spi_probe,
+ .id_table = ads1262_spi_match,
+};
+module_spi_driver(ads1262_spi_driver);
+
+MODULE_DESCRIPTION("Texas Instruments ADS1262 ADC driver");
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Kurt Borja <kuurtb@gmail.com>");
--
2.54.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* Re: [PATCH v2 2/7] iio: adc: Add ti-ads1262 driver
2026-06-28 5:36 ` [PATCH v2 2/7] iio: adc: Add ti-ads1262 driver Kurt Borja
@ 2026-06-28 17:15 ` David Lechner
2026-06-28 20:00 ` Kurt Borja
0 siblings, 1 reply; 14+ messages in thread
From: David Lechner @ 2026-06-28 17:15 UTC (permalink / raw)
To: Kurt Borja, Jonathan Cameron, Rob Herring, Krzysztof Kozlowski,
Conor Dooley
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel
On 6/28/26 12:36 AM, Kurt Borja wrote:
> Add the ti-ads1262 driver with initial support for the primary ADC
> (ADC1). The ADS1263 auxiliary ADC (ADC2) is handled by a separate driver
> and interoperability considerations were taken into account.
>
> Signed-off-by: Kurt Borja <kuurtb@gmail.com>
> ---
> MAINTAINERS | 1 +
> drivers/iio/adc/Kconfig | 13 +
> drivers/iio/adc/Makefile | 1 +
> drivers/iio/adc/ti-ads1262.c | 1206 ++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 1221 insertions(+)
Ideally, an intial patch would be 1/2 this size. Over 1000 lines in
a single patch make it take more than twice as long to review as
if it was split into two patches (or you just don't get a detailed
review). For example, sample rate and gain can be split out into
seprate patches.
I certainly don't have time to revew this plus an additonal 1000
lines of more patches on top of it in a week.
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 9b83d294734b574d..d868b25f2c65bcd9 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -26928,6 +26928,7 @@ M: Kurt Borja <kuurtb@gmail.com>
> L: linux-iio@vger.kernel.org
> S: Maintained
> F: Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
> +F: drivers/iio/adc/ti-ads1262.c
>
> TI ADS7924 ADC DRIVER
> M: Hugo Villeneuve <hvilleneuve@dimonoff.com>
> diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
> index 5845db2011fb5be1..6051092c20b96731 100644
> --- a/drivers/iio/adc/Kconfig
> +++ b/drivers/iio/adc/Kconfig
> @@ -1811,6 +1811,19 @@ config TI_ADS124S08
> This driver can also be built as a module. If so, the module will be
> called ti-ads124s08.
>
> +config TI_ADS1262
> + tristate "Texas Instruments ADS1262"
> + depends on SPI
> + select REGMAP
> + select IIO_BUFFER
> + select IIO_TRIGGERED_BUFFER
Buffer support isn't added until a later patch.
> + help
> + If you say yes here you get support for Texas Instruments ADS1262 and
> + ADS1263 ADC chips.
> +
> + This driver can also be built as a module. If so, the module will be
> + called ti-ads1262.
> +
> config TI_ADS1298
> tristate "Texas Instruments ADS1298"
> depends on SPI
> diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile
> index 72f4c6b33ca87b3f..4b1f89a2317a35f7 100644
> --- a/drivers/iio/adc/Makefile
> +++ b/drivers/iio/adc/Makefile
> @@ -155,6 +155,7 @@ obj-$(CONFIG_TI_ADS1100) += ti-ads1100.o
> obj-$(CONFIG_TI_ADS1119) += ti-ads1119.o
> obj-$(CONFIG_TI_ADS112C14) += ti-ads112c14.o
> obj-$(CONFIG_TI_ADS124S08) += ti-ads124s08.o
> +obj-$(CONFIG_TI_ADS1262) += ti-ads1262.o
> obj-$(CONFIG_TI_ADS1298) += ti-ads1298.o
> obj-$(CONFIG_TI_ADS131E08) += ti-ads131e08.o
> obj-$(CONFIG_TI_ADS131M02) += ti-ads131m02.o
> diff --git a/drivers/iio/adc/ti-ads1262.c b/drivers/iio/adc/ti-ads1262.c
> new file mode 100644
> index 0000000000000000..6103cf5a2d1624a9
> --- /dev/null
> +++ b/drivers/iio/adc/ti-ads1262.c
> @@ -0,0 +1,1206 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Texas Instruments ADS1262 ADC driver
> + *
> + * Copyright (C) 2025 Kurt Borja <kuurtb@gmail.com>
> + */
> +
> +#include <linux/array_size.h>
> +#include <linux/align.h>
> +#include <linux/bitfield.h>
> +#include <linux/bitmap.h>
> +#include <linux/bitops.h>
> +#include <linux/cleanup.h>
> +#include <linux/clk.h>
> +#include <linux/completion.h>
> +#include <linux/compiler_attributes.h>
> +#include <linux/compiler_types.h>
> +#include <linux/delay.h>
> +#include <linux/device.h>
> +#include <linux/gpio/consumer.h>
> +#include <linux/interrupt.h>
> +#include <linux/lockdep.h>
> +#include <linux/math.h>
> +#include <linux/math64.h>
> +#include <linux/module.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/mutex.h>
> +#include <linux/overflow.h>
> +#include <linux/property.h>
> +#include <linux/regmap.h>
> +#include <linux/regulator/consumer.h>
> +#include <linux/spi/spi.h>
> +#include <linux/string.h>
> +#include <linux/time.h>
> +#include <linux/types.h>
> +#include <linux/units.h>
> +
> +#include <asm/byteorder.h>
> +
> +#include <linux/iio/iio.h>
> +
> +#define ADS1262_OPCODE_NOP 0x00
> +#define ADS1262_OPCODE_RESET 0x06
> +#define ADS1262_OPCODE_START1 0x08
> +#define ADS1262_OPCODE_STOP1 0x0A
> +#define ADS1262_OPCODE_START2 0x0C
> +#define ADS1262_OPCODE_STOP2 0x0E
> +#define ADS1262_OPCODE_RDATA1 0x12
> +#define ADS1262_OPCODE_RDATA2 0x14
> +#define ADS1262_OPCODE_SYOCAL1 0x16
> +#define ADS1262_OPCODE_SYGCAL1 0x17
> +#define ADS1262_OPCODE_SFOCAL1 0x19
> +#define ADS1262_OPCODE_SYOCAL2 0x1B
> +#define ADS1262_OPCODE_SYGCAL2 0x1C
> +#define ADS1262_OPCODE_SFOCAL2 0x1E
> +#define ADS1262_OPCODE_RREG 0x20
> +#define ADS1262_OPCODE_WREG 0x40
> +
> +#define ADS1262_ID_REG 0x00
> +#define ADS1262_DEV_ID_MASK GENMASK(7, 5)
> +#define ADS1262_REV_ID_MASK GENMASK(4, 0)
> +
> +#define ADS1262_POWER_REG 0x01
> +#define ADS1262_POWER_RESET_MASK BIT(4)
> +#define ADS1262_POWER_VBIAS_MASK BIT(1)
> +#define ADS1262_POWER_INTREF_MASK BIT(0)
> +
> +#define ADS1262_INTERFACE_REG 0x02
> +#define ADS1262_INTERFACE_TIMEOUT_MASK BIT(3)
> +#define ADS1262_INTERFACE_STATUS_MASK BIT(2)
> +#define ADS1262_INTERFACE_CRC_MASK GENMASK(1, 0)
> +
> +#define ADS1262_MODE0_REG 0x03
> +#define ADS1262_MODE0_REFREV_MASK BIT(7)
> +#define ADS1262_MODE0_RUNMODE_MASK BIT(6)
> +#define ADS1262_MODE0_IDAC_CHOP_MASK BIT(5)
> +#define ADS1262_MODE0_INPUT_CHOP_MASK BIT(4)
> +#define ADS1262_MODE0_DELAY_MASK GENMASK(3, 0)
> +
> +#define ADS1262_MODE1_REG 0x04
> +#define ADS1262_MODE1_FILTER_MASK GENMASK(7, 5)
> +#define ADS1262_MODE1_SBADC_MASK BIT(4)
> +#define ADS1262_MODE1_SBPOL_MASK BIT(3)
> +#define ADS1262_MODE1_SBMAG_MASK GENMASK(2, 0)
> +
> +#define ADS1262_MODE2_REG 0x05
> +#define ADS1262_MODE2_BYPASS_MASK BIT(7)
> +#define ADS1262_MODE2_GAIN_MASK GENMASK(6, 4)
> +#define ADS1262_MODE2_DR_MASK GENMASK(3, 0)
> +
> +#define ADS1262_INPMUX_REG 0x06
> +#define ADS1262_INPMUX_MUXP_MASK GENMASK(7, 4)
> +#define ADS1262_INPMUX_MUXN_MASK GENMASK(3, 0)
> +
> +#define ADS1262_OFCAL0_REG 0x07
> +#define ADS1262_OFCAL1_REG 0x08
> +#define ADS1262_OFCAL2_REG 0x09
> +#define ADS1262_FSCAL0_REG 0x0A
> +#define ADS1262_FSCAL1_REG 0x0B
> +#define ADS1262_FSCAL2_REG 0x0C
> +
> +#define ADS1262_IDACMUX_REG 0x0D
> +#define ADS1262_IDACMUX_MUX2_MASK GENMASK(7, 4)
> +#define ADS1262_IDACMUX_MUX1_MASK GENMASK(3, 0)
> +
> +#define ADS1262_IDACMAG_REG 0x0E
> +#define ADS1262_IDACMAG_MAG2_MASK GENMASK(7, 4)
> +#define ADS1262_IDACMAG_MAG1_MASK GENMASK(3, 0)
> +
> +#define ADS1262_REFMUX_REG 0x0F
> +#define ADS1262_REFMUX_RMUXP_MASK GENMASK(5, 3)
> +#define ADS1262_REFMUX_RMUXN_MASK GENMASK(2, 0)
> +
> +#define ADS1262_TDACP_REG 0x10
> +#define ADS1262_TDACP_OUTP_MASK BIT(7)
> +#define ADS1262_TDACP_MAGP_MASK GENMASK(4, 0)
> +
> +#define ADS1262_TDACN_REG 0x11
> +#define ADS1262_TDACN_OUTN_MASK BIT(7)
> +#define ADS1262_TDACN_MAGN_MASK GENMASK(4, 0)
> +
> +#define ADS1262_GPIOCON_REG 0x12
> +#define ADS1262_GPIODIR_REG 0x13
> +#define ADS1262_GPIODAT_REG 0x14
> +
> +#define ADS1262_ADC2CFG_REG 0x15
> +#define ADS1262_ADC2CFG_DR2_MASK GENMASK(7, 6)
> +#define ADS1262_ADC2CFG_REF2_MASK GENMASK(5, 3)
> +#define ADS1262_ADC2CFG_GAIN2_MASK GENMASK(2, 0)
> +
> +#define ADS1262_ADC2MUX_REG 0x16
> +#define ADS1262_ADC2MUX_MUXP2_MASK GENMASK(7, 4)
> +#define ADS1262_ADC2MUX_MUXN2_MASK GENMASK(3, 0)
> +
> +#define ADS1262_ADC2OFC0_REG 0x17
> +#define ADS1262_ADC2OFC1_REG 0x18
> +#define ADS1262_ADC2FSC0_REG 0x19
> +#define ADS1262_ADC2FSC1_REG 0x1A
> +#define ADS1262_REG_COUNT 0x1B
> +
> +#define ADS1262_MAX_CHANNEL_COUNT 16
> +#define ADS1262_XFER_BUFFER_SZ 11
> +
> +enum {
> + ADS1262_RUNMODE_CONTINUOUS,
> + ADS1262_RUNMODE_PULSE,
> +};
> +
> +enum {
> + ADS1262_DR_2_5_SPS,
> + ADS1262_DR_5_SPS,
> + ADS1262_DR_10_SPS,
> + ADS1262_DR_16_6_SPS,
> + ADS1262_DR_20_SPS,
> + ADS1262_DR_50_SPS,
> + ADS1262_DR_60_SPS,
> + ADS1262_DR_100_SPS,
> + ADS1262_DR_400_SPS,
> + ADS1262_DR_1200_SPS,
> + ADS1262_DR_2400_SPS,
> + ADS1262_DR_4800_SPS,
> + ADS1262_DR_7200_SPS,
> + ADS1262_DR_14400_SPS,
> + ADS1262_DR_19200_SPS,
> + ADS1262_DR_38400_SPS,
> +};
> +
> +enum {
> + ADS1262_INPMUX_AIN0,
> + ADS1262_INPMUX_AIN1,
> + ADS1262_INPMUX_AIN2,
> + ADS1262_INPMUX_AIN3,
> + ADS1262_INPMUX_AIN4,
> + ADS1262_INPMUX_AIN5,
> + ADS1262_INPMUX_AIN6,
> + ADS1262_INPMUX_AIN7,
> + ADS1262_INPMUX_AIN8,
> + ADS1262_INPMUX_AIN9,
> + ADS1262_INPMUX_AINCOM,
> + ADS1262_INPMUX_TEMP,
> + ADS1262_INPMUX_AVDD,
> + ADS1262_INPMUX_DVDD,
> + ADS1262_INPMUX_TDAC,
> + ADS1262_INPMUX_FLOAT,
> + ADS1262_INPMUX_LAST
> +};
> +
> +enum {
> + ADS1262_REFMUX_INTERNAL,
> + ADS1262_REFMUX_AIN0_AIN1,
> + ADS1262_REFMUX_AIN2_AIN3,
> + ADS1262_REFMUX_AIN4_AIN5,
> + ADS1262_REFMUX_AVDD_AVSS,
> + ADS1262_REFMUX_LAST
> +};
> +
> +struct ads1262_chip_info {
> + const char *name;
> + bool has_aux_adc;
> +};
> +
> +struct ads1262_channel {
> + u8 input[2];
Since this is already stored in channel and channel2, we don't really need
to repeat the same thing here.
> + u8 gain;
> + u8 data_rate;
> + u8 reference[3];
I would just make 3 fields for this. We never pass a variable
value for the index, so would be easier to understand if we
give it proper names.
> + u8 pga_bypass:1;
> + u8 ref_reversal:1;
> + u8 input_chop:1;
> + u8 idac_chop:1;
Just make these bool instead of bitfields.
> +};
> +
> +struct ads1262 {
> + struct spi_device *spi;
> + const struct ads1262_chip_info *info;
> + struct regmap *regmap;
> + struct iio_dev *indio_dev;
> + struct gpio_desc *reset_gpiod;
> + struct gpio_desc *start_gpiod;
> +
> + /* Protects channel state */
> + struct mutex chan_lock;
> + unsigned int num_channels;
> + struct ads1262_channel *channels;
> + struct completion drdy;
> +
> + int refp_uV;
> + int refn_uV;
> + int avdd_uV;
> +
> + /* Protects transfer buffers and concurrent SPI transfers */
> + struct mutex xfer_lock;
> +
> + u8 tx[ADS1262_XFER_BUFFER_SZ] __aligned(IIO_DMA_MINALIGN);
> + u8 rx[ADS1262_XFER_BUFFER_SZ] __aligned(IIO_DMA_MINALIGN);
2nd one doesn't need __aligned(IIO_DMA_MINALIGN). They are never used at
the same time by different DMA operations.
> +};
> +
> +static const int ads1262_data_rate_avail[][2] = {
> + [ADS1262_DR_2_5_SPS] = { 2, 500000 },
> + [ADS1262_DR_5_SPS] = { 5, 0 },
> + [ADS1262_DR_10_SPS] = { 10, 0 },
> + [ADS1262_DR_16_6_SPS] = { 16, 666667 },
> + [ADS1262_DR_20_SPS] = { 20, 0 },
> + [ADS1262_DR_50_SPS] = { 50, 0 },
> + [ADS1262_DR_60_SPS] = { 60, 0 },
> + [ADS1262_DR_100_SPS] = { 100, 0 },
> + [ADS1262_DR_400_SPS] = { 400, 0 },
> + [ADS1262_DR_1200_SPS] = { 1200, 0 },
> + [ADS1262_DR_2400_SPS] = { 2400, 0 },
> + [ADS1262_DR_4800_SPS] = { 4800, 0 },
> + [ADS1262_DR_7200_SPS] = { 7200, 0 },
> + [ADS1262_DR_14400_SPS] = { 14400, 0 },
> + [ADS1262_DR_19200_SPS] = { 19200, 0 },
> + [ADS1262_DR_38400_SPS] = { 38400, 0 },
> +};
> +
> +static const int ads1262_pga_gain_avail[] = {
> + 1, 2, 4, 8, 16, 32
> +};
> +
> +#define ads1262_find_one(_arr, _val) \
> +({ \
> + int _i; \
> + for (_i = 0; _i < ARRAY_SIZE(_arr); _i++) { \
> + if ((_val) == _arr[_i]) \
> + break; \
> + } \
> + if (_i == ARRAY_SIZE(_arr)) \
> + _i = -EINVAL; \
> + _i; \
> +})
> +
> +#define ads1262_find_two(_arr, _val, _val2) \
> +({ \
> + int _i; \
> + for (_i = 0; _i < ARRAY_SIZE(_arr); _i++) { \
> + if ((_val) == _arr[_i][0] && (_val2) == _arr[_i][1]) \
> + break; \
> + } \
> + if (_i == ARRAY_SIZE(_arr)) \
> + _i = -EINVAL; \
> + _i; \
> +})
It looks like these are only used once, so not much point in making it
a macro.
> +
> +#define ads1262_find_string(_arr, _str) \
Isn't this the same as match_string()?
> +({ \
> + int _i; \
> + for (_i = 0; _i < ARRAY_SIZE(_arr); _i++) { \
> + if (!strcmp(_arr[_i], _str)) \
> + break; \
> + } \
> + if (_i == ARRAY_SIZE(_arr)) \
> + _i = -EINVAL; \
> + _i; \
> +})
> +
> +static int ads1262_dev_cmd(struct ads1262 *st, u8 opcode)
> +{
> + guard(mutex)(&st->xfer_lock);
> +
> + return spi_write_then_read(st->spi, &opcode, sizeof(opcode), NULL, 0);
> +}
> +
> +static int ads1262_dev_reset(struct ads1262 *st)
I would call this soft_reset since there is also a hardware reset pin.
Or also add the hardware reset here.
> +{
> + int ret;
> +
> + ret = ads1262_dev_cmd(st, ADS1262_OPCODE_RESET);
> + if (ret)
> + return ret;
> +
> + /* RESET command timing requirement is 1.085 usecs (Table 9-32) */
> + fsleep(2);
> +
> + return ret;
> +}
> +
> +static int ads1262_dev_start(struct ads1262 *st)
> +{
> + int ret;
> +
> + if (st->start_gpiod)
> + ret = gpiod_set_value_cansleep(st->start_gpiod, 1);
> + else
> + ret = ads1262_dev_cmd(st, ADS1262_OPCODE_START1);
> +
> + return ret;
> +}
> +
> +static int ads1262_dev_stop(struct ads1262 *st)
> +{
> + int ret;
> +
> + if (st->start_gpiod)
> + ret = gpiod_set_value_cansleep(st->start_gpiod, 0);
> + else
> + ret = ads1262_dev_cmd(st, ADS1262_OPCODE_STOP1);
> +
> + return ret;
> +}
> +
> +static int ads1262_dev_start_one(struct ads1262 *st, u8 runmode)
> +{
> + int ret;
> +
> + ret = ads1262_dev_start(st);
> + if (ret)
> + return ret;
> +
> + if (runmode == ADS1262_RUNMODE_CONTINUOUS)
Why would we not just put the device in single-shot mode to start
a single conversion?
OK, anwsered later.
> + return ads1262_dev_stop(st);
> +
> + return 0;
> +}
> +
> +static int ads1262_calculate_scale(struct ads1262 *st, u8 realbits, u8 gain,
> + u8 pos_ref, u8 neg_ref, int *val, int *val2)
> +{
> + u64 divd, divr, tmp, rem;
> + int pos_uV, neg_uV;
> +
> + switch (pos_ref) {
> + case ADS1262_REFMUX_INTERNAL:
> + /* Internal voltage reference is 2.5 V */
> + pos_uV = 2500000;
> + break;
> + case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
> + pos_uV = st->refp_uV;
I don't think it is safe to assume the same reference is wired to all of these.
> + break;
> + case ADS1262_REFMUX_AVDD_AVSS:
> + pos_uV = st->avdd_uV;
> + break;
> + default:
> + return -EINVAL;
> + }
> +
> + switch (neg_ref) {
> + case ADS1262_REFMUX_INTERNAL:
> + neg_uV = 0;
> + break;
> + case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
> + neg_uV = st->refn_uV == -ENODEV ? 0 : st->refn_uV;
Why not just store 0 in refn_uV directly?
> + break;
> + case ADS1262_REFMUX_AVDD_AVSS:
> + neg_uV = 0;
> + break;
> +
> + default:
> + return -EINVAL;
> + }
> +
Could use a comment to explain why pos would ever be less than negative.
> + if (pos_uV < neg_uV)
Why not checking chan->ref_reversal?
> + divd = neg_uV - pos_uV;
> + else
> + divd = pos_uV - neg_uV;
Why not just use abs()?
> + divr = BIT_ULL(gain + realbits - 1) * 1000;
> + tmp = div64_u64(divd * NANO, divr);
Not a fan of variables named tmp. Would prefer a meaningful name like scale_nano.
> +
> + *val = div64_u64_rem(tmp, NANO, &rem);
> + *val2 = rem;
> +
> + return 0;
> +}
> +
> +static int ads1262_channel_get_scale(struct ads1262 *st,
> + struct iio_chan_spec const *chan, int *val,
> + int *val2)
> +{
> + struct ads1262_channel *chan_data = &st->channels[chan->scan_index];
> +
> + return ads1262_calculate_scale(st, chan->scan_type.realbits, chan_data->gain,
> + chan_data->reference[0], chan_data->reference[1],
> + val, val2);
> +}
> +
> +static long ads1262_wait_for_conversion(struct ads1262 *st)
> +{
> + /*
> + * The first conversion latency is affected by the channel's data rate,
> + * filter, the configurable conversion delay and whether chop mode
> + * and/or IDAC rotation mode are enabled.
> + *
> + * The worst possible latency is calculated by taking the lowest data
> + * rate (2.5 SPS) and the sinc4 filter. This gives a latency of 1600 ms
> + * (Table 9-13). Then add the slowest configurable conversion delay
> + * (9 ms) and multiply by 4 to account for chop and IDAC rotation modes
> + * (Equation 20).
> + *
> + * Final result is 4 * (1600 ms + 9 ms) = 6436.
> + */
> + return wait_for_completion_interruptible_timeout(&st->drdy, msecs_to_jiffies(6436));
> +}
> +
> +static int ads1262_dev_read_by_cmd(struct ads1262 *st, u8 cmd, __be32 *val)
Would be more logical to put this right after ads1262_dev_cmd().
> +{
> + guard(mutex)(&st->xfer_lock);
> +
> + return spi_write_then_read(st->spi, &cmd, sizeof(cmd), val, sizeof(*val));
> +}
> +
> +static int ads1262_channel_enable(struct ads1262 *st,
> + struct ads1262_channel *chan)
> +{
> + u8 mode0, mode2, inpmux, refmux;
> + int ret;
> +
> + /* Avoid using guard() here to mitigate AB/BA deadlock warning */
> + mutex_lock(&st->chan_lock);
> + mode0 = FIELD_PREP(ADS1262_MODE0_INPUT_CHOP_MASK, chan->input_chop) |
> + FIELD_PREP(ADS1262_MODE0_IDAC_CHOP_MASK, chan->idac_chop) |
> + FIELD_PREP(ADS1262_MODE0_REFREV_MASK, chan->ref_reversal);
> + mode2 = FIELD_PREP(ADS1262_MODE2_DR_MASK, chan->data_rate) |
> + FIELD_PREP(ADS1262_MODE2_GAIN_MASK, chan->gain) |
> + FIELD_PREP(ADS1262_MODE2_BYPASS_MASK, chan->pga_bypass);
> + inpmux = FIELD_PREP(ADS1262_INPMUX_MUXN_MASK, chan->input[1]) |
> + FIELD_PREP(ADS1262_INPMUX_MUXP_MASK, chan->input[0]);
> + refmux = FIELD_PREP(ADS1262_REFMUX_RMUXN_MASK, chan->reference[1]) |
> + FIELD_PREP(ADS1262_REFMUX_RMUXP_MASK, chan->reference[0]);
> + mutex_unlock(&st->chan_lock);
Why does lock not also include actually writing the bits?
> +
> + ret = regmap_update_bits(st->regmap, ADS1262_MODE0_REG,
> + ADS1262_MODE0_INPUT_CHOP_MASK |
> + ADS1262_MODE0_IDAC_CHOP_MASK |
> + ADS1262_MODE0_REFREV_MASK, mode0);
> + if (ret)
> + return ret;
> +
> + ret = regmap_update_bits(st->regmap, ADS1262_MODE2_REG,
> + ADS1262_MODE2_DR_MASK |
> + ADS1262_MODE2_GAIN_MASK |
> + ADS1262_MODE2_BYPASS_MASK, mode2);
> + if (ret)
> + return ret;
> +
> + ret = regmap_update_bits(st->regmap, ADS1262_INPMUX_REG,
> + ADS1262_INPMUX_MUXN_MASK |
> + ADS1262_INPMUX_MUXP_MASK, inpmux);
> + if (ret)
> + return ret;
> +
> + return regmap_update_bits(st->regmap, ADS1262_REFMUX_REG,
> + ADS1262_REFMUX_RMUXN_MASK |
> + ADS1262_REFMUX_RMUXP_MASK, refmux);
> +}
> +
> +static int ads1262_set_runmode(struct ads1262 *st, u8 runmode)
> +{
> + return regmap_update_bits(st->regmap, ADS1262_MODE0_REG,
> + ADS1262_MODE0_RUNMODE_MASK,
> + FIELD_PREP(ADS1262_MODE0_RUNMODE_MASK, runmode));
> +}
> +
> +static int ads1262_channel_read(struct ads1262 *st,
> + struct ads1262_channel *chan_data, __be32 *val)
> +{
> + u8 runmode;
> + int ret;
> +
> + IIO_DEV_ACQUIRE_DIRECT_MODE(st->indio_dev, claim);
> + if (IIO_DEV_ACQUIRE_FAILED(claim))
> + return -EBUSY;
> +
> + /*
> + * When a channel has chop mode or IDAC rotation mode, the first
> + * conversion is always withheld so the datasheet suggests using the
> + * CONTINUOUS mode and briefly starting and stopping conversions to
> + * achieve the same effect (Section 9.4.1.2).
> + */
> + if (chan_data->input_chop || chan_data->idac_chop)
> + runmode = ADS1262_RUNMODE_CONTINUOUS;
> + else
> + runmode = ADS1262_RUNMODE_PULSE;
> +
> + ret = ads1262_set_runmode(st, runmode);
> + if (ret)
> + return ret;
> +
> + ret = ads1262_channel_enable(st, chan_data);
> + if (ret)
> + return ret;
> +
> + reinit_completion(&st->drdy);
> +
> + ret = ads1262_dev_start_one(st, runmode);
> + if (ret)
> + return ret;
> +
> + ads1262_wait_for_conversion(st);
This can timeout, so would expect to check return value here.
> +
> + return ads1262_dev_read_by_cmd(st, ADS1262_OPCODE_RDATA1, val);
> +}
> +
> +static int ads1262_read_raw(struct iio_dev *indio_dev,
> + struct iio_chan_spec const *chan, int *val,
> + int *val2, long mask)
> +{
> + struct ads1262 *st = iio_priv(indio_dev);
> + struct ads1262_channel *chan_data = &st->channels[chan->scan_index];
> + u8 realbits = chan->scan_type.realbits;
> + __be32 raw;
> + int ret;
> +
> + switch (mask) {
> + case IIO_CHAN_INFO_RAW:
> + ret = ads1262_channel_read(st, chan_data, &raw);
> + if (ret)
> + return ret;
> + *val = sign_extend32(be32_to_cpu(raw), realbits - 1);
> +
> + return IIO_VAL_INT;
> +
> + case IIO_CHAN_INFO_SCALE: {
> + guard(mutex)(&st->chan_lock);
> +
> + ret = ads1262_channel_get_scale(st, chan, val, val2);
> + if (ret)
> + return ret;
> +
> + return IIO_VAL_INT_PLUS_NANO;
> + }
> +
> + case IIO_CHAN_INFO_HARDWAREGAIN: {
There is only one other ADC that uses "hardwaregain". Usually, we just make
scale writeable to control the gain. I don't remember what the rules for
that attribute are. Using it for in_voltage is not documented in the ABI.
> + guard(mutex)(&st->chan_lock);
> +
> + *val = ads1262_pga_gain_avail[chan_data->gain];
> +
> + return IIO_VAL_INT;
> + }
> +
> + case IIO_CHAN_INFO_SAMP_FREQ: {
> + guard(mutex)(&st->chan_lock);
> +
> + *val = ads1262_data_rate_avail[chan_data->data_rate][0];
> + *val2 = ads1262_data_rate_avail[chan_data->data_rate][1];
> +
> + return IIO_VAL_INT_PLUS_MICRO;
> + }
> +
> + default:
> + return -EOPNOTSUPP;
> + }
> +}
> +
> +static int ads1262_read_avail(struct iio_dev *indio_dev,
> + struct iio_chan_spec const *chan, const int **vals,
> + int *type, int *length, long mask)
> +{
> + switch (mask) {
> + case IIO_CHAN_INFO_SAMP_FREQ:
> + *type = IIO_VAL_INT_PLUS_MICRO;
> + *vals = (const int *)ads1262_data_rate_avail;
> + *length = ARRAY_SIZE(ads1262_data_rate_avail) * 2;
> + return IIO_AVAIL_LIST;
> +
> + case IIO_CHAN_INFO_HARDWAREGAIN:
> + *type = IIO_VAL_INT;
> + *vals = ads1262_pga_gain_avail;
> + *length = ARRAY_SIZE(ads1262_pga_gain_avail);
> + return IIO_AVAIL_LIST;
> +
> + default:
> + return -EOPNOTSUPP;
> + }
> +}
> +
> +static int ads1262_write_raw(struct iio_dev *indio_dev,
> + struct iio_chan_spec const *chan, int val,
> + int val2, long mask)
> +{
> + struct ads1262 *st = iio_priv(indio_dev);
> + struct ads1262_channel *chan_data;
> + int i;
> +
> + chan_data = &st->channels[chan->scan_index];
> +
> + switch (mask) {
> + case IIO_CHAN_INFO_SAMP_FREQ: {
> + i = ads1262_find_two(ads1262_data_rate_avail, val, val2);
> + if (i < 0)
> + return i;
> +
> + guard(mutex)(&st->chan_lock);
> + chan_data->data_rate = i;
> +
> + break;
> + }
> +
> + case IIO_CHAN_INFO_HARDWAREGAIN: {
> + i = ads1262_find_one(ads1262_pga_gain_avail, val);
> + if (i < 0)
> + return i;
> +
> + guard(mutex)(&st->chan_lock);
> + chan_data->gain = i;
> +
> + break;
> + }
> +
> + default:
> + return -EOPNOTSUPP;
> + }
> +
> + return 0;
> +}
> +
> +static int ads1262_write_raw_get_fmt(struct iio_dev *indio_dev,
> + struct iio_chan_spec const *chan, long mask)
> +{
> + switch (mask) {
> + case IIO_CHAN_INFO_CONVDELAY:
Looks like this belongs in a later patch.
> + return IIO_VAL_INT_PLUS_NANO;
> + default:
> + return IIO_VAL_INT_PLUS_MICRO;
> + }
> +}
> +
> +static int ads1262_debugfs_reg_access(struct iio_dev *indio_dev, unsigned int reg,
> + unsigned int writeval, unsigned int *readval)
> +{
> + struct ads1262 *st = iio_priv(indio_dev);
> +
> + if (readval)
> + return regmap_read_bypassed(st->regmap, reg, readval);
> +
> + return regmap_write(st->regmap, reg, writeval);
> +}
> +
> +static const struct iio_info ads1262_iio_info = {
> + .read_raw = ads1262_read_raw,
> + .read_avail = ads1262_read_avail,
> + .write_raw = ads1262_write_raw,
> + .write_raw_get_fmt = ads1262_write_raw_get_fmt,
> + .debugfs_reg_access = ads1262_debugfs_reg_access,
> +};
> +
> +static irqreturn_t ads1262_irq_handler(int irq, void *dev_id)
> +{
> + struct ads1262 *st = dev_id;
> +
> + complete(&st->drdy);
> +
> + return IRQ_HANDLED;
> +}
> +
> +static int ads1262_alloc_channels(struct ads1262 *st,
> + struct iio_chan_spec **channels)
> +{
> + struct device *dev = &st->spi->dev;
> + struct ads1262_channel *chan_data;
> + struct iio_chan_spec *chans;
> + unsigned int i, num_channels;
> +
> + /* Account for the timestamp channel */
> + num_channels = st->num_channels + 1;
> + chans = devm_kcalloc(dev, num_channels, sizeof(*chans), GFP_KERNEL);
> + if (!chans)
> + return -ENOMEM;
> +
> + for (i = 0; i < st->num_channels; i++) {
> + chan_data = &st->channels[i];
> + chans[i] = (struct iio_chan_spec) {
> + .type = IIO_VOLTAGE,
> + .channel = chan_data->input[0],
> + .channel2 = chan_data->input[1],
> + .scan_index = i,
> + .scan_type = {
> + .format = IIO_SCAN_FORMAT_SIGNED_INT,
> + .realbits = 32,
> + .storagebits = 32,
> + .endianness = IIO_BE,
> + },
> + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
> + BIT(IIO_CHAN_INFO_SCALE) |
> + BIT(IIO_CHAN_INFO_HARDWAREGAIN) |
> + BIT(IIO_CHAN_INFO_SAMP_FREQ),
> + .info_mask_shared_by_type_available =
> + BIT(IIO_CHAN_INFO_HARDWAREGAIN) |
Gain available might make more sense as separate if there are restrictions
on gain allowed due to other conditions.
> + BIT(IIO_CHAN_INFO_SAMP_FREQ),
Could be the same case with sampling frequency if filters can affect that.
> + .indexed = true,
> + .differential = true,
These two make more sense closer to the top since they affect the
channel naming along with type and channel.
> + };
> + }
As mentioned in the DT bindings review, I would make the diagnostic
channels fixed so that they don't have to be always specified in
the devicetree.
> +
> + chans[i] = IIO_CHAN_SOFT_TIMESTAMP(i);
> +
> + *channels = chans;
> +
> + return num_channels;
> +}
> +
> +static int ads1262_dev_configure(struct ads1262 *st)
> +{
> + struct device *dev = &st->spi->dev;
> + int ret;
> +
> + ret = ads1262_dev_reset(st);
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to reset device\n");
> +
> + ret = regmap_clear_bits(st->regmap, ADS1262_POWER_REG,
> + ADS1262_POWER_RESET_MASK);
> + if (ret)
> + return ret;
> +
> + ret = regmap_clear_bits(st->regmap, ADS1262_INTERFACE_REG,
> + ADS1262_INTERFACE_STATUS_MASK |
> + ADS1262_INTERFACE_CRC_MASK);
> + if (ret)
> + return ret;
> +
> + if (device_property_present(dev, "ti,vbias")) {
> + ret = regmap_set_bits(st->regmap, ADS1262_POWER_REG,
> + ADS1262_POWER_VBIAS_MASK);
> + if (ret)
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static const struct regmap_range ads1262_read_write_range[] = {
> + regmap_reg_range(ADS1262_ID_REG, ADS1262_ADC2FSC1_REG),
> +};
> +
> +static const struct regmap_range ads1262_read_only_range[] = {
> + regmap_reg_range(ADS1262_ID_REG, ADS1262_ID_REG),
> +};
> +
> +static const struct regmap_access_table ads1262_wr_table = {
> + .yes_ranges = ads1262_read_write_range,
> + .n_yes_ranges = ARRAY_SIZE(ads1262_read_write_range),
> + .no_ranges = ads1262_read_only_range,
> + .n_no_ranges = ARRAY_SIZE(ads1262_read_only_range),
> +};
> +
> +static const struct regmap_access_table ads1262_rd_table = {
> + .yes_ranges = ads1262_read_write_range,
> + .n_yes_ranges = ARRAY_SIZE(ads1262_read_write_range),
> +};
> +
> +static const struct reg_default ads1262_reg_defaults[] = {
> + { ADS1262_POWER_REG, 0x11 },
> + { ADS1262_INTERFACE_REG, 0x05 },
> + { ADS1262_MODE0_REG, 0x00 },
> + { ADS1262_MODE1_REG, 0x80 },
> + { ADS1262_MODE2_REG, 0x04 },
> + { ADS1262_INPMUX_REG, 0x01 },
> + { ADS1262_OFCAL0_REG, 0x00 },
> + { ADS1262_OFCAL1_REG, 0x00 },
> + { ADS1262_OFCAL2_REG, 0x00 },
> + { ADS1262_FSCAL0_REG, 0x00 },
> + { ADS1262_FSCAL1_REG, 0x00 },
> + { ADS1262_FSCAL2_REG, 0x40 },
> + { ADS1262_IDACMUX_REG, 0xBB },
> + { ADS1262_IDACMAG_REG, 0x00 },
> + { ADS1262_REFMUX_REG, 0x00 },
> + { ADS1262_TDACP_REG, 0x00 },
> + { ADS1262_TDACN_REG, 0x00 },
> + { ADS1262_GPIOCON_REG, 0x00 },
> + { ADS1262_GPIODIR_REG, 0x00 },
> + { ADS1262_GPIODAT_REG, 0x00 },
> + { ADS1262_ADC2CFG_REG, 0x00 },
> + { ADS1262_ADC2MUX_REG, 0x01 },
> + { ADS1262_ADC2OFC0_REG, 0x00 },
> + { ADS1262_ADC2OFC1_REG, 0x00 },
> + { ADS1262_ADC2FSC0_REG, 0x00 },
> + { ADS1262_ADC2FSC1_REG, 0x40 },
> +};
Jonathan says he prefers to spell out the default values (e.g.
with FIELD_PREP_CONST()) it it isn't too messy.
> +
> +static const struct regmap_config ads1262_regmap_config = {
> + .reg_bits = 8,
> + .val_bits = 8,
> + .wr_table = &ads1262_wr_table,
> + .rd_table = &ads1262_rd_table,
> + .reg_defaults = ads1262_reg_defaults,
> + .num_reg_defaults = ARRAY_SIZE(ads1262_reg_defaults),
> + .max_register = ADS1262_ADC2FSC1_REG,
> + .read_flag_mask = ADS1262_OPCODE_RREG,
> + .write_flag_mask = ADS1262_OPCODE_WREG,
> + .can_sleep = true,
> + .cache_type = REGCACHE_MAPLE,
Need to define volatile registers in order for cache to work correctly.
> +};
> +
> +static int ads1262_regmap_read(void *context, const void *reg_buf,
> + size_t reg_size, void *val_buf, size_t val_size)
> +{
> + struct ads1262 *st = context;
> + struct spi_transfer xfer = {
> + .tx_buf = st->tx,
> + .rx_buf = st->rx,
> + .len = reg_size + 1 + val_size,
> + };
> + int ret;
> +
> + guard(mutex)(&st->xfer_lock);
SPI bus and regmap both already have their own locking, so putting a lock
here seems out of place. Instead, the lock should be for higher-level
operations where there are mulitple register access in a single operation.
> +
> + memset(st->tx, 0, reg_size + 1 + val_size);
> +
> + memcpy(&st->tx[0], reg_buf, 1);
> + st->tx[1] = val_size - 1;
> +
> + ret = spi_sync_transfer(st->spi, &xfer, 1);
> + if (ret)
> + return ret;
> +
> + memcpy(val_buf, &st->rx[2], val_size);
> +
> + return 0;
> +}
> +
> +static int ads1262_regmap_gather_write(void *context, const void *reg_buf,
> + size_t reg_size, const void *val_buf,
> + size_t val_size)
> +{
> + struct ads1262 *st = context;
> + struct spi_transfer xfer = {
> + .tx_buf = st->tx,
> + .rx_buf = st->rx,
> + .len = reg_size + 1 + val_size,
> + };
> +
> + guard(mutex)(&st->xfer_lock);
> +
> + memset(st->tx, 0, reg_size + 1 + val_size);
> +
> + memcpy(&st->tx[0], reg_buf, 1);
> + st->tx[1] = val_size - 1;
> + memcpy(&st->tx[2], val_buf, val_size);
> +
> + return spi_sync_transfer(st->spi, &xfer, 1);
> +}
> +
> +static int ads1262_regmap_write(void *context, const void *data, size_t count)
> +{
> + return ads1262_regmap_gather_write(context, data, 1, data + 1,
> + count - 1);
> +}
> +
> +static const struct regmap_bus ads1262_regmap_bus = {
> + .read = ads1262_regmap_read,
> + .gather_write = ads1262_regmap_gather_write,
> + .write = ads1262_regmap_write,
> + .reg_format_endian_default = REGMAP_ENDIAN_BIG,
> + .val_format_endian_default = REGMAP_ENDIAN_BIG,
> + /* The first two bytes of the buffer are reserved for the protocol */
> + .max_raw_read = ADS1262_XFER_BUFFER_SZ - 2,
> + .max_raw_write = ADS1262_XFER_BUFFER_SZ - 2,
> +};
Why do we need our own bus instead of using REGMAP_SPI?
> +
> +static int ads1262_channel_sanity_check(struct ads1262 *st,
> + struct ads1262_channel *chan)
> +{
> + struct device *dev = &st->spi->dev;
> + int pos_uV, neg_uV;
> +
> + /* Positive reference */
> + switch (chan->reference[0]) {
> + case ADS1262_REFMUX_INTERNAL:
> + /* Internal voltage reference is 2.5 V */
> + pos_uV = 2500000;
> + break;
> + case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
> + if (st->refp_uV == -ENODEV)
> + return dev_err_probe(dev, -ENODEV, "refp-supply not found\n");
> + pos_uV = st->refp_uV;
> + break;
> + case ADS1262_REFMUX_AVDD_AVSS:
> + pos_uV = st->avdd_uV;
> + break;
> + default:
> + return -EINVAL;
> + }
> +
> + /* Negative reference */
> + switch (chan->reference[1]) {
> + case ADS1262_REFMUX_INTERNAL:
> + neg_uV = 0;
> + break;
> + case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
> + neg_uV = st->refn_uV == -ENODEV ? 0 : st->refn_uV;
> + break;
> + case ADS1262_REFMUX_AVDD_AVSS:
> + neg_uV = 0;
> + break;
> + default:
> + return -EINVAL;
> + }
> +
> + /* If the positive reference is bellow the negative, reverse it */
s/bellow/below/
> + if (pos_uV < neg_uV)
> + chan->ref_reversal = 1;
> +
> + /* ADC2 reference */
> + switch (chan->reference[2]) {
> + case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
> + if (st->refp_uV == -ENODEV)
> + return dev_err_probe(dev, -ENODEV, "refp-supply not found\n");
> + pos_uV = st->refp_uV;
> + neg_uV = st->refn_uV == -ENODEV ? 0 : st->refn_uV;
> +
> + /* We can't reverse ADC2 reference */
> + if (pos_uV < neg_uV && st->info->has_aux_adc)
> + return dev_err_probe(dev, -EINVAL,
> + "ADC2 doesn't support negative reference voltage\n");
> +
> + break;
> + }
> +
> + return 0;
> +}
> +
> +static const char * const ads1262_ref_sources_positive[] = {
> + [ADS1262_REFMUX_INTERNAL] = "internal",
> + [ADS1262_REFMUX_AIN0_AIN1] = "ain0",
> + [ADS1262_REFMUX_AIN2_AIN3] = "ain2",
> + [ADS1262_REFMUX_AIN4_AIN5] = "ain4",
> + [ADS1262_REFMUX_AVDD_AVSS] = "avdd"
> +};
> +
> +static const char * const ads1262_ref_sources_negative[] = {
> + [ADS1262_REFMUX_INTERNAL] = "internal",
> + [ADS1262_REFMUX_AIN0_AIN1] = "ain1",
> + [ADS1262_REFMUX_AIN2_AIN3] = "ain3",
> + [ADS1262_REFMUX_AIN4_AIN5] = "ain5",
> + [ADS1262_REFMUX_AVDD_AVSS] = "avss"
> +};
> +
> +static const char * const ads1262_ref_sources_adc2[] = {
> + [ADS1262_REFMUX_INTERNAL] = "internal",
> + [ADS1262_REFMUX_AIN0_AIN1] = "ain0-ain1",
> + [ADS1262_REFMUX_AIN2_AIN3] = "ain2-ain3",
> + [ADS1262_REFMUX_AIN4_AIN5] = "ain4-ain5",
> + [ADS1262_REFMUX_AVDD_AVSS] = "avdd-avss"
> +};
> +
> +static int ads1262_parse_channel_node(struct ads1262 *st,
> + struct ads1262_channel *chan,
> + struct fwnode_handle *node)
> +{
> + struct device *dev = &st->spi->dev;
> + const char *ref_sources[3] = {};
IIO style is with space between `{ }`.
> + u32 pins[2];
> + int ret;
> +
> + /* Write non-zero default configuration values */
> + chan->data_rate = ADS1262_DR_20_SPS;
> +
> + ret = fwnode_property_read_u32_array(node, "diff-channels", pins, ARRAY_SIZE(pins));
> + if (ret)
> + return dev_err_probe(dev, ret, "%s: Failed to read diff-channels\n",
> + fwnode_get_name(node));
> + if (pins[0] >= ADS1262_INPMUX_LAST || pins[1] >= ADS1262_INPMUX_LAST)
> + return dev_err_probe(dev, -EINVAL, "%s: input channels not in range\n",
> + fwnode_get_name(node));
> + chan->input[0] = pins[0];
> + chan->input[1] = pins[1];
> +
> + if (fwnode_property_present(node, "reference-sources")) {
> + ret = fwnode_property_read_string_array(node, "reference-sources", ref_sources,
> + st->info->has_aux_adc ? 3 : 2);
> + if (ret < 0)
> + return dev_err_probe(dev, ret, "%s: Failed to read reference-sources\n",
> + fwnode_get_name(node));
> +
> + ret = ads1262_find_string(ads1262_ref_sources_positive, ref_sources[0]);
> + if (ret < 0)
> + return dev_err_probe(dev, ret, "%s: Invalid positive reference\n",
> + fwnode_get_name(node));
> + chan->reference[0] = ret;
> +
> + ret = ads1262_find_string(ads1262_ref_sources_negative, ref_sources[1]);
> + if (ret < 0)
> + return dev_err_probe(dev, ret, "%s: Invalid negative reference\n",
> + fwnode_get_name(node));
> + chan->reference[1] = ret;
> +
> + if (st->info->has_aux_adc) {
> + ret = ads1262_find_string(ads1262_ref_sources_adc2, ref_sources[2]);
> + if (ret < 0)
> + return dev_err_probe(dev, ret, "%s: Invalid ADC2 reference\n",
> + fwnode_get_name(node));
> + chan->reference[2] = ret;
> + }
> + }
> +
> + if (fwnode_property_present(node, "ti,pga-bypass"))
> + chan->pga_bypass = 1;
These can be one-line:
chan->pga_bypass = fwnode_property_read_bool(node, "ti,pga-bypass");
> +
> + if (fwnode_property_present(node, "input-chopping"))
> + chan->input_chop = 1;
> +
> + if (fwnode_property_present(node, "ti,idac-chopping"))
> + chan->idac_chop = 1;
> +
> + return ads1262_channel_sanity_check(st, chan);
> +}
> +
> +static int ads1262_parse_firmware(struct ads1262 *st)
> +{
> + struct device *dev = &st->spi->dev;
> + struct clk *clk;
> + u32 reg;
> + int ret;
> +
> + /* Set the nominal clock frequency */
> + clk = devm_clk_get_optional_enabled_with_rate(dev, NULL, 7372800);
This is quite unusual. Usually an external clock would be a fixed clock
and therefore can't be set.
> + if (IS_ERR(clk))
> + return dev_err_probe(dev, PTR_ERR(clk),
> + "Failed to get external clock\n");
> +
> + ret = devm_regulator_get_enable(dev, "dvdd");
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to get dvdd regulator\n");
> +
> + st->avdd_uV = devm_regulator_get_enable_read_voltage(dev, "avdd");
We only need the voltage of avdd if it is actually used as a reference, which
is probably quite rare. Not all regulators provide a voltage value.
> + if (st->avdd_uV < 0)
> + return dev_err_probe(dev, st->avdd_uV, "Failed to get avdd regulator\n");
> +
> + st->refp_uV = devm_regulator_get_enable_read_voltage(dev, "refp");
> + if (st->refp_uV < 0 && st->refp_uV != -ENODEV)
> + return dev_err_probe(dev, st->refp_uV, "Failed to get refp regulator\n");
> +
> + st->refn_uV = devm_regulator_get_enable_read_voltage(dev, "refn");
> + if (st->refn_uV < 0 && st->refn_uV != -ENODEV)
> + return dev_err_probe(dev, st->refn_uV, "Failed to get refn regulator\n");
> +
> + st->start_gpiod = devm_gpiod_get_optional(dev, "start", GPIOD_OUT_LOW);
> + if (IS_ERR(st->start_gpiod))
> + return dev_err_probe(dev, PTR_ERR(st->start_gpiod),
> + "Failed to get start GPIO\n");
> +
> + st->reset_gpiod = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
> + if (IS_ERR(st->reset_gpiod))
> + return dev_err_probe(dev, PTR_ERR(st->reset_gpiod),
> + "Failed to get reset GPIO\n");
This is currently never used.
> +
> + st->num_channels = device_get_named_child_node_count(dev, "channel");
> + if (!st->num_channels)
> + return dev_err_probe(dev, -ENXIO,
> + "No 'channel' nodes configured\n");
> + if (st->num_channels > ADS1262_MAX_CHANNEL_COUNT)
> + return dev_err_probe(dev, -EINVAL, "Too many channels\n");
> +
> + st->channels = devm_kcalloc(dev, st->num_channels, sizeof(*st->channels),
> + GFP_KERNEL);
> + if (!st->channels)
> + return -ENOMEM;
> +
> + device_for_each_named_child_node_scoped(dev, node, "channel") {
> + ret = fwnode_property_read_u32(node, "reg", ®);
> + if (ret)
> + return dev_err_probe(dev, ret, "%s: Failed to read channel reg\n",
> + fwnode_get_name(node));
> + if (reg >= st->num_channels)
> + return dev_err_probe(dev, -EINVAL, "%s: reg out of range\n",
> + fwnode_get_name(node));
If reg is going to be the channel index, then we need to make sure there are
no gaps in the array as well.
> +
> + ret = ads1262_parse_channel_node(st, &st->channels[reg], node);
> + if (ret)
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static int ads1262_spi_probe(struct spi_device *spi)
> +{
> + const struct ads1262_chip_info *info;
> + struct iio_chan_spec *channels;
> + struct device *dev = &spi->dev;
> + struct iio_dev *indio_dev;
> + struct ads1262 *st;
> + int num_channels;
> + int ret;
> +
> + info = spi_get_device_match_data(spi);
> + if (!info)
> + return -EINVAL;
> +
> + indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
> + if (!indio_dev)
> + return -ENOMEM;
> +
> + st = iio_priv(indio_dev);
> + st->spi = spi;
> + st->indio_dev = indio_dev;
It is quite rare to need a circular reference between indio_dev and private
data. We can eliminate it by passing indio_dev to ads1262_channel_read().
> + st->info = info;
> + init_completion(&st->drdy);
> + dev_set_drvdata(dev, st);
We never get this, so don't need to set it.
> +
> + ret = devm_mutex_init(dev, &st->chan_lock);
> + if (ret)
> + return ret;
> + ret = devm_mutex_init(dev, &st->xfer_lock);
> + if (ret)
> + return ret;
> +
> + ret = ads1262_parse_firmware(st);
> + if (ret)
> + return ret;
> +
> + st->regmap = devm_regmap_init(dev, &ads1262_regmap_bus, st,
> + &ads1262_regmap_config);
> + if (IS_ERR(st->regmap))
> + return PTR_ERR(st->regmap);
> +
> + ret = ads1262_dev_configure(st);
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to configure device\n");
> +
> + num_channels = ads1262_alloc_channels(st, &channels);
> + if (num_channels < 0)
> + return num_channels;
> + indio_dev->name = info->name;
> + indio_dev->modes = INDIO_DIRECT_MODE;
> + indio_dev->info = &ads1262_iio_info;
> + indio_dev->channels = channels;
> + indio_dev->num_channels = num_channels;
> +
> + if (spi->irq > 0) {
> + ret = devm_request_irq(dev, spi->irq, ads1262_irq_handler,
> + IRQF_NO_THREAD, info->name, st);
> + if (ret)
> + return ret;
It seems like the driver would not work without an interrupt currently
so this should not be optional.
> + }
> +
> + return devm_iio_device_register(dev, indio_dev);
> +}
> +
> +static const struct ads1262_chip_info ads1262_chip_info = {
> + .name = "ads1262",
> + .has_aux_adc = false,
> +};
> +
> +static const struct ads1262_chip_info ads1263_chip_info = {
> + .name = "ads1263",
> + .has_aux_adc = true,
> +};
> +
> +static const struct of_device_id ads1262_of_match[] = {
> + { .compatible = "ti,ads1263", .data = &ads1263_chip_info },
> + { .compatible = "ti,ads1262", .data = &ads1262_chip_info },
> + { }
> +};
> +MODULE_DEVICE_TABLE(of, ads1262_of_match);
> +
> +static const struct spi_device_id ads1262_spi_match[] = {
> + { .name = "ads1263", .driver_data = (kernel_ulong_t)&ads1263_chip_info },
> + { .name = "ads1262", .driver_data = (kernel_ulong_t)&ads1262_chip_info },
> + { }
> +};
> +MODULE_DEVICE_TABLE(spi, ads1262_spi_match);
> +
> +static struct spi_driver ads1262_spi_driver = {
> + .driver = {
> + .name = "ads1262",
> + .of_match_table = ads1262_of_match,
> + },
> + .probe = ads1262_spi_probe,
> + .id_table = ads1262_spi_match,
> +};
> +module_spi_driver(ads1262_spi_driver);
> +
> +MODULE_DESCRIPTION("Texas Instruments ADS1262 ADC driver");
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Kurt Borja <kuurtb@gmail.com>");
>
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH v2 2/7] iio: adc: Add ti-ads1262 driver
2026-06-28 17:15 ` David Lechner
@ 2026-06-28 20:00 ` Kurt Borja
0 siblings, 0 replies; 14+ messages in thread
From: Kurt Borja @ 2026-06-28 20:00 UTC (permalink / raw)
To: David Lechner, Kurt Borja, Jonathan Cameron, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel
On Sun Jun 28, 2026 at 12:15 PM -05, David Lechner wrote:
> On 6/28/26 12:36 AM, Kurt Borja wrote:
>> Add the ti-ads1262 driver with initial support for the primary ADC
>> (ADC1). The ADS1263 auxiliary ADC (ADC2) is handled by a separate driver
>> and interoperability considerations were taken into account.
>>
>> Signed-off-by: Kurt Borja <kuurtb@gmail.com>
>> ---
>> MAINTAINERS | 1 +
>> drivers/iio/adc/Kconfig | 13 +
>> drivers/iio/adc/Makefile | 1 +
>> drivers/iio/adc/ti-ads1262.c | 1206 ++++++++++++++++++++++++++++++++++++++++++
>> 4 files changed, 1221 insertions(+)
>
> Ideally, an intial patch would be 1/2 this size. Over 1000 lines in
> a single patch make it take more than twice as long to review as
> if it was split into two patches (or you just don't get a detailed
> review). For example, sample rate and gain can be split out into
> seprate patches.
>
> I certainly don't have time to revew this plus an additonal 1000
> lines of more patches on top of it in a week.
Sure, I'll split it even more.
[...]
>> +static int ads1262_calculate_scale(struct ads1262 *st, u8 realbits, u8 gain,
>> + u8 pos_ref, u8 neg_ref, int *val, int *val2)
>> +{
>> + u64 divd, divr, tmp, rem;
>> + int pos_uV, neg_uV;
>> +
>> + switch (pos_ref) {
>> + case ADS1262_REFMUX_INTERNAL:
>> + /* Internal voltage reference is 2.5 V */
>> + pos_uV = 2500000;
>> + break;
>> + case ADS1262_REFMUX_AIN0_AIN1...ADS1262_REFMUX_AIN4_AIN5:
>> + pos_uV = st->refp_uV;
>
> I don't think it is safe to assume the same reference is wired to all of these.
Yes, you're right. I'll add supplies for the other options.
[...]
>> +static int ads1262_channel_enable(struct ads1262 *st,
>> + struct ads1262_channel *chan)
>> +{
>> + u8 mode0, mode2, inpmux, refmux;
>> + int ret;
>> +
>> + /* Avoid using guard() here to mitigate AB/BA deadlock warning */
>> + mutex_lock(&st->chan_lock);
>> + mode0 = FIELD_PREP(ADS1262_MODE0_INPUT_CHOP_MASK, chan->input_chop) |
>> + FIELD_PREP(ADS1262_MODE0_IDAC_CHOP_MASK, chan->idac_chop) |
>> + FIELD_PREP(ADS1262_MODE0_REFREV_MASK, chan->ref_reversal);
>> + mode2 = FIELD_PREP(ADS1262_MODE2_DR_MASK, chan->data_rate) |
>> + FIELD_PREP(ADS1262_MODE2_GAIN_MASK, chan->gain) |
>> + FIELD_PREP(ADS1262_MODE2_BYPASS_MASK, chan->pga_bypass);
>> + inpmux = FIELD_PREP(ADS1262_INPMUX_MUXN_MASK, chan->input[1]) |
>> + FIELD_PREP(ADS1262_INPMUX_MUXP_MASK, chan->input[0]);
>> + refmux = FIELD_PREP(ADS1262_REFMUX_RMUXN_MASK, chan->reference[1]) |
>> + FIELD_PREP(ADS1262_REFMUX_RMUXP_MASK, chan->reference[0]);
>> + mutex_unlock(&st->chan_lock);
>
> Why does lock not also include actually writing the bits?
Because we take the xfer_lock in that path. Later in the series, I'm
forced to take the xfer_lock first, so that would be AB/BA bug. But...
See the regmap answer below.
[...]
>> +static int ads1262_read_raw(struct iio_dev *indio_dev,
>> + struct iio_chan_spec const *chan, int *val,
>> + int *val2, long mask)
>> +{
>> + struct ads1262 *st = iio_priv(indio_dev);
>> + struct ads1262_channel *chan_data = &st->channels[chan->scan_index];
>> + u8 realbits = chan->scan_type.realbits;
>> + __be32 raw;
>> + int ret;
>> +
>> + switch (mask) {
>> + case IIO_CHAN_INFO_RAW:
>> + ret = ads1262_channel_read(st, chan_data, &raw);
>> + if (ret)
>> + return ret;
>> + *val = sign_extend32(be32_to_cpu(raw), realbits - 1);
>> +
>> + return IIO_VAL_INT;
>> +
>> + case IIO_CHAN_INFO_SCALE: {
>> + guard(mutex)(&st->chan_lock);
>> +
>> + ret = ads1262_channel_get_scale(st, chan, val, val2);
>> + if (ret)
>> + return ret;
>> +
>> + return IIO_VAL_INT_PLUS_NANO;
>> + }
>> +
>> + case IIO_CHAN_INFO_HARDWAREGAIN: {
>
> There is only one other ADC that uses "hardwaregain". Usually, we just make
> scale writeable to control the gain. I don't remember what the rules for
> that attribute are. Using it for in_voltage is not documented in the ABI.
I went with hardwaregain because the scale loses too many significant
digits at high gain. With the internal reference and gain = 1, the scale
is at 0.000001164; then at gain = 32, the scale is at 0.000000036.
In this case I expect users to just calculate the scale themselves based
on the hardwaregain. Is this acceptable? If not I'll go with
scale_available.
[...]
>> +static int ads1262_alloc_channels(struct ads1262 *st,
>> + struct iio_chan_spec **channels)
>> +{
>> + struct device *dev = &st->spi->dev;
>> + struct ads1262_channel *chan_data;
>> + struct iio_chan_spec *chans;
>> + unsigned int i, num_channels;
>> +
>> + /* Account for the timestamp channel */
>> + num_channels = st->num_channels + 1;
>> + chans = devm_kcalloc(dev, num_channels, sizeof(*chans), GFP_KERNEL);
>> + if (!chans)
>> + return -ENOMEM;
>> +
>> + for (i = 0; i < st->num_channels; i++) {
>> + chan_data = &st->channels[i];
>> + chans[i] = (struct iio_chan_spec) {
>> + .type = IIO_VOLTAGE,
>> + .channel = chan_data->input[0],
>> + .channel2 = chan_data->input[1],
>> + .scan_index = i,
>> + .scan_type = {
>> + .format = IIO_SCAN_FORMAT_SIGNED_INT,
>> + .realbits = 32,
>> + .storagebits = 32,
>> + .endianness = IIO_BE,
>> + },
>> + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
>> + BIT(IIO_CHAN_INFO_SCALE) |
>> + BIT(IIO_CHAN_INFO_HARDWAREGAIN) |
>> + BIT(IIO_CHAN_INFO_SAMP_FREQ),
>> + .info_mask_shared_by_type_available =
>> + BIT(IIO_CHAN_INFO_HARDWAREGAIN) |
>
> Gain available might make more sense as separate if there are restrictions
> on gain allowed due to other conditions.
>
>> + BIT(IIO_CHAN_INFO_SAMP_FREQ),
>
> Could be the same case with sampling frequency if filters can affect that.
Actually, sampling frequency takes precedence over the filter
configuration. Some rates cause the chip to ignore the filter
configuration.
>
>> + .indexed = true,
>> + .differential = true,
>
> These two make more sense closer to the top since they affect the
> channel naming along with type and channel.
>
>> + };
>> + }
>
> As mentioned in the DT bindings review, I would make the diagnostic
> channels fixed so that they don't have to be always specified in
> the devicetree.
I agree. I'll add the monitors here.
[...]
>> +static int ads1262_regmap_read(void *context, const void *reg_buf,
>> + size_t reg_size, void *val_buf, size_t val_size)
>> +{
>> + struct ads1262 *st = context;
>> + struct spi_transfer xfer = {
>> + .tx_buf = st->tx,
>> + .rx_buf = st->rx,
>> + .len = reg_size + 1 + val_size,
>> + };
>> + int ret;
>> +
>> + guard(mutex)(&st->xfer_lock);
>
> SPI bus and regmap both already have their own locking, so putting a lock
> here seems out of place. Instead, the lock should be for higher-level
> operations where there are mulitple register access in a single operation.
I agree. I can definitely move this one to a "higher level". But IMO,
because this also protects tx and rx buffers, it makes sense to have it
here too.
>
>> +
>> + memset(st->tx, 0, reg_size + 1 + val_size);
>> +
>> + memcpy(&st->tx[0], reg_buf, 1);
>> + st->tx[1] = val_size - 1;
>> +
>> + ret = spi_sync_transfer(st->spi, &xfer, 1);
>> + if (ret)
>> + return ret;
>> +
>> + memcpy(val_buf, &st->rx[2], val_size);
>> +
>> + return 0;
>> +}
>> +
>> +static int ads1262_regmap_gather_write(void *context, const void *reg_buf,
>> + size_t reg_size, const void *val_buf,
>> + size_t val_size)
>> +{
>> + struct ads1262 *st = context;
>> + struct spi_transfer xfer = {
>> + .tx_buf = st->tx,
>> + .rx_buf = st->rx,
>> + .len = reg_size + 1 + val_size,
>> + };
>> +
>> + guard(mutex)(&st->xfer_lock);
>> +
>> + memset(st->tx, 0, reg_size + 1 + val_size);
>> +
>> + memcpy(&st->tx[0], reg_buf, 1);
>> + st->tx[1] = val_size - 1;
>> + memcpy(&st->tx[2], val_buf, val_size);
>> +
>> + return spi_sync_transfer(st->spi, &xfer, 1);
>> +}
>> +
>> +static int ads1262_regmap_write(void *context, const void *data, size_t count)
>> +{
>> + return ads1262_regmap_gather_write(context, data, 1, data + 1,
>> + count - 1);
>> +}
>> +
>> +static const struct regmap_bus ads1262_regmap_bus = {
>> + .read = ads1262_regmap_read,
>> + .gather_write = ads1262_regmap_gather_write,
>> + .write = ads1262_regmap_write,
>> + .reg_format_endian_default = REGMAP_ENDIAN_BIG,
>> + .val_format_endian_default = REGMAP_ENDIAN_BIG,
>> + /* The first two bytes of the buffer are reserved for the protocol */
>> + .max_raw_read = ADS1262_XFER_BUFFER_SZ - 2,
>> + .max_raw_write = ADS1262_XFER_BUFFER_SZ - 2,
>> +};
>
> Why do we need our own bus instead of using REGMAP_SPI?
Because the protocol is different. Read/write protocol for this chip is
first reg + command | number of regs - 1 | reg values
while regmap_spi is
first reg | reg values
I had a comment explaining this, but I forgot to restore it.
[...]
>> +static int ads1262_parse_firmware(struct ads1262 *st)
>> +{
>> + struct device *dev = &st->spi->dev;
>> + struct clk *clk;
>> + u32 reg;
>> + int ret;
>> +
>> + /* Set the nominal clock frequency */
>> + clk = devm_clk_get_optional_enabled_with_rate(dev, NULL, 7372800);
>
> This is quite unusual. Usually an external clock would be a fixed clock
> and therefore can't be set.
Really? It can be a crystal of course, but it also can be anything else.
Shouldn't I be trying to set the clock frequency in that case?
>
>> + if (IS_ERR(clk))
>> + return dev_err_probe(dev, PTR_ERR(clk),
>> + "Failed to get external clock\n");
>> +
>> + ret = devm_regulator_get_enable(dev, "dvdd");
>> + if (ret)
>> + return dev_err_probe(dev, ret, "Failed to get dvdd regulator\n");
>> +
>> + st->avdd_uV = devm_regulator_get_enable_read_voltage(dev, "avdd");
>
> We only need the voltage of avdd if it is actually used as a reference, which
> is probably quite rare. Not all regulators provide a voltage value.
Then I should just check for ENODEV here.
>
>> + if (st->avdd_uV < 0)
>> + return dev_err_probe(dev, st->avdd_uV, "Failed to get avdd regulator\n");
>> +
>> + st->refp_uV = devm_regulator_get_enable_read_voltage(dev, "refp");
>> + if (st->refp_uV < 0 && st->refp_uV != -ENODEV)
>> + return dev_err_probe(dev, st->refp_uV, "Failed to get refp regulator\n");
>> +
>> + st->refn_uV = devm_regulator_get_enable_read_voltage(dev, "refn");
>> + if (st->refn_uV < 0 && st->refn_uV != -ENODEV)
>> + return dev_err_probe(dev, st->refn_uV, "Failed to get refn regulator\n");
>> +
>> + st->start_gpiod = devm_gpiod_get_optional(dev, "start", GPIOD_OUT_LOW);
>> + if (IS_ERR(st->start_gpiod))
>> + return dev_err_probe(dev, PTR_ERR(st->start_gpiod),
>> + "Failed to get start GPIO\n");
>> +
>> + st->reset_gpiod = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
>> + if (IS_ERR(st->reset_gpiod))
>> + return dev_err_probe(dev, PTR_ERR(st->reset_gpiod),
>> + "Failed to get reset GPIO\n");
>
> This is currently never used.
It has to be de-asserted for the chip to be in an active state though.
[...]
>> +MODULE_DESCRIPTION("Texas Instruments ADS1262 ADC driver");
>> +MODULE_LICENSE("GPL");
>> +MODULE_AUTHOR("Kurt Borja <kuurtb@gmail.com>");
I agree with all other comments. Thanks a lot for the review!
--
Thanks,
~ Kurt
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v2 3/7] iio: adc: ti-ads1262: Add channel filter support
2026-06-28 5:36 [PATCH v2 0/7] iio: adc: Add TI ADS126X ADC family support Kurt Borja
2026-06-28 5:36 ` [PATCH v2 1/7] dt-bindings: iio: adc: Add TI ADS126x ADC family Kurt Borja
2026-06-28 5:36 ` [PATCH v2 2/7] iio: adc: Add ti-ads1262 driver Kurt Borja
@ 2026-06-28 5:36 ` Kurt Borja
2026-06-28 5:36 ` [PATCH v2 4/7] iio: adc: ti-ads1262: Add excitation current support Kurt Borja
` (3 subsequent siblings)
6 siblings, 0 replies; 14+ messages in thread
From: Kurt Borja @ 2026-06-28 5:36 UTC (permalink / raw)
To: Kurt Borja, Jonathan Cameron, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, David Lechner
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel, Jonathan Cameron
Expose per-channel filter configuration through the filter_type
attribute.
Signed-off-by: Kurt Borja <kuurtb@gmail.com>
---
drivers/iio/adc/ti-ads1262.c | 65 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 64 insertions(+), 1 deletion(-)
diff --git a/drivers/iio/adc/ti-ads1262.c b/drivers/iio/adc/ti-ads1262.c
index 6103cf5a2d1624a9..ece97a0c2b1304ad 100644
--- a/drivers/iio/adc/ti-ads1262.c
+++ b/drivers/iio/adc/ti-ads1262.c
@@ -146,6 +146,14 @@ enum {
ADS1262_RUNMODE_PULSE,
};
+enum {
+ ADS1262_FILTER_SINC1,
+ ADS1262_FILTER_SINC2,
+ ADS1262_FILTER_SINC3,
+ ADS1262_FILTER_SINC4,
+ ADS1262_FILTER_FIR,
+};
+
enum {
ADS1262_DR_2_5_SPS,
ADS1262_DR_5_SPS,
@@ -201,6 +209,7 @@ struct ads1262_chip_info {
struct ads1262_channel {
u8 input[2];
+ u8 filter;
u8 gain;
u8 data_rate;
u8 reference[3];
@@ -441,7 +450,7 @@ static int ads1262_dev_read_by_cmd(struct ads1262 *st, u8 cmd, __be32 *val)
static int ads1262_channel_enable(struct ads1262 *st,
struct ads1262_channel *chan)
{
- u8 mode0, mode2, inpmux, refmux;
+ u8 mode0, mode1, mode2, inpmux, refmux;
int ret;
/* Avoid using guard() here to mitigate AB/BA deadlock warning */
@@ -449,6 +458,7 @@ static int ads1262_channel_enable(struct ads1262 *st,
mode0 = FIELD_PREP(ADS1262_MODE0_INPUT_CHOP_MASK, chan->input_chop) |
FIELD_PREP(ADS1262_MODE0_IDAC_CHOP_MASK, chan->idac_chop) |
FIELD_PREP(ADS1262_MODE0_REFREV_MASK, chan->ref_reversal);
+ mode1 = FIELD_PREP(ADS1262_MODE1_FILTER_MASK, chan->filter);
mode2 = FIELD_PREP(ADS1262_MODE2_DR_MASK, chan->data_rate) |
FIELD_PREP(ADS1262_MODE2_GAIN_MASK, chan->gain) |
FIELD_PREP(ADS1262_MODE2_BYPASS_MASK, chan->pga_bypass);
@@ -465,6 +475,11 @@ static int ads1262_channel_enable(struct ads1262 *st,
if (ret)
return ret;
+ ret = regmap_update_bits(st->regmap, ADS1262_MODE1_REG,
+ ADS1262_MODE1_FILTER_MASK, mode1);
+ if (ret)
+ return ret;
+
ret = regmap_update_bits(st->regmap, ADS1262_MODE2_REG,
ADS1262_MODE2_DR_MASK |
ADS1262_MODE2_GAIN_MASK |
@@ -682,6 +697,52 @@ static irqreturn_t ads1262_irq_handler(int irq, void *dev_id)
return IRQ_HANDLED;
}
+static int ads1262_get_filter_type(struct iio_dev *indio_dev,
+ const struct iio_chan_spec *chan)
+{
+ struct ads1262 *st = iio_priv(indio_dev);
+ struct ads1262_channel *chan_data;
+
+ guard(mutex)(&st->chan_lock);
+
+ chan_data = &st->channels[chan->scan_index];
+ return chan_data->filter;
+}
+
+static int ads1262_set_filter_type(struct iio_dev *indio_dev,
+ const struct iio_chan_spec *chan,
+ unsigned int val)
+{
+ struct ads1262 *st = iio_priv(indio_dev);
+
+ guard(mutex)(&st->chan_lock);
+ st->channels[chan->scan_index].filter = val;
+
+ return 0;
+}
+
+static const char * const ads1262_filter_type_labels[] = {
+ [ADS1262_FILTER_SINC1] = "sinc1",
+ [ADS1262_FILTER_SINC2] = "sinc2",
+ [ADS1262_FILTER_SINC3] = "sinc3",
+ [ADS1262_FILTER_SINC4] = "sinc4",
+ [ADS1262_FILTER_FIR] = "fir",
+};
+
+static const struct iio_enum ads1262_filter_type_enum = {
+ .items = ads1262_filter_type_labels,
+ .num_items = ARRAY_SIZE(ads1262_filter_type_labels),
+ .get = ads1262_get_filter_type,
+ .set = ads1262_set_filter_type,
+};
+
+static const struct iio_chan_spec_ext_info ads1262_ext_info[] = {
+ IIO_ENUM("filter_type", IIO_SEPARATE, &ads1262_filter_type_enum),
+ IIO_ENUM_AVAILABLE("filter_type", IIO_SHARED_BY_TYPE,
+ &ads1262_filter_type_enum),
+ { }
+};
+
static int ads1262_alloc_channels(struct ads1262 *st,
struct iio_chan_spec **channels)
{
@@ -718,6 +779,7 @@ static int ads1262_alloc_channels(struct ads1262 *st,
BIT(IIO_CHAN_INFO_SAMP_FREQ),
.indexed = true,
.differential = true,
+ .ext_info = ads1262_ext_info,
};
}
@@ -983,6 +1045,7 @@ static int ads1262_parse_channel_node(struct ads1262 *st,
int ret;
/* Write non-zero default configuration values */
+ chan->filter = ADS1262_FILTER_FIR;
chan->data_rate = ADS1262_DR_20_SPS;
ret = fwnode_property_read_u32_array(node, "diff-channels", pins, ARRAY_SIZE(pins));
--
2.54.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH v2 4/7] iio: adc: ti-ads1262: Add excitation current support
2026-06-28 5:36 [PATCH v2 0/7] iio: adc: Add TI ADS126X ADC family support Kurt Borja
` (2 preceding siblings ...)
2026-06-28 5:36 ` [PATCH v2 3/7] iio: adc: ti-ads1262: Add channel filter support Kurt Borja
@ 2026-06-28 5:36 ` Kurt Borja
2026-06-28 5:36 ` [PATCH v2 5/7] iio: adc: ti-ads1262: Add conversion delay support Kurt Borja
` (2 subsequent siblings)
6 siblings, 0 replies; 14+ messages in thread
From: Kurt Borja @ 2026-06-28 5:36 UTC (permalink / raw)
To: Kurt Borja, Jonathan Cameron, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, David Lechner
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel, Jonathan Cameron
Support the two IDAC excitation current sources. Each channel can route
its IDAC1/IDAC2 outputs to a pin via the "excitation-channels" property
and select a magnitude via "excitation-current-nanoamp".
Signed-off-by: Kurt Borja <kuurtb@gmail.com>
---
drivers/iio/adc/ti-ads1262.c | 78 ++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 76 insertions(+), 2 deletions(-)
diff --git a/drivers/iio/adc/ti-ads1262.c b/drivers/iio/adc/ti-ads1262.c
index ece97a0c2b1304ad..8921eaae537f6b0a 100644
--- a/drivers/iio/adc/ti-ads1262.c
+++ b/drivers/iio/adc/ti-ads1262.c
@@ -193,6 +193,22 @@ enum {
ADS1262_INPMUX_LAST
};
+enum {
+ ADS1262_IDACMUX_AIN0,
+ ADS1262_IDACMUX_AIN1,
+ ADS1262_IDACMUX_AIN2,
+ ADS1262_IDACMUX_AIN3,
+ ADS1262_IDACMUX_AIN4,
+ ADS1262_IDACMUX_AIN5,
+ ADS1262_IDACMUX_AIN6,
+ ADS1262_IDACMUX_AIN7,
+ ADS1262_IDACMUX_AIN8,
+ ADS1262_IDACMUX_AIN9,
+ ADS1262_IDACMUX_AINCOM,
+ ADS1262_IDACMUX_NO_CONN,
+ ADS1262_IDACMUX_LAST
+};
+
enum {
ADS1262_REFMUX_INTERNAL,
ADS1262_REFMUX_AIN0_AIN1,
@@ -213,6 +229,8 @@ struct ads1262_channel {
u8 gain;
u8 data_rate;
u8 reference[3];
+ u8 idac_mux[2];
+ u8 idac_mag[2];
u8 pga_bypass:1;
u8 ref_reversal:1;
u8 input_chop:1;
@@ -450,7 +468,7 @@ static int ads1262_dev_read_by_cmd(struct ads1262 *st, u8 cmd, __be32 *val)
static int ads1262_channel_enable(struct ads1262 *st,
struct ads1262_channel *chan)
{
- u8 mode0, mode1, mode2, inpmux, refmux;
+ u8 mode0, mode1, mode2, inpmux, idacmux, idacmag, refmux;
int ret;
/* Avoid using guard() here to mitigate AB/BA deadlock warning */
@@ -464,6 +482,10 @@ static int ads1262_channel_enable(struct ads1262 *st,
FIELD_PREP(ADS1262_MODE2_BYPASS_MASK, chan->pga_bypass);
inpmux = FIELD_PREP(ADS1262_INPMUX_MUXN_MASK, chan->input[1]) |
FIELD_PREP(ADS1262_INPMUX_MUXP_MASK, chan->input[0]);
+ idacmux = FIELD_PREP(ADS1262_IDACMUX_MUX1_MASK, chan->idac_mux[0]) |
+ FIELD_PREP(ADS1262_IDACMUX_MUX2_MASK, chan->idac_mux[1]);
+ idacmag = FIELD_PREP(ADS1262_IDACMAG_MAG1_MASK, chan->idac_mag[0]) |
+ FIELD_PREP(ADS1262_IDACMAG_MAG2_MASK, chan->idac_mag[1]);
refmux = FIELD_PREP(ADS1262_REFMUX_RMUXN_MASK, chan->reference[1]) |
FIELD_PREP(ADS1262_REFMUX_RMUXP_MASK, chan->reference[0]);
mutex_unlock(&st->chan_lock);
@@ -493,6 +515,18 @@ static int ads1262_channel_enable(struct ads1262 *st,
if (ret)
return ret;
+ ret = regmap_update_bits(st->regmap, ADS1262_IDACMUX_REG,
+ ADS1262_IDACMUX_MUX1_MASK |
+ ADS1262_IDACMUX_MUX2_MASK, idacmux);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(st->regmap, ADS1262_IDACMAG_REG,
+ ADS1262_IDACMAG_MAG1_MASK |
+ ADS1262_IDACMAG_MAG2_MASK, idacmag);
+ if (ret)
+ return ret;
+
return regmap_update_bits(st->regmap, ADS1262_REFMUX_REG,
ADS1262_REFMUX_RMUXN_MASK |
ADS1262_REFMUX_RMUXP_MASK, refmux);
@@ -1040,13 +1074,19 @@ static int ads1262_parse_channel_node(struct ads1262 *st,
struct fwnode_handle *node)
{
struct device *dev = &st->spi->dev;
+ static const u32 idac_nA[] = {
+ 0, 50000, 100000, 250000, 500000, 750000,
+ 1000000, 1500000, 2000000, 2500000, 3000000
+ };
const char *ref_sources[3] = {};
- u32 pins[2];
+ u32 pins[2], mags[2];
int ret;
/* Write non-zero default configuration values */
chan->filter = ADS1262_FILTER_FIR;
chan->data_rate = ADS1262_DR_20_SPS;
+ chan->idac_mux[0] = ADS1262_IDACMUX_NO_CONN;
+ chan->idac_mux[1] = ADS1262_IDACMUX_NO_CONN;
ret = fwnode_property_read_u32_array(node, "diff-channels", pins, ARRAY_SIZE(pins));
if (ret)
@@ -1086,6 +1126,40 @@ static int ads1262_parse_channel_node(struct ads1262 *st,
}
}
+ if (fwnode_property_present(node, "excitation-channels")) {
+ ret = fwnode_property_read_u32_array(node, "excitation-channels",
+ pins, ARRAY_SIZE(pins));
+ if (ret)
+ return dev_err_probe(dev, ret, "%s: Failed to read excitation-channels\n",
+ fwnode_get_name(node));
+ if (pins[0] >= ADS1262_IDACMUX_LAST || pins[1] >= ADS1262_IDACMUX_LAST)
+ return dev_err_probe(dev, -EINVAL, "%s: excitation-channels not in range\n",
+ fwnode_get_name(node));
+ chan->idac_mux[0] = pins[0];
+ chan->idac_mux[1] = pins[1];
+
+ ret = fwnode_property_read_u32_array(node, "excitation-current-nanoamp",
+ mags, ARRAY_SIZE(mags));
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "%s: Failed to read excitation-current-nanoamp\n",
+ fwnode_get_name(node));
+
+ ret = ads1262_find_one(idac_nA, mags[0]);
+ if (ret < 0)
+ return dev_err_probe(dev, ret,
+ "%s: Invalid excitation-current-nanoamp\n",
+ fwnode_get_name(node));
+ chan->idac_mag[0] = ret;
+
+ ret = ads1262_find_one(idac_nA, mags[1]);
+ if (ret < 0)
+ return dev_err_probe(dev, ret,
+ "%s: Invalid excitation-current-nanoamp\n",
+ fwnode_get_name(node));
+ chan->idac_mag[1] = ret;
+ }
+
if (fwnode_property_present(node, "ti,pga-bypass"))
chan->pga_bypass = 1;
--
2.54.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH v2 5/7] iio: adc: ti-ads1262: Add conversion delay support
2026-06-28 5:36 [PATCH v2 0/7] iio: adc: Add TI ADS126X ADC family support Kurt Borja
` (3 preceding siblings ...)
2026-06-28 5:36 ` [PATCH v2 4/7] iio: adc: ti-ads1262: Add excitation current support Kurt Borja
@ 2026-06-28 5:36 ` Kurt Borja
2026-06-28 5:36 ` [PATCH v2 6/7] iio: adc: ti-ads1262: Add buffer and trigger support Kurt Borja
2026-06-28 5:36 ` [PATCH v2 7/7] iio: adc: Add ti-ads1263-adc2 driver Kurt Borja
6 siblings, 0 replies; 14+ messages in thread
From: Kurt Borja @ 2026-06-28 5:36 UTC (permalink / raw)
To: Kurt Borja, Jonathan Cameron, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, David Lechner
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel, Jonathan Cameron
Expose the programmable conversion start delay as a per-channel
IIO_CHAN_INFO_CONVDELAY attribute.
Signed-off-by: Kurt Borja <kuurtb@gmail.com>
---
drivers/iio/adc/ti-ads1262.c | 63 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 62 insertions(+), 1 deletion(-)
diff --git a/drivers/iio/adc/ti-ads1262.c b/drivers/iio/adc/ti-ads1262.c
index 8921eaae537f6b0a..4ae22c1b0b4b7d79 100644
--- a/drivers/iio/adc/ti-ads1262.c
+++ b/drivers/iio/adc/ti-ads1262.c
@@ -141,6 +141,21 @@
#define ADS1262_MAX_CHANNEL_COUNT 16
#define ADS1262_XFER_BUFFER_SZ 11
+enum {
+ ADS1262_DELAY_NO_DELAY,
+ ADS1262_DELAY_8700_NS,
+ ADS1262_DELAY_17_US,
+ ADS1262_DELAY_35_US,
+ ADS1262_DELAY_69_US,
+ ADS1262_DELAY_139_US,
+ ADS1262_DELAY_278_US,
+ ADS1262_DELAY_555_US,
+ ADS1262_DELAY_1100_US,
+ ADS1262_DELAY_2200_US,
+ ADS1262_DELAY_4400_US,
+ ADS1262_DELAY_8800_US,
+};
+
enum {
ADS1262_RUNMODE_CONTINUOUS,
ADS1262_RUNMODE_PULSE,
@@ -225,6 +240,7 @@ struct ads1262_chip_info {
struct ads1262_channel {
u8 input[2];
+ u8 delay;
u8 filter;
u8 gain;
u8 data_rate;
@@ -281,6 +297,21 @@ static const int ads1262_data_rate_avail[][2] = {
[ADS1262_DR_38400_SPS] = { 38400, 0 },
};
+static const int ads1262_conv_delay_avail[][2] = {
+ [ADS1262_DELAY_NO_DELAY] = { 0, 0 },
+ [ADS1262_DELAY_8700_NS] = { 0, 8700 },
+ [ADS1262_DELAY_17_US] = { 0, 17000 },
+ [ADS1262_DELAY_35_US] = { 0, 35000 },
+ [ADS1262_DELAY_69_US] = { 0, 69000 },
+ [ADS1262_DELAY_139_US] = { 0, 139000 },
+ [ADS1262_DELAY_278_US] = { 0, 278000 },
+ [ADS1262_DELAY_555_US] = { 0, 555000 },
+ [ADS1262_DELAY_1100_US] = { 0, 1100000 },
+ [ADS1262_DELAY_2200_US] = { 0, 2200000 },
+ [ADS1262_DELAY_4400_US] = { 0, 4400000 },
+ [ADS1262_DELAY_8800_US] = { 0, 8800000 },
+};
+
static const int ads1262_pga_gain_avail[] = {
1, 2, 4, 8, 16, 32
};
@@ -473,7 +504,8 @@ static int ads1262_channel_enable(struct ads1262 *st,
/* Avoid using guard() here to mitigate AB/BA deadlock warning */
mutex_lock(&st->chan_lock);
- mode0 = FIELD_PREP(ADS1262_MODE0_INPUT_CHOP_MASK, chan->input_chop) |
+ mode0 = FIELD_PREP(ADS1262_MODE0_DELAY_MASK, chan->delay) |
+ FIELD_PREP(ADS1262_MODE0_INPUT_CHOP_MASK, chan->input_chop) |
FIELD_PREP(ADS1262_MODE0_IDAC_CHOP_MASK, chan->idac_chop) |
FIELD_PREP(ADS1262_MODE0_REFREV_MASK, chan->ref_reversal);
mode1 = FIELD_PREP(ADS1262_MODE1_FILTER_MASK, chan->filter);
@@ -491,6 +523,7 @@ static int ads1262_channel_enable(struct ads1262 *st,
mutex_unlock(&st->chan_lock);
ret = regmap_update_bits(st->regmap, ADS1262_MODE0_REG,
+ ADS1262_MODE0_DELAY_MASK |
ADS1262_MODE0_INPUT_CHOP_MASK |
ADS1262_MODE0_IDAC_CHOP_MASK |
ADS1262_MODE0_REFREV_MASK, mode0);
@@ -625,6 +658,15 @@ static int ads1262_read_raw(struct iio_dev *indio_dev,
return IIO_VAL_INT_PLUS_MICRO;
}
+ case IIO_CHAN_INFO_CONVDELAY: {
+ guard(mutex)(&st->chan_lock);
+
+ *val = ads1262_conv_delay_avail[chan_data->delay][0];
+ *val2 = ads1262_conv_delay_avail[chan_data->delay][1];
+
+ return IIO_VAL_INT_PLUS_NANO;
+ }
+
default:
return -EOPNOTSUPP;
}
@@ -647,6 +689,12 @@ static int ads1262_read_avail(struct iio_dev *indio_dev,
*length = ARRAY_SIZE(ads1262_pga_gain_avail);
return IIO_AVAIL_LIST;
+ case IIO_CHAN_INFO_CONVDELAY:
+ *type = IIO_VAL_INT_PLUS_NANO;
+ *vals = (const int *)ads1262_conv_delay_avail;
+ *length = ARRAY_SIZE(ads1262_conv_delay_avail) * 2;
+ return IIO_AVAIL_LIST;
+
default:
return -EOPNOTSUPP;
}
@@ -685,6 +733,17 @@ static int ads1262_write_raw(struct iio_dev *indio_dev,
break;
}
+ case IIO_CHAN_INFO_CONVDELAY: {
+ i = ads1262_find_two(ads1262_conv_delay_avail, val, val2);
+ if (i < 0)
+ return i;
+
+ guard(mutex)(&st->chan_lock);
+ chan_data->delay = i;
+
+ break;
+ }
+
default:
return -EOPNOTSUPP;
}
@@ -807,8 +866,10 @@ static int ads1262_alloc_channels(struct ads1262 *st,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
BIT(IIO_CHAN_INFO_SCALE) |
BIT(IIO_CHAN_INFO_HARDWAREGAIN) |
+ BIT(IIO_CHAN_INFO_CONVDELAY) |
BIT(IIO_CHAN_INFO_SAMP_FREQ),
.info_mask_shared_by_type_available =
+ BIT(IIO_CHAN_INFO_CONVDELAY) |
BIT(IIO_CHAN_INFO_HARDWAREGAIN) |
BIT(IIO_CHAN_INFO_SAMP_FREQ),
.indexed = true,
--
2.54.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH v2 6/7] iio: adc: ti-ads1262: Add buffer and trigger support
2026-06-28 5:36 [PATCH v2 0/7] iio: adc: Add TI ADS126X ADC family support Kurt Borja
` (4 preceding siblings ...)
2026-06-28 5:36 ` [PATCH v2 5/7] iio: adc: ti-ads1262: Add conversion delay support Kurt Borja
@ 2026-06-28 5:36 ` Kurt Borja
2026-06-28 5:36 ` [PATCH v2 7/7] iio: adc: Add ti-ads1263-adc2 driver Kurt Borja
6 siblings, 0 replies; 14+ messages in thread
From: Kurt Borja @ 2026-06-28 5:36 UTC (permalink / raw)
To: Kurt Borja, Jonathan Cameron, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, David Lechner
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel, Jonathan Cameron
Add triggered buffer support and a data-ready (DRDY) hardware trigger.
Signed-off-by: Kurt Borja <kuurtb@gmail.com>
---
drivers/iio/adc/ti-ads1262.c | 265 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 265 insertions(+)
diff --git a/drivers/iio/adc/ti-ads1262.c b/drivers/iio/adc/ti-ads1262.c
index 4ae22c1b0b4b7d79..53bc70e0c35a59da 100644
--- a/drivers/iio/adc/ti-ads1262.c
+++ b/drivers/iio/adc/ti-ads1262.c
@@ -38,6 +38,9 @@
#include <asm/byteorder.h>
#include <linux/iio/iio.h>
+#include <linux/iio/trigger.h>
+#include <linux/iio/trigger_consumer.h>
+#include <linux/iio/triggered_buffer.h>
#define ADS1262_OPCODE_NOP 0x00
#define ADS1262_OPCODE_RESET 0x06
@@ -258,6 +261,7 @@ struct ads1262 {
const struct ads1262_chip_info *info;
struct regmap *regmap;
struct iio_dev *indio_dev;
+ struct iio_trigger *trig;
struct gpio_desc *reset_gpiod;
struct gpio_desc *start_gpiod;
@@ -273,6 +277,11 @@ struct ads1262 {
/* Protects transfer buffers and concurrent SPI transfers */
struct mutex xfer_lock;
+ struct spi_message msg;
+ struct spi_transfer xfer[2];
+
+ IIO_DECLARE_BUFFER_WITH_TS(__be32, scan_buffer,
+ ADS1262_MAX_CHANNEL_COUNT);
u8 tx[ADS1262_XFER_BUFFER_SZ] __aligned(IIO_DMA_MINALIGN);
u8 rx[ADS1262_XFER_BUFFER_SZ] __aligned(IIO_DMA_MINALIGN);
@@ -781,10 +790,250 @@ static const struct iio_info ads1262_iio_info = {
.debugfs_reg_access = ads1262_debugfs_reg_access,
};
+static int ads1262_buffer_preenable(struct iio_dev *indio_dev)
+{
+ struct ads1262 *st = iio_priv(indio_dev);
+ unsigned int weight;
+ unsigned long i;
+ int ret;
+
+ weight = bitmap_weight(indio_dev->active_scan_mask,
+ iio_get_masklength(indio_dev));
+ if (weight == 1) {
+ /*
+ * A single channel is read by command (RDATA1), so one transfer
+ * holds the command byte plus the 4 conversion bytes, which end
+ * up at offset 1 of the rx buffer.
+ */
+ st->xfer[0].len = 5;
+ st->xfer[0].tx_buf = st->tx;
+ st->xfer[0].rx_buf = st->rx;
+ st->xfer[0].cs_change = 0;
+ spi_message_init_with_transfers(&st->msg, st->xfer, 1);
+
+ i = find_first_bit(indio_dev->active_scan_mask,
+ iio_get_masklength(indio_dev));
+ ret = ads1262_channel_enable(st, &st->channels[i]);
+ if (ret)
+ return ret;
+ } else {
+ /*
+ * Multiple channels use software sequencing: each transfer
+ * rewrites the per-channel configuration registers while
+ * returning the conversion of the previously enabled channel,
+ * found at offset 0 of the rx buffer. The registers are not
+ * contiguous, so the write is split in two bulk steps.
+ *
+ * First step: write protocol (2 bytes) + MODE0, MODE1, MODE2,
+ * INPMUX (4 registers).
+ */
+ st->xfer[0].len = 6;
+ st->xfer[0].tx_buf = st->tx;
+ st->xfer[0].rx_buf = st->rx;
+ st->xfer[0].cs_change = 1;
+ /*
+ * Second step: write protocol (2 bytes) + IDACMUX, IDACMAG,
+ * REFMUX (3 registers).
+ */
+ st->xfer[1].len = 5;
+ st->xfer[1].tx_buf = st->tx + 6;
+ st->xfer[1].rx_buf = st->rx + 6;
+ spi_message_init_with_transfers(&st->msg, st->xfer, 2);
+
+ regcache_drop_region(st->regmap, ADS1262_MODE0_REG,
+ ADS1262_INPMUX_REG);
+ regcache_drop_region(st->regmap, ADS1262_IDACMUX_REG,
+ ADS1262_REFMUX_REG);
+ }
+
+ ret = ads1262_set_runmode(st, ADS1262_RUNMODE_CONTINUOUS);
+ if (ret)
+ return ret;
+
+ ret = spi_optimize_message(st->spi, &st->msg);
+ if (ret)
+ return ret;
+
+ ret = ads1262_dev_start(st);
+ if (ret) {
+ spi_unoptimize_message(&st->msg);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ads1262_buffer_postdisable(struct iio_dev *indio_dev)
+{
+ struct ads1262 *st = iio_priv(indio_dev);
+
+ ads1262_dev_stop(st);
+ spi_unoptimize_message(&st->msg);
+
+ return 0;
+}
+
+static bool ads1262_validate_scan_mask(struct iio_dev *indio_dev,
+ const unsigned long *scan_mask)
+{
+ struct ads1262 *st = iio_priv(indio_dev);
+ struct device *dev = &st->spi->dev;
+
+ if (iio_trigger_using_own(indio_dev)) {
+ dev_err_once(dev, "The %s trigger only supports one active channel\n",
+ st->trig->name);
+ return iio_validate_scan_mask_onehot(indio_dev, scan_mask);
+ }
+
+ return true;
+}
+
+static const struct iio_buffer_setup_ops ads1262_buffer_ops = {
+ .preenable = ads1262_buffer_preenable,
+ .postdisable = ads1262_buffer_postdisable,
+ .validate_scan_mask = ads1262_validate_scan_mask,
+};
+
+static int ads1262_enable_and_read_last(struct ads1262 *st,
+ const struct ads1262_channel *chan,
+ __be32 *val)
+{
+ int ret;
+
+ lockdep_assert_held(&st->xfer_lock);
+
+ if (chan) {
+ guard(mutex)(&st->chan_lock);
+
+ st->tx[0] = ADS1262_MODE0_REG | ADS1262_OPCODE_WREG;
+ st->tx[1] = ADS1262_INPMUX_REG - ADS1262_MODE0_REG;
+ st->tx[2] = FIELD_PREP(ADS1262_MODE0_DELAY_MASK, chan->delay) |
+ FIELD_PREP(ADS1262_MODE0_INPUT_CHOP_MASK, chan->input_chop) |
+ FIELD_PREP(ADS1262_MODE0_IDAC_CHOP_MASK, chan->idac_chop) |
+ FIELD_PREP(ADS1262_MODE0_RUNMODE_MASK, ADS1262_RUNMODE_CONTINUOUS) |
+ FIELD_PREP(ADS1262_MODE0_REFREV_MASK, chan->ref_reversal);
+ st->tx[3] = FIELD_PREP(ADS1262_MODE1_FILTER_MASK, chan->filter);
+ st->tx[4] = FIELD_PREP(ADS1262_MODE2_DR_MASK, chan->data_rate) |
+ FIELD_PREP(ADS1262_MODE2_GAIN_MASK, chan->gain) |
+ FIELD_PREP(ADS1262_MODE2_BYPASS_MASK, chan->pga_bypass);
+ st->tx[5] = FIELD_PREP(ADS1262_INPMUX_MUXP_MASK, chan->input[0]) |
+ FIELD_PREP(ADS1262_INPMUX_MUXN_MASK, chan->input[1]);
+
+ st->tx[6] = ADS1262_IDACMUX_REG | ADS1262_OPCODE_WREG;
+ st->tx[7] = ADS1262_REFMUX_REG - ADS1262_IDACMUX_REG;
+ st->tx[8] = FIELD_PREP(ADS1262_IDACMUX_MUX1_MASK, chan->idac_mux[0]) |
+ FIELD_PREP(ADS1262_IDACMUX_MUX2_MASK, chan->idac_mux[1]);
+ st->tx[9] = FIELD_PREP(ADS1262_IDACMAG_MAG1_MASK, chan->idac_mag[0]) |
+ FIELD_PREP(ADS1262_IDACMAG_MAG2_MASK, chan->idac_mag[1]);
+ st->tx[10] = FIELD_PREP(ADS1262_REFMUX_RMUXP_MASK, chan->reference[0]) |
+ FIELD_PREP(ADS1262_REFMUX_RMUXN_MASK, chan->reference[1]);
+ } else {
+ memset(st->tx, 0, sizeof(st->tx));
+ }
+
+ ret = spi_sync(st->spi, &st->msg);
+ if (ret)
+ return ret;
+
+ memcpy(val, st->rx, sizeof(*val));
+
+ return 0;
+}
+
+static int ads1262_fill_buffer_mult(struct ads1262 *st)
+{
+ unsigned int chan;
+ __be32 val;
+ int i = -1;
+ int ret;
+
+ /*
+ * This routine enables and reads channels in a full-duplex fashion.
+ *
+ * When a channel is enabled, the previous conversion is clocked out of
+ * the shift data register on the same transfer (Section 9.4.7.1). This
+ * allows for low latency software sequencing but forbids any
+ * communication with the chip in-between or data corruption may occur,
+ * hence the need to take the xfer_lock for the whole operation.
+ */
+ guard(mutex)(&st->xfer_lock);
+
+ iio_for_each_active_channel(st->indio_dev, chan) {
+ ret = ads1262_enable_and_read_last(st, &st->channels[chan], &val);
+ if (ret)
+ return ret;
+
+ reinit_completion(&st->drdy);
+
+ if (i > -1)
+ st->scan_buffer[i] = val;
+ i++;
+
+ ads1262_wait_for_conversion(st);
+ }
+
+ return ads1262_enable_and_read_last(st, NULL, &st->scan_buffer[i]);
+}
+
+static int ads1262_fill_buffer_one(struct ads1262 *st)
+{
+ int ret;
+
+ guard(mutex)(&st->xfer_lock);
+
+ /*
+ * When only one channel is enabled, we can't really avoid SPI activity
+ * from happening when the auxiliary ADC is in use, thus we have to read
+ * from the data-holding register (command mode).
+ */
+ st->tx[0] = ADS1262_OPCODE_RDATA1;
+ ret = spi_sync(st->spi, &st->msg);
+ if (ret)
+ return ret;
+
+ /* In command mode the conversion data is found at offset 1 */
+ memcpy(st->scan_buffer, &st->rx[1], sizeof(*st->scan_buffer));
+
+ return 0;
+}
+
+static irqreturn_t ads1262_trigger_handler(int irq, void *p)
+{
+ struct iio_poll_func *pf = p;
+ struct iio_dev *indio_dev = pf->indio_dev;
+ struct ads1262 *st = iio_priv(indio_dev);
+ s64 ts = pf->timestamp;
+ unsigned int weight;
+ int ret;
+
+ weight = bitmap_weight(indio_dev->active_scan_mask,
+ iio_get_masklength(indio_dev));
+
+ memset(st->scan_buffer, 0, sizeof(st->scan_buffer));
+
+ if (weight == 1)
+ ret = ads1262_fill_buffer_one(st);
+ else
+ ret = ads1262_fill_buffer_mult(st);
+ if (ret)
+ goto out_notify_done;
+
+ iio_push_to_buffers_with_ts(indio_dev, st->scan_buffer,
+ sizeof(st->scan_buffer), ts);
+
+out_notify_done:
+ iio_trigger_notify_done(indio_dev->trig);
+
+ return IRQ_HANDLED;
+}
+
static irqreturn_t ads1262_irq_handler(int irq, void *dev_id)
{
struct ads1262 *st = dev_id;
+ if (iio_buffer_enabled(st->indio_dev))
+ iio_trigger_poll(st->trig);
+
complete(&st->drdy);
return IRQ_HANDLED;
@@ -1355,7 +1604,23 @@ static int ads1262_spi_probe(struct spi_device *spi)
indio_dev->channels = channels;
indio_dev->num_channels = num_channels;
+ ret = devm_iio_triggered_buffer_setup(dev, indio_dev,
+ iio_pollfunc_store_time,
+ ads1262_trigger_handler,
+ &ads1262_buffer_ops);
+ if (ret)
+ return ret;
+
if (spi->irq > 0) {
+ st->trig = devm_iio_trigger_alloc(dev, "%s-dev%d-drdy", info->name,
+ iio_device_id(indio_dev));
+ if (!st->trig)
+ return -ENOMEM;
+ iio_trigger_set_drvdata(st->trig, st);
+ ret = devm_iio_trigger_register(dev, st->trig);
+ if (ret)
+ return ret;
+
ret = devm_request_irq(dev, spi->irq, ads1262_irq_handler,
IRQF_NO_THREAD, info->name, st);
if (ret)
--
2.54.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH v2 7/7] iio: adc: Add ti-ads1263-adc2 driver
2026-06-28 5:36 [PATCH v2 0/7] iio: adc: Add TI ADS126X ADC family support Kurt Borja
` (5 preceding siblings ...)
2026-06-28 5:36 ` [PATCH v2 6/7] iio: adc: ti-ads1262: Add buffer and trigger support Kurt Borja
@ 2026-06-28 5:36 ` Kurt Borja
2026-06-28 17:22 ` David Lechner
6 siblings, 1 reply; 14+ messages in thread
From: Kurt Borja @ 2026-06-28 5:36 UTC (permalink / raw)
To: Kurt Borja, Jonathan Cameron, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, David Lechner
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel, Jonathan Cameron
The TI ADS1263 embeds a second 24-bit delta-sigma ADC (ADC2) with its
own input mux, reference, gain and sample-rate selection.
Model ADC2 as a separate IIO device on the auxiliary bus: the ti-ads1262
SPI driver instantiates the auxiliary device and exports a small set of
TI_ADS1262-namespaced helpers for the conversion and register accesses
that must go through the shared bus. ADC2 channels are derived from the
parent's configured channels.
Signed-off-by: Kurt Borja <kuurtb@gmail.com>
---
MAINTAINERS | 2 +
drivers/iio/adc/Kconfig | 14 ++
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ti-ads1262.c | 168 ++++++++++++++++-
drivers/iio/adc/ti-ads1262.h | 39 ++++
drivers/iio/adc/ti-ads1263-adc2.c | 379 ++++++++++++++++++++++++++++++++++++++
6 files changed, 602 insertions(+), 1 deletion(-)
diff --git a/MAINTAINERS b/MAINTAINERS
index d868b25f2c65bcd9..342c661f079bcf39 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -26929,6 +26929,8 @@ L: linux-iio@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/iio/adc/ti,ads1262.yaml
F: drivers/iio/adc/ti-ads1262.c
+F: drivers/iio/adc/ti-ads1262.h
+F: drivers/iio/adc/ti-ads1263-adc2.c
TI ADS7924 ADC DRIVER
M: Hugo Villeneuve <hvilleneuve@dimonoff.com>
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 6051092c20b96731..ab2e8e45f3b442d6 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -1817,6 +1817,7 @@ config TI_ADS1262
select REGMAP
select IIO_BUFFER
select IIO_TRIGGERED_BUFFER
+ select AUXILIARY_BUS
help
If you say yes here you get support for Texas Instruments ADS1262 and
ADS1263 ADC chips.
@@ -1824,6 +1825,19 @@ config TI_ADS1262
This driver can also be built as a module. If so, the module will be
called ti-ads1262.
+config TI_ADS1263_ADC2
+ tristate "Texas Instruments ADS1263 auxiliary ADC (ADC2) driver"
+ depends on TI_ADS1262
+ select AUXILIARY_BUS
+ select IIO_BUFFER
+ select IIO_TRIGGERED_BUFFER
+ help
+ If you say yes here you get support for Texas Instruments ADS1263
+ auxiliary ADC (ADC2).
+
+ This driver can also be built as a module. If so, the module will be
+ called ti-ads1263-adc2.
+
config TI_ADS1298
tristate "Texas Instruments ADS1298"
depends on SPI
diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile
index 4b1f89a2317a35f7..4215f56f525349a5 100644
--- a/drivers/iio/adc/Makefile
+++ b/drivers/iio/adc/Makefile
@@ -156,6 +156,7 @@ obj-$(CONFIG_TI_ADS1119) += ti-ads1119.o
obj-$(CONFIG_TI_ADS112C14) += ti-ads112c14.o
obj-$(CONFIG_TI_ADS124S08) += ti-ads124s08.o
obj-$(CONFIG_TI_ADS1262) += ti-ads1262.o
+obj-$(CONFIG_TI_ADS1263_ADC2) += ti-ads1263-adc2.o
obj-$(CONFIG_TI_ADS1298) += ti-ads1298.o
obj-$(CONFIG_TI_ADS131E08) += ti-ads131e08.o
obj-$(CONFIG_TI_ADS131M02) += ti-ads131m02.o
diff --git a/drivers/iio/adc/ti-ads1262.c b/drivers/iio/adc/ti-ads1262.c
index 53bc70e0c35a59da..e3acc2eb9042c40a 100644
--- a/drivers/iio/adc/ti-ads1262.c
+++ b/drivers/iio/adc/ti-ads1262.c
@@ -14,10 +14,10 @@
#include <linux/clk.h>
#include <linux/completion.h>
#include <linux/compiler_attributes.h>
-#include <linux/compiler_types.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/gpio/consumer.h>
+#include <linux/idr.h>
#include <linux/interrupt.h>
#include <linux/lockdep.h>
#include <linux/math.h>
@@ -25,6 +25,7 @@
#include <linux/module.h>
#include <linux/mod_devicetable.h>
#include <linux/mutex.h>
+#include <linux/of.h>
#include <linux/overflow.h>
#include <linux/property.h>
#include <linux/regmap.h>
@@ -42,6 +43,8 @@
#include <linux/iio/trigger_consumer.h>
#include <linux/iio/triggered_buffer.h>
+#include "ti-ads1262.h"
+
#define ADS1262_OPCODE_NOP 0x00
#define ADS1262_OPCODE_RESET 0x06
#define ADS1262_OPCODE_START1 0x08
@@ -144,6 +147,8 @@
#define ADS1262_MAX_CHANNEL_COUNT 16
#define ADS1262_XFER_BUFFER_SZ 11
+static DEFINE_IDA(ads1262_ida);
+
enum {
ADS1262_DELAY_NO_DELAY,
ADS1262_DELAY_8700_NS,
@@ -1039,6 +1044,161 @@ static irqreturn_t ads1262_irq_handler(int irq, void *dev_id)
return IRQ_HANDLED;
}
+int ads1263_adc2_channel_get_scale(struct ads1263_adc2_ctx *ctx, u8 realbits,
+ u8 gain, u8 ref_source, int *val, int *val2)
+{
+ return ads1262_calculate_scale(ctx->chip, realbits, gain, ref_source, ref_source,
+ val, val2);
+}
+EXPORT_SYMBOL_NS_GPL(ads1263_adc2_channel_get_scale, "TI_ADS1262");
+
+int ads1263_adc2_channel_enable(struct ads1263_adc2_ctx *ctx,
+ const struct ads1263_adc2_channel *chan)
+{
+ struct ads1262 *st = ctx->chip;
+ u8 val;
+ int ret;
+
+ guard(mutex)(&ctx->chan_lock);
+
+ val = FIELD_PREP(ADS1262_ADC2CFG_GAIN2_MASK, chan->gain) |
+ FIELD_PREP(ADS1262_ADC2CFG_REF2_MASK, chan->reference) |
+ FIELD_PREP(ADS1262_ADC2CFG_DR2_MASK, chan->data_rate);
+ ret = regmap_update_bits(st->regmap, ADS1262_ADC2CFG_REG,
+ ADS1262_ADC2CFG_GAIN2_MASK |
+ ADS1262_ADC2CFG_REF2_MASK |
+ ADS1262_ADC2CFG_DR2_MASK, val);
+
+ val = FIELD_PREP(ADS1262_ADC2MUX_MUXP2_MASK, chan->input[0]) |
+ FIELD_PREP(ADS1262_ADC2MUX_MUXN2_MASK, chan->input[1]);
+ return regmap_update_bits(st->regmap, ADS1262_ADC2MUX_REG,
+ ADS1262_ADC2MUX_MUXP2_MASK |
+ ADS1262_ADC2MUX_MUXN2_MASK, val);
+}
+EXPORT_SYMBOL_NS_GPL(ads1263_adc2_channel_enable, "TI_ADS1262");
+
+int ads1263_adc2_start(struct ads1263_adc2_ctx *ctx)
+{
+ struct ads1262 *st = ctx->chip;
+
+ return ads1262_dev_cmd(st, ADS1262_OPCODE_START2);
+}
+EXPORT_SYMBOL_NS_GPL(ads1263_adc2_start, "TI_ADS1262");
+
+int ads1263_adc2_stop(struct ads1263_adc2_ctx *ctx)
+{
+ struct ads1262 *st = ctx->chip;
+
+ return ads1262_dev_cmd(st, ADS1262_OPCODE_STOP2);
+}
+EXPORT_SYMBOL_NS_GPL(ads1263_adc2_stop, "TI_ADS1262");
+
+int ads1263_adc2_read(struct ads1263_adc2_ctx *ctx, __be32 *val)
+{
+ struct ads1262 *st = ctx->chip;
+
+ return ads1262_dev_read_by_cmd(st, ADS1262_OPCODE_RDATA2, val);
+}
+EXPORT_SYMBOL_NS_GPL(ads1263_adc2_read, "TI_ADS1262");
+
+static void ads1262_aux_device_destroy(void *data)
+{
+ struct auxiliary_device *adev = data;
+
+ auxiliary_device_delete(adev);
+ auxiliary_device_uninit(adev);
+}
+
+static void ads1262_aux_device_release(struct device *dev)
+{
+ struct auxiliary_device *adev = to_auxiliary_dev(dev);
+ struct ads1263_adc2_ctx *ctx =
+ container_of(adev, struct ads1263_adc2_ctx, adev);
+ struct device_node *node = adev->dev.of_node;
+
+ of_node_put(node);
+ mutex_destroy(&ctx->chan_lock);
+ kfree(ctx->channels);
+ ida_free(&ads1262_ida, adev->id);
+ kfree(ctx);
+}
+
+static int ads1262_aux_device_setup(struct ads1262 *st)
+{
+ struct device *parent = &st->spi->dev;
+ struct ads1263_adc2_channel *chans;
+ struct ads1262_channel *chan_data;
+ struct auxiliary_device *adev;
+ struct ads1263_adc2_ctx *ctx;
+ struct device_link *link;
+ int id, ret;
+
+ ctx = kzalloc_obj(*ctx);
+ if (!ctx)
+ return -ENOMEM;
+
+ id = ida_alloc(&ads1262_ida, GFP_KERNEL);
+ if (id < 0) {
+ ret = id;
+ goto out_free_adc2;
+ }
+
+ chans = kcalloc(st->num_channels, sizeof(*chans), GFP_KERNEL);
+ if (!chans) {
+ ret = -ENOMEM;
+ goto out_free_id;
+ }
+
+ for (unsigned int i = 0; i < st->num_channels; i++) {
+ chan_data = &st->channels[i];
+ chans[i].input[0] = chan_data->input[0];
+ chans[i].input[1] = chan_data->input[1];
+ chans[i].reference = chan_data->reference[2];
+ }
+
+ ctx->chip = st;
+ ctx->num_channels = st->num_channels;
+ ctx->channels = chans;
+ mutex_init(&ctx->chan_lock);
+
+ adev = &ctx->adev;
+ adev->name = "ads1263_adc2";
+ adev->id = id;
+ adev->dev.release = ads1262_aux_device_release;
+ adev->dev.parent = parent;
+ device_set_of_node_from_dev(&adev->dev, parent);
+
+ ret = auxiliary_device_init(adev);
+ if (ret)
+ goto out_free_res;
+
+ link = device_link_add(&adev->dev, parent, DL_FLAG_AUTOPROBE_CONSUMER);
+ if (!link) {
+ auxiliary_device_uninit(adev);
+ return dev_err_probe(parent, -ENXIO,
+ "Failed to add link to auxiliary device\n");
+ }
+
+ ret = auxiliary_device_add(adev);
+ if (ret) {
+ auxiliary_device_uninit(adev);
+ return ret;
+ }
+
+ return devm_add_action_or_reset(parent, ads1262_aux_device_destroy, adev);
+
+out_free_res:
+ of_node_put(adev->dev.of_node);
+ mutex_destroy(&ctx->chan_lock);
+ kfree(chans);
+out_free_id:
+ ida_free(&ads1262_ida, id);
+out_free_adc2:
+ kfree(ctx);
+
+ return ret;
+}
+
static int ads1262_get_filter_type(struct iio_dev *indio_dev,
const struct iio_chan_spec *chan)
{
@@ -1627,6 +1787,12 @@ static int ads1262_spi_probe(struct spi_device *spi)
return ret;
}
+ if (info->has_aux_adc) {
+ ret = ads1262_aux_device_setup(st);
+ if (ret)
+ return ret;
+ }
+
return devm_iio_device_register(dev, indio_dev);
}
diff --git a/drivers/iio/adc/ti-ads1262.h b/drivers/iio/adc/ti-ads1262.h
new file mode 100644
index 0000000000000000..7a94ffd1fa983f9f
--- /dev/null
+++ b/drivers/iio/adc/ti-ads1262.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Texas Instruments ADS1262 ADC driver
+ *
+ * Copyright (C) 2025 Kurt Borja <kuurtb@gmail.com>
+ */
+
+#ifndef _ADS1262_H_
+#define _ADS1262_H_
+
+#include <linux/auxiliary_bus.h>
+#include <linux/types.h>
+
+struct ads1263_adc2_channel {
+ u8 gain;
+ u8 reference;
+ u8 data_rate;
+ u8 input[2];
+};
+
+struct ads1263_adc2_ctx {
+ struct auxiliary_device adev;
+ struct ads1262 *chip;
+
+ /* Protects channel state */
+ struct mutex chan_lock;
+ struct ads1263_adc2_channel *channels;
+ unsigned int num_channels;
+};
+
+int ads1263_adc2_channel_get_scale(struct ads1263_adc2_ctx *ctx, u8 realbits,
+ u8 gain, u8 ref_source, int *val, int *val2);
+int ads1263_adc2_channel_enable(struct ads1263_adc2_ctx *ctx,
+ const struct ads1263_adc2_channel *chan);
+int ads1263_adc2_start(struct ads1263_adc2_ctx *ctx);
+int ads1263_adc2_stop(struct ads1263_adc2_ctx *ctx);
+int ads1263_adc2_read(struct ads1263_adc2_ctx *ctx, __be32 *val);
+
+#endif
diff --git a/drivers/iio/adc/ti-ads1263-adc2.c b/drivers/iio/adc/ti-ads1263-adc2.c
new file mode 100644
index 0000000000000000..385531d96de11269
--- /dev/null
+++ b/drivers/iio/adc/ti-ads1263-adc2.c
@@ -0,0 +1,379 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Texas Instruments ADS1263 auxiliary ADC (ADC2) driver
+ *
+ * Copyright (C) 2025 Kurt Borja <kuurtb@gmail.com>
+ */
+
+#include <linux/align.h>
+#include <linux/array_size.h>
+#include <linux/bitmap.h>
+#include <linux/bitops.h>
+#include <linux/cleanup.h>
+#include <linux/container_of.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/dev_printk.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/property.h>
+#include <linux/regulator/consumer.h>
+#include <linux/unaligned.h>
+#include <linux/units.h>
+
+#include <linux/iio/iio.h>
+#include <linux/iio/trigger_consumer.h>
+#include <linux/iio/triggered_buffer.h>
+
+#include "ti-ads1262.h"
+
+/* ADC2CFG REF2 constants */
+#define ADS1263_ADC2_REF2_INTER 0
+#define ADS1263_ADC2_REF2_COUNT 5
+
+struct ads1263_adc2 {
+ struct iio_dev *indio_dev;
+ struct ads1263_adc2_ctx *ctx;
+};
+
+static const int ads1263_adc2_gain_avail[] = {
+ 1, 2, 4, 8, 16, 32, 64, 128
+};
+
+static const int ads1263_adc2_data_rate_avail[] = {
+ 10, 100, 400, 800
+};
+
+static const unsigned long ads1263_adc2_latency_us[] = {
+ 121000, 31200, 8710, 4970
+};
+
+static int ads1263_adc2_channel_read(struct iio_dev *indio_dev,
+ struct ads1263_adc2_channel *chan_data,
+ __be32 *val)
+{
+ struct ads1263_adc2 *st = iio_priv(indio_dev);
+ struct ads1263_adc2_ctx *ctx = st->ctx;
+ int ret;
+
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ ret = ads1263_adc2_channel_enable(ctx, chan_data);
+ if (ret)
+ return ret;
+
+ ret = ads1263_adc2_start(ctx);
+ if (ret)
+ return ret;
+
+ ret = ads1263_adc2_stop(ctx);
+ if (ret)
+ return ret;
+
+ fsleep(ads1263_adc2_latency_us[chan_data->data_rate]);
+
+ return ads1263_adc2_read(ctx, val);
+}
+
+static int ads1263_adc2_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int *val, int *val2, long mask)
+{
+ struct ads1263_adc2 *st = iio_priv(indio_dev);
+ struct ads1263_adc2_ctx *ctx = st->ctx;
+ struct ads1263_adc2_channel *chan_data = &ctx->channels[chan->scan_index];
+ u8 realbits = chan->scan_type.realbits;
+ __be32 raw;
+ int ret;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_RAW:
+ ret = ads1263_adc2_channel_read(indio_dev, chan_data, &raw);
+ if (ret)
+ return ret;
+
+ *val = sign_extend32(get_unaligned_be24(&raw), realbits - 1);
+
+ return IIO_VAL_INT;
+
+ case IIO_CHAN_INFO_SCALE: {
+ guard(mutex)(&ctx->chan_lock);
+
+ ret = ads1263_adc2_channel_get_scale(ctx, realbits, chan_data->gain,
+ chan_data->reference, val, val2);
+ if (ret)
+ return ret;
+
+ return IIO_VAL_INT_PLUS_NANO;
+ }
+
+ case IIO_CHAN_INFO_HARDWAREGAIN: {
+ guard(mutex)(&ctx->chan_lock);
+
+ *val = ads1263_adc2_gain_avail[chan_data->gain];
+
+ return IIO_VAL_INT;
+ }
+
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ guard(mutex)(&ctx->chan_lock);
+
+ *val = ads1263_adc2_data_rate_avail[chan_data->data_rate];
+
+ return IIO_VAL_INT;
+ }
+
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ads1263_adc2_read_avail(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ const int **vals, int *type,
+ int *length, long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_HARDWAREGAIN:
+ *type = IIO_VAL_INT;
+ *vals = ads1263_adc2_gain_avail;
+ *length = ARRAY_SIZE(ads1263_adc2_gain_avail);
+ return IIO_AVAIL_LIST;
+
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ *type = IIO_VAL_INT;
+ *vals = ads1263_adc2_data_rate_avail;
+ *length = ARRAY_SIZE(ads1263_adc2_data_rate_avail);
+ return IIO_AVAIL_LIST;
+
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ads1263_adc2_write_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int val, int val2, long mask)
+{
+ struct ads1263_adc2 *st = iio_priv(indio_dev);
+ struct ads1263_adc2_ctx *ctx = st->ctx;
+ struct ads1263_adc2_channel *chan_data = &ctx->channels[chan->scan_index];
+ unsigned int i;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_HARDWAREGAIN: {
+ for (i = 0; i < ARRAY_SIZE(ads1263_adc2_gain_avail); i++) {
+ if (val == ads1263_adc2_gain_avail[i])
+ break;
+ }
+ if (i == ARRAY_SIZE(ads1263_adc2_gain_avail))
+ return -EINVAL;
+
+ guard(mutex)(&ctx->chan_lock);
+ chan_data->gain = i;
+
+ break;
+ }
+
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ for (i = 0; i < ARRAY_SIZE(ads1263_adc2_data_rate_avail); i++) {
+ if (val == ads1263_adc2_data_rate_avail[i])
+ break;
+ }
+ if (i == ARRAY_SIZE(ads1263_adc2_data_rate_avail))
+ return -EINVAL;
+
+ guard(mutex)(&ctx->chan_lock);
+ chan_data->data_rate = i;
+
+ break;
+ }
+
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ return 0;
+}
+
+static int ads1263_adc2_write_raw_get_fmt(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_CONVDELAY:
+ return IIO_VAL_INT_PLUS_NANO;
+ default:
+ return IIO_VAL_INT_PLUS_MICRO;
+ }
+}
+
+static const struct iio_info ads1263_adc2_iio_info = {
+ .read_raw = ads1263_adc2_read_raw,
+ .read_avail = ads1263_adc2_read_avail,
+ .write_raw = ads1263_adc2_write_raw,
+ .write_raw_get_fmt = ads1263_adc2_write_raw_get_fmt,
+};
+
+static int ads1263_adc2_buffer_preenable(struct iio_dev *indio_dev)
+{
+ struct ads1263_adc2 *st = iio_priv(indio_dev);
+ struct ads1263_adc2_ctx *ctx = st->ctx;
+ unsigned long i;
+ int ret;
+
+ i = find_first_bit(indio_dev->active_scan_mask,
+ iio_get_masklength(indio_dev));
+ ret = ads1263_adc2_channel_enable(ctx, &ctx->channels[i]);
+ if (ret)
+ return ret;
+
+ return ads1263_adc2_start(ctx);
+}
+
+static int ads1263_adc2_buffer_postdisable(struct iio_dev *indio_dev)
+{
+ struct ads1263_adc2 *st = iio_priv(indio_dev);
+ struct ads1263_adc2_ctx *ctx = st->ctx;
+
+ ads1263_adc2_stop(ctx);
+
+ return 0;
+}
+
+static const struct iio_buffer_setup_ops ads1263_adc2_buffer_ops = {
+ .preenable = ads1263_adc2_buffer_preenable,
+ .postdisable = ads1263_adc2_buffer_postdisable,
+ .validate_scan_mask = iio_validate_scan_mask_onehot,
+};
+
+static irqreturn_t ads1263_adc2_trigger_handler(int irq, void *p)
+{
+ struct iio_poll_func *pf = p;
+ struct iio_dev *indio_dev = pf->indio_dev;
+ struct ads1263_adc2 *st = iio_priv(indio_dev);
+ struct ads1263_adc2_ctx *ctx = st->ctx;
+ struct {
+ __be32 conv;
+ aligned_s64 ts;
+ } scan = {};
+ int ret;
+
+ ret = ads1263_adc2_read(ctx, &scan.conv);
+ if (ret)
+ goto out_notify_done;
+
+ iio_push_to_buffers_with_ts(indio_dev, &scan, sizeof(scan),
+ pf->timestamp);
+
+out_notify_done:
+ iio_trigger_notify_done(indio_dev->trig);
+
+ return IRQ_HANDLED;
+}
+
+static int ads1263_adc2_channels_setup(struct iio_dev *indio_dev)
+{
+ struct ads1263_adc2 *st = iio_priv(indio_dev);
+ struct device *dev = &st->ctx->adev.dev;
+ struct ads1263_adc2_ctx *ctx = st->ctx;
+ struct ads1263_adc2_channel *chan_data;
+ struct iio_chan_spec *chns;
+ unsigned int i;
+
+ /* Account for the timestamp channel */
+ chns = devm_kcalloc(dev, ctx->num_channels + 1, sizeof(*chns),
+ GFP_KERNEL);
+ if (!chns)
+ return -ENOMEM;
+
+ for (i = 0; i < ctx->num_channels; i++) {
+ guard(mutex)(&ctx->chan_lock);
+
+ chan_data = &ctx->channels[i];
+ chns[i] = (struct iio_chan_spec) {
+ .type = IIO_VOLTAGE,
+ .channel = chan_data->input[0],
+ .channel2 = chan_data->input[1],
+ .scan_index = i,
+ .scan_type = {
+ .format = IIO_SCAN_FORMAT_SIGNED_INT,
+ .realbits = 24,
+ .storagebits = 32,
+ .shift = 8,
+ .endianness = IIO_BE,
+ },
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE) |
+ BIT(IIO_CHAN_INFO_HARDWAREGAIN) |
+ BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ .info_mask_shared_by_type_available =
+ BIT(IIO_CHAN_INFO_HARDWAREGAIN) |
+ BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ .indexed = true,
+ .differential = true,
+ };
+ }
+
+ chns[i] = IIO_CHAN_SOFT_TIMESTAMP(i);
+
+ indio_dev->num_channels = ctx->num_channels + 1;
+ indio_dev->channels = chns;
+
+ return 0;
+}
+
+static int ads1263_adc2_probe(struct auxiliary_device *auxdev,
+ const struct auxiliary_device_id *id)
+{
+ struct ads1263_adc2_ctx *ctx =
+ container_of(auxdev, struct ads1263_adc2_ctx, adev);
+ struct device *dev = &auxdev->dev;
+ struct iio_dev *indio_dev;
+ struct ads1263_adc2 *st;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ st = iio_priv(indio_dev);
+ st->ctx = ctx;
+ st->indio_dev = indio_dev;
+
+ indio_dev->name = "ads1263_adc2";
+ indio_dev->modes = INDIO_DIRECT_MODE;
+ indio_dev->info = &ads1263_adc2_iio_info;
+ ret = ads1263_adc2_channels_setup(indio_dev);
+ if (ret)
+ return ret;
+
+ ret = devm_iio_triggered_buffer_setup(dev, indio_dev,
+ iio_pollfunc_store_time,
+ ads1263_adc2_trigger_handler,
+ &ads1263_adc2_buffer_ops);
+ if (ret)
+ return ret;
+
+ return devm_iio_device_register(dev, indio_dev);
+}
+
+static const struct auxiliary_device_id ads1263_adc2_auxiliary_match[] = {
+ { .name = "ti_ads1262.ads1263_adc2" },
+ { }
+};
+MODULE_DEVICE_TABLE(auxiliary, ads1263_adc2_auxiliary_match);
+
+static struct auxiliary_driver ads1263_adc2_driver = {
+ .name = "ads1263_adc2",
+ .probe = ads1263_adc2_probe,
+ .id_table = ads1263_adc2_auxiliary_match,
+};
+module_auxiliary_driver(ads1263_adc2_driver);
+
+MODULE_IMPORT_NS("TI_ADS1262");
+MODULE_DESCRIPTION("Texas Instruments ADS1263 auxiliary ADC (ADC2) driver");
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Kurt Borja <kuurtb@gmail.com>");
--
2.54.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* Re: [PATCH v2 7/7] iio: adc: Add ti-ads1263-adc2 driver
2026-06-28 5:36 ` [PATCH v2 7/7] iio: adc: Add ti-ads1263-adc2 driver Kurt Borja
@ 2026-06-28 17:22 ` David Lechner
2026-06-28 20:08 ` Kurt Borja
0 siblings, 1 reply; 14+ messages in thread
From: David Lechner @ 2026-06-28 17:22 UTC (permalink / raw)
To: Kurt Borja, Jonathan Cameron, Rob Herring, Krzysztof Kozlowski,
Conor Dooley
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel
On 6/28/26 12:36 AM, Kurt Borja wrote:
> The TI ADS1263 embeds a second 24-bit delta-sigma ADC (ADC2) with its
> own input mux, reference, gain and sample-rate selection.
>
> Model ADC2 as a separate IIO device on the auxiliary bus: the ti-ads1262
> SPI driver instantiates the auxiliary device and exports a small set of
> TI_ADS1262-namespaced helpers for the conversion and register accesses
> that must go through the shared bus. ADC2 channels are derived from the
> parent's configured channels.
>
Can these just be additional channels in the main iio device rather
than a separate iio device?
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2 7/7] iio: adc: Add ti-ads1263-adc2 driver
2026-06-28 17:22 ` David Lechner
@ 2026-06-28 20:08 ` Kurt Borja
0 siblings, 0 replies; 14+ messages in thread
From: Kurt Borja @ 2026-06-28 20:08 UTC (permalink / raw)
To: David Lechner, Kurt Borja, Jonathan Cameron, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: Nuno Sá, Andy Shevchenko, linux-iio, devicetree,
linux-kernel
On Sun Jun 28, 2026 at 12:22 PM -05, David Lechner wrote:
> On 6/28/26 12:36 AM, Kurt Borja wrote:
>> The TI ADS1263 embeds a second 24-bit delta-sigma ADC (ADC2) with its
>> own input mux, reference, gain and sample-rate selection.
>>
>> Model ADC2 as a separate IIO device on the auxiliary bus: the ti-ads1262
>> SPI driver instantiates the auxiliary device and exports a small set of
>> TI_ADS1262-namespaced helpers for the conversion and register accesses
>> that must go through the shared bus. ADC2 channels are derived from the
>> parent's configured channels.
>>
> Can these just be additional channels in the main iio device rather
> than a separate iio device?
I guess we can do it, but wouldn't it be quite a mess? I think doing it
that way adds a lot of complexity: channel naming, available scan masks
(because both ADCs can be sampled at the same time), optimized software
sequencing would only work in ADC1 channels, ADC2 doesn't have a DRDY
IRQ, etc.
IMO separating both drivers makes everything simpler, easier to
understand and easier to maintain in the future.
--
Thanks,
~ Kurt
^ permalink raw reply [flat|nested] 14+ messages in thread