* [PATCH v2 0/4] iio: adc: Add support for LTC2378 and similar ADCs
@ 2026-05-28 15:02 Marcelo Schmitt
2026-05-28 15:03 ` [PATCH v2 1/4] dt-bindings: iio: adc: Add ltc2378 Marcelo Schmitt
` (3 more replies)
0 siblings, 4 replies; 9+ messages in thread
From: Marcelo Schmitt @ 2026-05-28 15:02 UTC (permalink / raw)
To: linux-iio, devicetree, linux-kernel
Cc: jic23, nuno.sa, Michael.Hennerich, dlechner, andy, robh, krzk+dt,
conor+dt, pop.ioan-daniel, marcelo.schmitt1
This patch series adds support for LTC2378 and similar low noise, low power,
high speed, successive approximation register (SAR) ADCs. These ADCs are similar
among each other, varying mainly on the amount of precision bits, maximum sample
rate, and input configuration (either fully differential or pseudo-differential).
The initial support patch enables single-shot sample read with a GPIO connected
to the CNV pin.
The second support patch enables high-speed data captures with SPI offloading.
The setup is similar to AD4030, with a specialized PWM generator being used both
for SPI offload triggering and conversion start signaling.
The last support patch enables running buffered data captures without SPI offloading.
Some structures and variables are introduced earlier to reduce diff in latter patches.
The initial version of the LTC2378 driver was developed by Ioan-Daniel. Though,
despite of the many changes I made to the code, I've kept him as module author
to provide credit for his work.
Link to v1: https://lore.kernel.org/linux-iio/cover.1779117444.git.marcelo.schmitt1@gmail.com/
Change log v1 -> v2:
[DT]
- Use unevaluatedProperties:false with spi-peripheral-props.yaml reference/include.
- Use unique compatibles (no fallbacks).
[IIO]
- Added missing includes for LTC2378 driver.
- Added waiting time required before reading back LTC2378 conversion data.
- Fixed voltage regulator read error path.
- Properly right-aligned sample data.
- Set loop count to prevent CPU stall when calculating PWM parameters.
- Hardcode set specific offload supported amount of data element bits.
- Fixed devm_spi_offload_get() error path.
- Dropped device ID table and simplified chip specific information keeping.
- Using named device_id data initializers.
- Use bool for output code type distinction.
- Initialize init scan_type according to buffer selection.
- Added scope for IIO_DEV_ACQUIRE_DIRECT_MODE usage.
- Offload attributes now are channel based instead of device based.
- Use IIO_CHAN_SOFT_TIMESTAMP directly on right hand of assignment.
- Wrapped comments close to 80 columns.
- Organized includes.
Marcelo Schmitt (4):
dt-bindings: iio: adc: Add ltc2378
iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs
iio: adc: ltc2378: Enable high-speed data capture
iio: adc: ltc2378: Enable triggered buffer data capture
.../bindings/iio/adc/adi,ltc2378.yaml | 118 +++++
MAINTAINERS | 8 +
drivers/iio/adc/Kconfig | 22 +
drivers/iio/adc/Makefile | 2 +
drivers/iio/adc/ltc2378-offload-buffer.c | 296 ++++++++++++
drivers/iio/adc/ltc2378.c | 454 ++++++++++++++++++
drivers/iio/adc/ltc2378.h | 113 +++++
7 files changed, 1013 insertions(+)
create mode 100644 Documentation/devicetree/bindings/iio/adc/adi,ltc2378.yaml
create mode 100644 drivers/iio/adc/ltc2378-offload-buffer.c
create mode 100644 drivers/iio/adc/ltc2378.c
create mode 100644 drivers/iio/adc/ltc2378.h
base-commit: 6f15decff24dac42895c0f01f0c2143f9b8961a6
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2 1/4] dt-bindings: iio: adc: Add ltc2378
2026-05-28 15:02 [PATCH v2 0/4] iio: adc: Add support for LTC2378 and similar ADCs Marcelo Schmitt
@ 2026-05-28 15:03 ` Marcelo Schmitt
2026-05-28 16:55 ` Conor Dooley
2026-05-28 15:03 ` [PATCH v2 2/4] iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs Marcelo Schmitt
` (2 subsequent siblings)
3 siblings, 1 reply; 9+ messages in thread
From: Marcelo Schmitt @ 2026-05-28 15:03 UTC (permalink / raw)
To: linux-iio, devicetree, linux-kernel
Cc: jic23, nuno.sa, Michael.Hennerich, dlechner, andy, robh, krzk+dt,
conor+dt, pop.ioan-daniel, marcelo.schmitt1
Document how to describe LTC2378-20 and similar ADCs in device tree.
Signed-off-by: Marcelo Schmitt <marcelo.schmitt@analog.com>
---
Change log v1 -> v2:
- Use unevaluatedProperties:false with spi-peripheral-props.yaml reference/include.
- Use unique compatibles (no fallbacks).
.../bindings/iio/adc/adi,ltc2378.yaml | 118 ++++++++++++++++++
MAINTAINERS | 7 ++
2 files changed, 125 insertions(+)
create mode 100644 Documentation/devicetree/bindings/iio/adc/adi,ltc2378.yaml
diff --git a/Documentation/devicetree/bindings/iio/adc/adi,ltc2378.yaml b/Documentation/devicetree/bindings/iio/adc/adi,ltc2378.yaml
new file mode 100644
index 000000000000..fdc004049521
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/adc/adi,ltc2378.yaml
@@ -0,0 +1,118 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/iio/adc/adi,ltc2378.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Analog Devices LTC2378 and similar Analog to Digital Converters
+
+maintainers:
+ - Marcelo Schmitt <marcelo.schmitt@analog.com>
+
+description: |
+ Analog Devices LTC2378 series of ADCs.
+ Specifications can be found at:
+ https://www.analog.com/media/en/technical-documentation/data-sheets/233818fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/236416fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/236418f.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/236716fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/236718f.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/236816f.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/236818f.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/236918fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237016fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237616fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237618fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237620fb.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237716fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237718fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237720fb.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237816fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237818fa.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237820fb.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/237918fb.pdf
+ https://www.analog.com/media/en/technical-documentation/data-sheets/238016fb.pdf
+
+$ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+ compatible:
+ enum:
+ - adi,ltc2338-18
+ - adi,ltc2364-16
+ - adi,ltc2364-18
+ - adi,ltc2367-16
+ - adi,ltc2367-18
+ - adi,ltc2368-16
+ - adi,ltc2368-18
+ - adi,ltc2369-18
+ - adi,ltc2370-16
+ - adi,ltc2376-16
+ - adi,ltc2376-18
+ - adi,ltc2376-20
+ - adi,ltc2377-16
+ - adi,ltc2377-18
+ - adi,ltc2377-20
+ - adi,ltc2378-16
+ - adi,ltc2378-18
+ - adi,ltc2378-20
+ - adi,ltc2379-18
+ - adi,ltc2380-16
+
+ reg:
+ maxItems: 1
+
+ spi-max-frequency:
+ maximum: 100000000
+
+ vdd-supply:
+ description: A 2.5V supply that powers the chip (VDD).
+
+ ovdd-supply:
+ description:
+ A 1.71V to 5.25V supply that sets the logic level for digital interface.
+
+ ref-supply:
+ description:
+ A 2.5V to 5.1V supply for the reference input (REF).
+
+ cnv-gpios:
+ description:
+ When provided, this property indicates the GPIO that is connected to the
+ CNV pin.
+ maxItems: 1
+
+ interrupts:
+ description:
+ Interrupt for signaling the completion of conversion results. The active
+ low signal provided on the BUSY pin asserts when ADC conversions finish.
+ maxItems: 1
+
+required:
+ - compatible
+ - reg
+ - vdd-supply
+ - ovdd-supply
+ - ref-supply
+
+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 = "adi,ltc2378-20";
+ reg = <0>;
+ spi-max-frequency = <71000000>;
+ vdd-supply = <&supply_2_5V>;
+ ovdd-supply = <&supply_3_3V>;
+ ref-supply = <&supply_5V>;
+ cnv-gpios = <&gpio0 88 GPIO_ACTIVE_HIGH>;
+ interrupts = <7 IRQ_TYPE_EDGE_FALLING>;
+ interrupt-parent = <&gpio>;
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 9e9457c7bba6..43c691ba48cd 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15203,6 +15203,13 @@ S: Maintained
F: Documentation/devicetree/bindings/iio/dac/lltc,ltc1660.yaml
F: drivers/iio/dac/ltc1660.c
+LTC2378 IIO ADC DRIVER
+M: Marcelo Schmitt <marcelo.schmitt@analog.com>
+L: linux-iio@vger.kernel.org
+S: Supported
+W: https://ez.analog.com/linux-software-drivers
+F: Documentation/devicetree/bindings/iio/adc/adi,ltc2378.yaml
+
LTC2664 IIO DAC DRIVER
M: Michael Hennerich <michael.hennerich@analog.com>
M: Kim Seer Paller <kimseer.paller@analog.com>
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 2/4] iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs
2026-05-28 15:02 [PATCH v2 0/4] iio: adc: Add support for LTC2378 and similar ADCs Marcelo Schmitt
2026-05-28 15:03 ` [PATCH v2 1/4] dt-bindings: iio: adc: Add ltc2378 Marcelo Schmitt
@ 2026-05-28 15:03 ` Marcelo Schmitt
2026-05-28 15:51 ` sashiko-bot
2026-05-28 15:04 ` [PATCH v2 3/4] iio: adc: ltc2378: Enable high-speed data capture Marcelo Schmitt
2026-05-28 15:04 ` [PATCH v2 4/4] iio: adc: ltc2378: Enable triggered buffer " Marcelo Schmitt
3 siblings, 1 reply; 9+ messages in thread
From: Marcelo Schmitt @ 2026-05-28 15:03 UTC (permalink / raw)
To: linux-iio, devicetree, linux-kernel
Cc: jic23, nuno.sa, Michael.Hennerich, dlechner, andy, robh, krzk+dt,
conor+dt, pop.ioan-daniel, marcelo.schmitt1
Support for LTC2378-20 and similar analog-to-digital converters.
Signed-off-by: Marcelo Schmitt <marcelo.schmitt@analog.com>
---
Change log v1 -> v2:
- Added missing includes and sorted the include section.
- Added waiting time required before reading back LTC2378 conversion data.
- Fixed voltage regulator read error path.
- Properly right-aligned sample data.
- Dropped device ID table and simplified chip specific information keeping.
- Using named device_id data initializers.
- Use bool for output code type distinction.
- Added scope for IIO_DEV_ACQUIRE_DIRECT_MODE usage.
MAINTAINERS | 1 +
drivers/iio/adc/Kconfig | 12 ++
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ltc2378.c | 342 ++++++++++++++++++++++++++++++++++++++
drivers/iio/adc/ltc2378.h | 47 ++++++
5 files changed, 403 insertions(+)
create mode 100644 drivers/iio/adc/ltc2378.c
create mode 100644 drivers/iio/adc/ltc2378.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 43c691ba48cd..aed265ecabba 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15209,6 +15209,7 @@ L: linux-iio@vger.kernel.org
S: Supported
W: https://ez.analog.com/linux-software-drivers
F: Documentation/devicetree/bindings/iio/adc/adi,ltc2378.yaml
+F: drivers/iio/adc/ltc2378*
LTC2664 IIO DAC DRIVER
M: Michael Hennerich <michael.hennerich@analog.com>
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 8550917226a1..70fec8e3e891 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -939,6 +939,18 @@ config LTC2309
This driver can also be built as a module. If so, the module will
be called ltc2309.
+config LTC2378
+ tristate "Analog Devices LTC2378 ADC driver"
+ depends on SPI
+ depends on GPIOLIB || PWM
+ select IIO_BUFFER
+ help
+ Say yes here to build support for Analog Devices LTC2378-20 and
+ similar analog to digital converters.
+
+ This driver can also be built as a module. If so, the module will
+ be called ltc2378.
+
config LTC2471
tristate "Linear Technology LTC2471 and LTC2473 ADC driver"
depends on I2C
diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile
index 707dd708912f..1814fb78dde3 100644
--- a/drivers/iio/adc/Makefile
+++ b/drivers/iio/adc/Makefile
@@ -81,6 +81,7 @@ obj-$(CONFIG_LP8788_ADC) += lp8788_adc.o
obj-$(CONFIG_LPC18XX_ADC) += lpc18xx_adc.o
obj-$(CONFIG_LPC32XX_ADC) += lpc32xx_adc.o
obj-$(CONFIG_LTC2309) += ltc2309.o
+obj-$(CONFIG_LTC2378) += ltc2378.o
obj-$(CONFIG_LTC2471) += ltc2471.o
obj-$(CONFIG_LTC2485) += ltc2485.o
obj-$(CONFIG_LTC2496) += ltc2496.o ltc2497-core.o
diff --git a/drivers/iio/adc/ltc2378.c b/drivers/iio/adc/ltc2378.c
new file mode 100644
index 000000000000..bdff98157979
--- /dev/null
+++ b/drivers/iio/adc/ltc2378.c
@@ -0,0 +1,342 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Analog Devices LTC2378 ADC series driver
+ *
+ * Copyright (C) 2026 Analog Devices Inc.
+ * Author: Ioan-Daniel Pop <pop.ioan-daniel@analog.com>
+ * Author: Marcelo Schmitt <marcelo.schmitt@analog.com>
+ */
+
+#include <linux/bitops.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/regulator/consumer.h>
+#include <linux/spi/spi.h>
+#include <linux/types.h>
+
+#include <linux/iio/iio.h>
+#include <linux/iio/types.h>
+
+#include "ltc2378.h"
+
+static const struct ltc2378_chip_info ltc2338_18_chip_info = {
+ .name = "ltc2338-18",
+ .resolution = 18,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2364_16_chip_info = {
+ .name = "ltc2364-16",
+ .resolution = 16,
+ .twos_comp = false,
+};
+
+static const struct ltc2378_chip_info ltc2364_18_chip_info = {
+ .name = "ltc2364-18",
+ .resolution = 18,
+ .twos_comp = false,
+};
+
+static const struct ltc2378_chip_info ltc2367_16_chip_info = {
+ .name = "ltc2367-16",
+ .resolution = 16,
+ .twos_comp = false,
+};
+
+static const struct ltc2378_chip_info ltc2367_18_chip_info = {
+ .name = "ltc2367-18",
+ .resolution = 18,
+ .twos_comp = false,
+};
+
+static const struct ltc2378_chip_info ltc2368_16_chip_info = {
+ .name = "ltc2368-16",
+ .resolution = 16,
+ .twos_comp = false,
+
+};
+
+static const struct ltc2378_chip_info ltc2368_18_chip_info = {
+ .name = "ltc2368-18",
+ .resolution = 18,
+ .twos_comp = false,
+};
+
+static const struct ltc2378_chip_info ltc2369_18_chip_info = {
+ .name = "ltc2369-18",
+ .resolution = 18,
+ .twos_comp = false,
+};
+
+static const struct ltc2378_chip_info ltc2370_16_chip_info = {
+ .name = "ltc2370-16",
+ .resolution = 16,
+ .twos_comp = false,
+};
+
+static const struct ltc2378_chip_info ltc2376_16_chip_info = {
+ .name = "ltc2376-16",
+ .resolution = 16,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2376_18_chip_info = {
+ .name = "ltc2376-18",
+ .resolution = 18,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2376_20_chip_info = {
+ .name = "ltc2376-20",
+ .resolution = 20,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2377_16_chip_info = {
+ .name = "ltc2377-16",
+ .resolution = 16,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2377_18_chip_info = {
+ .name = "ltc2377-18",
+ .resolution = 18,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2377_20_chip_info = {
+ .name = "ltc2377-20",
+ .resolution = 20,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2378_16_chip_info = {
+ .name = "ltc2378-16",
+ .resolution = 16,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2378_18_chip_info = {
+ .name = "ltc2378-18",
+ .resolution = 18,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2378_20_chip_info = {
+ .name = "ltc2378-20",
+ .resolution = 20,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2379_18_chip_info = {
+ .name = "ltc2379-18",
+ .resolution = 18,
+ .twos_comp = true,
+};
+
+static const struct ltc2378_chip_info ltc2380_16_chip_info = {
+ .name = "ltc2380-16",
+ .resolution = 16,
+ .twos_comp = true,
+};
+
+static int ltc2378_convert_and_acquire(struct ltc2378_state *st)
+{
+ int ret;
+
+ /* Cause a rising edge of CNV to initiate a new ADC conversion */
+ gpiod_set_value_cansleep(st->cnv_gpio, 1);
+ fsleep(4);
+ ret = spi_sync_transfer(st->spi, &st->xfer, 1);
+ gpiod_set_value_cansleep(st->cnv_gpio, 0);
+
+ return ret;
+}
+
+static int ltc2378_channel_single_read(const struct iio_chan_spec *chan,
+ struct ltc2378_state *st, int *val)
+{
+ const struct iio_scan_type *scan_type = &chan->scan_type;
+ u32 sample;
+ int ret;
+
+ ret = ltc2378_convert_and_acquire(st);
+ if (ret)
+ return ret;
+
+ if (scan_type->realbits > 16)
+ sample = st->scan.data.sample_buf32;
+ else
+ sample = st->scan.data.sample_buf16;
+
+ if (scan_type->format == IIO_SCAN_FORMAT_SIGNED_INT)
+ *val = sign_extend32(sample, scan_type->realbits - 1);
+ else
+ *val = sample;
+
+ return 0;
+}
+
+static int ltc2378_read_raw(struct iio_dev *indio_dev,
+ const struct iio_chan_spec *chan,
+ int *val, int *val2, long info)
+{
+ struct ltc2378_state *st = iio_priv(indio_dev);
+ int ret;
+
+ switch (info) {
+ case IIO_CHAN_INFO_RAW:
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ ret = ltc2378_channel_single_read(chan, st, val);
+ if (ret)
+ return ret;
+
+ return IIO_VAL_INT;
+
+ case IIO_CHAN_INFO_SCALE:
+ *val = st->ref_uV / MILLI;
+ /*
+ * For all LTC2378-like devices, the amount of bits that express
+ * voltage magnitude depend on the output code format:
+ * - straight binary: All precision/resolution bits are used.
+ * - 2's complement: One of the precision bits is used for sign.
+ */
+ if (st->info->twos_comp)
+ *val2 = st->info->resolution - 1;
+ else
+ *val2 = st->info->resolution;
+
+ return IIO_VAL_FRACTIONAL_LOG2;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct iio_info ltc2378_iio_info = {
+ .read_raw = <c2378_read_raw,
+};
+
+static int ltc2378_probe(struct spi_device *spi)
+{
+ struct device *dev = &spi->dev;
+ unsigned int num_iio_chans = 1;
+ struct iio_dev *indio_dev;
+ struct ltc2378_state *st;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*st));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ st = iio_priv(indio_dev);
+ st->spi = spi;
+
+ ret = devm_regulator_get_enable_read_voltage(dev, "ref");
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "failed to read ref regulator\n");
+
+ st->ref_uV = ret;
+ st->info = spi_get_device_match_data(spi);
+ if (!st->info)
+ return -EINVAL;
+
+ indio_dev->name = st->info->name;
+ indio_dev->info = <c2378_iio_info;
+ indio_dev->modes = INDIO_DIRECT_MODE;
+
+ st->cnv_gpio = devm_gpiod_get_optional(dev, "cnv", GPIOD_OUT_LOW);
+ if (st->cnv_gpio && IS_ERR(st->cnv_gpio))
+ return dev_err_probe(dev, PTR_ERR(st->cnv_gpio),
+ "failed to get CNV GPIO");
+
+ st->chans[0].type = IIO_VOLTAGE;
+ st->chans[0].info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE);
+ st->chans[0].scan_type.format = st->info->twos_comp ? IIO_SCAN_FORMAT_SIGNED_INT :
+ IIO_SCAN_FORMAT_UNSIGNED_INT;
+ st->chans[0].scan_type.realbits = st->info->resolution;
+ st->chans[0].scan_type.storagebits = st->info->resolution > 16 ? 32 : 16;
+
+ st->xfer.rx_buf = &st->scan.data;
+ st->xfer.len = BITS_TO_BYTES(st->chans[0].scan_type.storagebits);
+ st->xfer.bits_per_word = st->info->resolution > 16 ? 32 : 16;
+
+ indio_dev->channels = st->chans;
+ indio_dev->num_channels = num_iio_chans;
+
+ return devm_iio_device_register(&spi->dev, indio_dev);
+}
+
+static const struct of_device_id ltc2378_of_match[] = {
+ { .compatible = "adi,ltc2338-18", .data = <c2338_18_chip_info },
+ { .compatible = "adi,ltc2364-16", .data = <c2364_16_chip_info },
+ { .compatible = "adi,ltc2364-18", .data = <c2364_18_chip_info },
+ { .compatible = "adi,ltc2367-16", .data = <c2367_16_chip_info },
+ { .compatible = "adi,ltc2367-18", .data = <c2367_18_chip_info },
+ { .compatible = "adi,ltc2368-16", .data = <c2368_16_chip_info },
+ { .compatible = "adi,ltc2368-18", .data = <c2368_18_chip_info },
+ { .compatible = "adi,ltc2369-18", .data = <c2369_18_chip_info },
+ { .compatible = "adi,ltc2370-16", .data = <c2370_16_chip_info },
+ { .compatible = "adi,ltc2376-16", .data = <c2376_16_chip_info },
+ { .compatible = "adi,ltc2376-18", .data = <c2376_18_chip_info },
+ { .compatible = "adi,ltc2376-20", .data = <c2376_20_chip_info },
+ { .compatible = "adi,ltc2377-16", .data = <c2377_16_chip_info },
+ { .compatible = "adi,ltc2377-18", .data = <c2377_18_chip_info },
+ { .compatible = "adi,ltc2377-20", .data = <c2377_20_chip_info },
+ { .compatible = "adi,ltc2378-16", .data = <c2378_16_chip_info },
+ { .compatible = "adi,ltc2378-18", .data = <c2378_18_chip_info },
+ { .compatible = "adi,ltc2378-20", .data = <c2378_20_chip_info },
+ { .compatible = "adi,ltc2379-18", .data = <c2379_18_chip_info },
+ { .compatible = "adi,ltc2380-16", .data = <c2380_16_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ltc2378_of_match);
+
+static const struct spi_device_id ltc2378_spi_id[] = {
+ { .name = "ltc2338-18", .driver_data = (kernel_ulong_t)<c2338_18_chip_info },
+ { .name = "ltc2364-16", .driver_data = (kernel_ulong_t)<c2364_16_chip_info },
+ { .name = "ltc2364-18", .driver_data = (kernel_ulong_t)<c2364_18_chip_info },
+ { .name = "ltc2367-16", .driver_data = (kernel_ulong_t)<c2367_16_chip_info },
+ { .name = "ltc2367-18", .driver_data = (kernel_ulong_t)<c2367_18_chip_info },
+ { .name = "ltc2368-16", .driver_data = (kernel_ulong_t)<c2368_16_chip_info },
+ { .name = "ltc2368-18", .driver_data = (kernel_ulong_t)<c2368_18_chip_info },
+ { .name = "ltc2369-18", .driver_data = (kernel_ulong_t)<c2369_18_chip_info },
+ { .name = "ltc2370-16", .driver_data = (kernel_ulong_t)<c2370_16_chip_info },
+ { .name = "ltc2376-16", .driver_data = (kernel_ulong_t)<c2376_16_chip_info },
+ { .name = "ltc2376-18", .driver_data = (kernel_ulong_t)<c2376_18_chip_info },
+ { .name = "ltc2376-20", .driver_data = (kernel_ulong_t)<c2376_20_chip_info },
+ { .name = "ltc2377-16", .driver_data = (kernel_ulong_t)<c2377_16_chip_info },
+ { .name = "ltc2377-18", .driver_data = (kernel_ulong_t)<c2377_18_chip_info },
+ { .name = "ltc2377-20", .driver_data = (kernel_ulong_t)<c2377_20_chip_info },
+ { .name = "ltc2378-16", .driver_data = (kernel_ulong_t)<c2378_16_chip_info },
+ { .name = "ltc2378-18", .driver_data = (kernel_ulong_t)<c2378_18_chip_info },
+ { .name = "ltc2378-20", .driver_data = (kernel_ulong_t)<c2378_20_chip_info },
+ { .name = "ltc2379-18", .driver_data = (kernel_ulong_t)<c2379_18_chip_info },
+ { .name = "ltc2380-16", .driver_data = (kernel_ulong_t)<c2380_16_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(spi, ltc2378_spi_id);
+
+static struct spi_driver ltc2378_driver = {
+ .driver = {
+ .name = "ltc2378",
+ .of_match_table = ltc2378_of_match
+ },
+ .probe = ltc2378_probe,
+ .id_table = ltc2378_spi_id,
+};
+module_spi_driver(ltc2378_driver);
+
+MODULE_AUTHOR("Ioan-Daniel Pop <pop.ioan-daniel@analog.com>");
+MODULE_AUTHOR("Marcelo Schmitt <marcelo.schmitt@analog.com>");
+MODULE_DESCRIPTION("Analog Devices LTC2378 ADC series driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/iio/adc/ltc2378.h b/drivers/iio/adc/ltc2378.h
new file mode 100644
index 000000000000..399e8f67cd0e
--- /dev/null
+++ b/drivers/iio/adc/ltc2378.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Analog Devices LTC2378 and similar ADCs common definitions and properties
+ * Copyright (C) 2026 Analog Devices, Inc.
+ * Author: Marcelo Schmitt <marcelo.schmitt@analog.com>
+ */
+
+#ifndef __DRIVERS_IIO_ADC_LTC2378_H__
+#define __DRIVERS_IIO_ADC_LTC2378_H__
+
+#include <linux/iio/iio.h>
+#include <linux/spi/spi.h>
+#include <linux/types.h>
+#include <linux/units.h>
+
+#define LTC2378_TDSDOBUSYL_NS 5
+#define LTC2378_TBUSYLH_NS 13
+#define LTC2378_TCNV_HIGH_NS 20
+
+struct ltc2378_chip_info {
+ const char *name;
+ int resolution;
+ bool twos_comp; /* Output code is 2's complement or straight binary */
+};
+
+struct ltc2378_state {
+ const struct ltc2378_chip_info *info;
+ struct gpio_desc *cnv_gpio;
+ struct spi_device *spi;
+ struct spi_transfer xfer;
+ struct iio_chan_spec chans[2]; /* 1 physical chan + 1 timestamp chan */
+ int ref_uV;
+
+ /*
+ * DMA (thus cache coherency maintenance) requires the
+ * transfer buffers to live in their own cache lines.
+ */
+ struct {
+ union {
+ u16 sample_buf16;
+ u32 sample_buf32;
+ } data;
+ aligned_s64 timestamp;
+ } scan __aligned(IIO_DMA_MINALIGN);
+};
+
+#endif /* __DRIVERS_IIO_ADC_LTC2378_H__ */
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 3/4] iio: adc: ltc2378: Enable high-speed data capture
2026-05-28 15:02 [PATCH v2 0/4] iio: adc: Add support for LTC2378 and similar ADCs Marcelo Schmitt
2026-05-28 15:03 ` [PATCH v2 1/4] dt-bindings: iio: adc: Add ltc2378 Marcelo Schmitt
2026-05-28 15:03 ` [PATCH v2 2/4] iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs Marcelo Schmitt
@ 2026-05-28 15:04 ` Marcelo Schmitt
2026-05-28 16:39 ` sashiko-bot
2026-05-28 15:04 ` [PATCH v2 4/4] iio: adc: ltc2378: Enable triggered buffer " Marcelo Schmitt
3 siblings, 1 reply; 9+ messages in thread
From: Marcelo Schmitt @ 2026-05-28 15:04 UTC (permalink / raw)
To: linux-iio, devicetree, linux-kernel
Cc: jic23, nuno.sa, Michael.Hennerich, dlechner, andy, robh, krzk+dt,
conor+dt, pop.ioan-daniel, marcelo.schmitt1
Make use of SPI transfer offloading to speed up data capture, enabling data
acquisition at faster sample rates (up to 2 MSPS).
Signed-off-by: Marcelo Schmitt <marcelo.schmitt@analog.com>
---
Change log v1 -> v2:
- Set loop count to prevent CPU stall when calculating PWM parameters.
- Hardcode set specific offload supported amount of data element bits.
- Fixed devm_spi_offload_get() error path.
- Initialize init scan_type according to buffer selection.
- Offload attributes now are channel based instead of device based.
- Wrapped comments close to 80 columns.
I haven't manage to code an is_visible() filter for device attributes specific
of offload usage.
Using DEFINE_SYSFS_GROUP_VISIBLE() didn't work out as the
sampling_frequency_available attribute would still appear even without offload.
Using DEFINE_SYSFS_GROUP_VISIBLE() and sysfs attribute specific visibility
function resulted in kernel error (Invalid permissions 01).
Instead, I made them channel shared by all attributes.
I tried a lib-like code arrangement like ADIS IMU but didn't manage to get it
to work. Separated the code into ltc2378.h, ltc2378.c (lib), ltc2378-core.c
(base driver), and ltc2378-offload.c with lib and core built as module and
offload.c builtin. Though, I ran into different build and linkage errors
depending on how ltc2378-offload.c utilities were encapsulated. Having read
through IMU ADIS code, I now think LTC2378 is different and doesn't require
a code design that much elaborated. Differently from the separate code for
supporting specific adis devices, all ltc2378-like devices can be supported
by the same set of functions from ltc2378.c. We can maybe have an ops struct
with pointers to buffer specific routines (e.g. offload vs triggered). That
might be worth it if we would want to switch between buffer types at runtime.
For v2, I'm keeping the module design similar to v1. I appreciate additional
examples if a module redesign is still recommended.
drivers/iio/adc/Kconfig | 9 +
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ltc2378-offload-buffer.c | 296 +++++++++++++++++++++++
drivers/iio/adc/ltc2378.c | 91 ++++++-
drivers/iio/adc/ltc2378.h | 66 +++++
5 files changed, 457 insertions(+), 6 deletions(-)
create mode 100644 drivers/iio/adc/ltc2378-offload-buffer.c
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 70fec8e3e891..5f7038b5e731 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -944,6 +944,7 @@ config LTC2378
depends on SPI
depends on GPIOLIB || PWM
select IIO_BUFFER
+ select LTC2378_OFFLOAD_BUFFER if SPI_OFFLOAD && PWM && IIO_BUFFER
help
Say yes here to build support for Analog Devices LTC2378-20 and
similar analog to digital converters.
@@ -2027,3 +2028,11 @@ config XILINX_AMS
xilinx-ams.
endmenu
+
+config LTC2378_OFFLOAD_BUFFER
+ bool
+ select SPI_OFFLOAD_TRIGGER_PWM
+ select IIO_BUFFER_DMA
+ select IIO_BUFFER_DMAENGINE
+ help
+ Say yes here to build support for high speed data capture with LTC2378
diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile
index 1814fb78dde3..2fa5dce0ceea 100644
--- a/drivers/iio/adc/Makefile
+++ b/drivers/iio/adc/Makefile
@@ -82,6 +82,7 @@ obj-$(CONFIG_LPC18XX_ADC) += lpc18xx_adc.o
obj-$(CONFIG_LPC32XX_ADC) += lpc32xx_adc.o
obj-$(CONFIG_LTC2309) += ltc2309.o
obj-$(CONFIG_LTC2378) += ltc2378.o
+obj-$(CONFIG_LTC2378_OFFLOAD_BUFFER) += ltc2378-offload-buffer.o
obj-$(CONFIG_LTC2471) += ltc2471.o
obj-$(CONFIG_LTC2485) += ltc2485.o
obj-$(CONFIG_LTC2496) += ltc2496.o ltc2497-core.o
diff --git a/drivers/iio/adc/ltc2378-offload-buffer.c b/drivers/iio/adc/ltc2378-offload-buffer.c
new file mode 100644
index 000000000000..8ad2d319f669
--- /dev/null
+++ b/drivers/iio/adc/ltc2378-offload-buffer.c
@@ -0,0 +1,296 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2026 Analog Devices, Inc.
+ * Author: Marcelo Schmitt <marcelo.schmitt@analog.com>
+ */
+
+#include <linux/cleanup.h>
+#include <linux/err.h>
+#include <linux/math.h>
+#include <linux/math64.h>
+#include <linux/pwm.h>
+#include <linux/spi/spi.h>
+#include <linux/spi/offload/consumer.h>
+#include <linux/spi/offload/types.h>
+#include <linux/time64.h>
+
+#include <linux/iio/buffer.h>
+#include <linux/iio/buffer-dmaengine.h>
+#include <linux/iio/iio.h>
+#include <linux/iio/types.h>
+
+#include "ltc2378.h"
+
+/*
+ * SPI offload wiring schema
+ *
+ * +-------------+ +-------------+
+ * | CNV |<-----+--| GPIO |
+ * | | +--| PWM0 |
+ * | | | |
+ * | | +--| PWM1 |
+ * | | | +-------------+
+ * | | +->| TRIGGER |
+ * | | | |
+ * | ADC | | SPI |
+ * | | | controller |
+ * | | | |
+ * | SDI |<--------| SDO |
+ * | SDO |-------->| SDI |
+ * | SCLK |<--------| SCLK |
+ * +-------------+ +-------------+
+ *
+ */
+static int ltc2378_update_conversion_rate(struct ltc2378_state *st, int freq_Hz)
+{
+ struct spi_offload_trigger_config *config = &st->offload_trigger_config;
+ unsigned int min_read_offset, offload_period_ns;
+ struct pwm_waveform cnv_wf = { };
+ u64 target = LTC2378_TCNV_HIGH_NS;
+ unsigned int count = 0;
+ u64 offload_offset_ns;
+ int ret;
+
+ if (freq_Hz == 0)
+ return -EINVAL;
+
+ if (freq_Hz < 1 || freq_Hz > st->info->max_sample_rate_hz)
+ return -ERANGE;
+
+ /* Configure CNV PWM waveform */
+ cnv_wf.period_length_ns = DIV_ROUND_CLOSEST(NSEC_PER_SEC, freq_Hz);
+
+ /*
+ * Ensure CNV high time meets minimum requirement (20ns). The PWM
+ * hardware may round the duty cycle, so iterate until we get at least
+ * the minimum required high time.
+ */
+ do {
+ cnv_wf.duty_length_ns = target;
+ ret = pwm_round_waveform_might_sleep(st->cnv_trigger, &cnv_wf);
+ if (ret)
+ return ret;
+ target += 10; /* Increment by PWM duty cycle period */
+ } while (cnv_wf.duty_length_ns < LTC2378_TCNV_HIGH_NS || count++ < 100);
+
+ /*
+ * Configure SPI offload PWM trigger.
+ * The trigger should fire after tBUSYLH + tCONV + tDSDOBUSYL.
+ * Minimum time needed: TBUSYLH (13ns) + TCONV (part-specific) + TDSDOBUSYL (5ns)
+ *
+ * Use the same period as CNV PWM to avoid timing issues.
+ * Convert back from period to frequency for the SPI offload API.
+ */
+ offload_period_ns = cnv_wf.period_length_ns;
+ config->periodic.frequency_hz = DIV_ROUND_UP(HZ_PER_GHZ, offload_period_ns);
+ min_read_offset = LTC2378_TBUSYLH_NS + st->info->tconv_ns + LTC2378_TDSDOBUSYL_NS;
+ offload_offset_ns = min_read_offset;
+ count = 0;
+ do {
+ config->periodic.offset_ns = offload_offset_ns;
+ ret = spi_offload_trigger_validate(st->offload_trigger, config);
+ if (ret)
+ return ret;
+ offload_offset_ns += 10;
+ } while (config->periodic.offset_ns < min_read_offset || count++ < 100);
+
+ st->cnv_wf = cnv_wf;
+ st->cnv_Hz = DIV_ROUND_CLOSEST_ULL(HZ_PER_GHZ, cnv_wf.period_length_ns);
+
+ return 0;
+}
+
+int ltc2378_get_sampling_frequency(struct ltc2378_state *st, int *val)
+{
+ *val = st->cnv_Hz;
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(ltc2378_get_sampling_frequency, "IIO_LTC2378");
+
+int ltc2378_set_sampling_frequency(struct ltc2378_state *st, int freq_Hz)
+{
+ return ltc2378_update_conversion_rate(st, freq_Hz);
+}
+EXPORT_SYMBOL_NS_GPL(ltc2378_set_sampling_frequency, "IIO_LTC2378");
+
+int ltc2378_read_avail(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
+ const int **vals, int *type, int *length, long mask)
+{
+ struct ltc2378_state *st = iio_priv(indio_dev);
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ *vals = st->sample_freq_range;
+ *type = IIO_VAL_INT;
+ return IIO_AVAIL_RANGE;
+ default:
+ return -EINVAL;
+ }
+}
+EXPORT_SYMBOL_NS_GPL(ltc2378_read_avail, "IIO_LTC2378");
+
+int ltc2378_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
+ int val, int val2, long mask)
+{
+ struct ltc2378_state *st = iio_priv(indio_dev);
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ if (val < 1 || val > st->info->max_sample_rate_hz)
+ return -EINVAL;
+
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ return ltc2378_set_sampling_frequency(st, val);
+ }
+ default:
+ return -EINVAL;
+ }
+}
+EXPORT_SYMBOL_NS_GPL(ltc2378_write_raw, "IIO_LTC2378");
+
+static int ltc2378_prepare_offload_message(struct device *dev,
+ struct ltc2378_state *st)
+{
+ st->offload_xfer.bits_per_word = st->info->resolution;
+ /*
+ * Ideally, we would ask the offload provider what data word sizes are
+ * supported so we could use smaller words for less precise ADCs.
+ * Though, the currently available SPI offloading hardware only supports
+ * pushing 32-bit sized data elements to DMA memory. Because of that,
+ * we hardcode set 4 byte sized transfers.
+ */
+ st->offload_xfer.len = 4;
+ st->offload_xfer.offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
+
+ /* Initialize message with offload */
+ spi_message_init_with_transfers(&st->offload_msg, &st->offload_xfer, 1);
+ st->offload_msg.offload = st->offload;
+
+ return devm_spi_optimize_message(dev, st->spi, &st->offload_msg);
+}
+
+static int ltc2378_offload_buffer_postenable(struct iio_dev *indio_dev)
+{
+ struct ltc2378_state *st = iio_priv(indio_dev);
+ int ret;
+
+ ret = pwm_set_waveform_might_sleep(st->cnv_trigger, &st->cnv_wf, false);
+ if (ret)
+ return ret;
+
+ ret = spi_offload_trigger_enable(st->offload, st->offload_trigger,
+ &st->offload_trigger_config);
+ if (ret)
+ goto out_pwm_disable;
+
+ return 0;
+
+out_pwm_disable:
+ pwm_disable(st->cnv_trigger);
+ return ret;
+}
+
+static int ltc2378_offload_buffer_predisable(struct iio_dev *indio_dev)
+{
+ struct ltc2378_state *st = iio_priv(indio_dev);
+
+ spi_offload_trigger_disable(st->offload, st->offload_trigger);
+ pwm_disable(st->cnv_trigger);
+
+ return 0;
+}
+
+static const struct iio_buffer_setup_ops ltc2378_offload_buffer_ops = {
+ .postenable = <c2378_offload_buffer_postenable,
+ .predisable = <c2378_offload_buffer_predisable,
+};
+
+static int ltc2378_spi_offload_setup(struct iio_dev *indio_dev,
+ struct ltc2378_state *st)
+{
+ struct device *dev = &st->spi->dev;
+ struct dma_chan *rx_dma;
+
+ indio_dev->setup_ops = <c2378_offload_buffer_ops;
+
+ st->offload_trigger = devm_spi_offload_trigger_get(dev, st->offload,
+ SPI_OFFLOAD_TRIGGER_PERIODIC);
+ if (IS_ERR(st->offload_trigger))
+ return dev_err_probe(dev, PTR_ERR(st->offload_trigger),
+ "failed to get offload trigger\n");
+
+ st->offload_trigger_config.type = SPI_OFFLOAD_TRIGGER_PERIODIC;
+
+ rx_dma = devm_spi_offload_rx_stream_request_dma_chan(dev, st->offload);
+ if (IS_ERR(rx_dma))
+ return dev_err_probe(dev, PTR_ERR(rx_dma), "failed to get offload RX DMA\n");
+
+ return devm_iio_dmaengine_buffer_setup_with_handle(dev, indio_dev, rx_dma,
+ IIO_BUFFER_DIRECTION_IN);
+}
+
+static int ltc2378_pwm_get(struct ltc2378_state *st)
+{
+ struct device *dev = &st->spi->dev;
+
+ st->cnv_trigger = devm_pwm_get(dev, NULL);
+ if (IS_ERR(st->cnv_trigger))
+ return dev_err_probe(dev, PTR_ERR(st->cnv_trigger),
+ "failed to get cnv pwm\n");
+
+ pwm_disable(st->cnv_trigger);
+
+ return 0;
+}
+
+static const struct spi_offload_config ltc2378_offload_config = {
+ .capability_flags = SPI_OFFLOAD_CAP_TRIGGER |
+ SPI_OFFLOAD_CAP_RX_STREAM_DMA,
+};
+
+int ltc2378_offload_buffer_setup(struct iio_dev *indio_dev, struct spi_device *spi)
+{
+ struct ltc2378_state *st = iio_priv(indio_dev);
+ struct device *dev = &spi->dev;
+ int ret;
+
+ st->offload = devm_spi_offload_get(dev, spi, <c2378_offload_config);
+ ret = PTR_ERR_OR_ZERO(st->offload);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to get offload\n");
+
+ ret = ltc2378_spi_offload_setup(indio_dev, st);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to setup SPI offload\n");
+
+ ret = ltc2378_pwm_get(st);
+ if (ret)
+ return ret;
+
+ st->sample_freq_range[0] = 1; /* min */
+ st->sample_freq_range[1] = 1; /* step */
+ st->sample_freq_range[2] = st->info->max_sample_rate_hz; /* max */
+
+ /*
+ * Start with a slower sampling rate so there is some room for
+ * adjusting the sampling frequency without hitting the maximum
+ * conversion rate.
+ */
+ ret = ltc2378_update_conversion_rate(st, st->info->max_sample_rate_hz >> 4);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to sampling frequency\n");
+
+ ret = ltc2378_prepare_offload_message(&spi->dev, st);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to optimize SPI message\n");
+
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(ltc2378_offload_buffer_setup, "IIO_LTC2378");
+
+MODULE_IMPORT_NS("IIO_LTC2378");
diff --git a/drivers/iio/adc/ltc2378.c b/drivers/iio/adc/ltc2378.c
index bdff98157979..6b01d8e96cc6 100644
--- a/drivers/iio/adc/ltc2378.c
+++ b/drivers/iio/adc/ltc2378.c
@@ -27,36 +27,48 @@
static const struct ltc2378_chip_info ltc2338_18_chip_info = {
.name = "ltc2338-18",
.resolution = 18,
+ .max_sample_rate_hz = HZ_PER_MHZ,
+ .tconv_ns = 527,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2364_16_chip_info = {
.name = "ltc2364-16",
.resolution = 16,
+ .max_sample_rate_hz = 250 * HZ_PER_KHZ,
+ .tconv_ns = 3000,
.twos_comp = false,
};
static const struct ltc2378_chip_info ltc2364_18_chip_info = {
.name = "ltc2364-18",
.resolution = 18,
+ .max_sample_rate_hz = 250 * HZ_PER_KHZ,
+ .tconv_ns = 3000,
.twos_comp = false,
};
static const struct ltc2378_chip_info ltc2367_16_chip_info = {
.name = "ltc2367-16",
.resolution = 16,
+ .max_sample_rate_hz = 500 * HZ_PER_KHZ,
+ .tconv_ns = 1500,
.twos_comp = false,
};
static const struct ltc2378_chip_info ltc2367_18_chip_info = {
.name = "ltc2367-18",
.resolution = 18,
+ .max_sample_rate_hz = 500 * HZ_PER_KHZ,
+ .tconv_ns = 1500,
.twos_comp = false,
};
static const struct ltc2378_chip_info ltc2368_16_chip_info = {
.name = "ltc2368-16",
.resolution = 16,
+ .max_sample_rate_hz = HZ_PER_MHZ,
+ .tconv_ns = 527,
.twos_comp = false,
};
@@ -64,84 +76,112 @@ static const struct ltc2378_chip_info ltc2368_16_chip_info = {
static const struct ltc2378_chip_info ltc2368_18_chip_info = {
.name = "ltc2368-18",
.resolution = 18,
+ .max_sample_rate_hz = HZ_PER_MHZ,
+ .tconv_ns = 527,
.twos_comp = false,
};
static const struct ltc2378_chip_info ltc2369_18_chip_info = {
.name = "ltc2369-18",
.resolution = 18,
+ .max_sample_rate_hz = 1600 * HZ_PER_KHZ,
+ .tconv_ns = 412,
.twos_comp = false,
};
static const struct ltc2378_chip_info ltc2370_16_chip_info = {
.name = "ltc2370-16",
.resolution = 16,
+ .max_sample_rate_hz = 2 * HZ_PER_MHZ,
+ .tconv_ns = 322,
.twos_comp = false,
};
static const struct ltc2378_chip_info ltc2376_16_chip_info = {
.name = "ltc2376-16",
.resolution = 16,
+ .max_sample_rate_hz = 250 * HZ_PER_KHZ,
+ .tconv_ns = 3000,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2376_18_chip_info = {
.name = "ltc2376-18",
.resolution = 18,
+ .max_sample_rate_hz = 250 * HZ_PER_KHZ,
+ .tconv_ns = 3000,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2376_20_chip_info = {
.name = "ltc2376-20",
.resolution = 20,
+ .max_sample_rate_hz = 250 * HZ_PER_KHZ,
+ .tconv_ns = 3000,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2377_16_chip_info = {
.name = "ltc2377-16",
.resolution = 16,
+ .max_sample_rate_hz = 500 * HZ_PER_KHZ,
+ .tconv_ns = 1500,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2377_18_chip_info = {
.name = "ltc2377-18",
.resolution = 18,
+ .max_sample_rate_hz = 500 * HZ_PER_KHZ,
+ .tconv_ns = 1500,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2377_20_chip_info = {
.name = "ltc2377-20",
.resolution = 20,
+ .max_sample_rate_hz = 500 * HZ_PER_KHZ,
+ .tconv_ns = 1500,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2378_16_chip_info = {
.name = "ltc2378-16",
.resolution = 16,
+ .max_sample_rate_hz = HZ_PER_MHZ,
+ .tconv_ns = 527,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2378_18_chip_info = {
.name = "ltc2378-18",
.resolution = 18,
+ .max_sample_rate_hz = HZ_PER_MHZ,
+ .tconv_ns = 527,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2378_20_chip_info = {
.name = "ltc2378-20",
.resolution = 20,
+ .max_sample_rate_hz = HZ_PER_MHZ,
+ .tconv_ns = 675,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2379_18_chip_info = {
.name = "ltc2379-18",
.resolution = 18,
+ .max_sample_rate_hz = 1600 * HZ_PER_KHZ,
+ .tconv_ns = 412,
.twos_comp = true,
};
static const struct ltc2378_chip_info ltc2380_16_chip_info = {
.name = "ltc2380-16",
.resolution = 16,
+ .max_sample_rate_hz = 2 * HZ_PER_MHZ,
+ .tconv_ns = 322,
.twos_comp = true,
};
@@ -190,7 +230,7 @@ static int ltc2378_read_raw(struct iio_dev *indio_dev,
int ret;
switch (info) {
- case IIO_CHAN_INFO_RAW:
+ case IIO_CHAN_INFO_RAW: {
IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
if (IIO_DEV_ACQUIRE_FAILED(claim))
return -EBUSY;
@@ -200,7 +240,7 @@ static int ltc2378_read_raw(struct iio_dev *indio_dev,
return ret;
return IIO_VAL_INT;
-
+ }
case IIO_CHAN_INFO_SCALE:
*val = st->ref_uV / MILLI;
/*
@@ -216,6 +256,17 @@ static int ltc2378_read_raw(struct iio_dev *indio_dev,
return IIO_VAL_FRACTIONAL_LOG2;
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ ret = ltc2378_get_sampling_frequency(st, val);
+ if (ret)
+ return ret;
+
+ return IIO_VAL_INT;
+ }
default:
return -EINVAL;
}
@@ -223,6 +274,8 @@ static int ltc2378_read_raw(struct iio_dev *indio_dev,
static const struct iio_info ltc2378_iio_info = {
.read_raw = <c2378_read_raw,
+ .write_raw = LTC2378_WRITE_RAW,
+ .read_avail = LTC2378_READ_AVAIL,
};
static int ltc2378_probe(struct spi_device *spi)
@@ -261,10 +314,35 @@ static int ltc2378_probe(struct spi_device *spi)
st->chans[0].type = IIO_VOLTAGE;
st->chans[0].info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
BIT(IIO_CHAN_INFO_SCALE);
- st->chans[0].scan_type.format = st->info->twos_comp ? IIO_SCAN_FORMAT_SIGNED_INT :
- IIO_SCAN_FORMAT_UNSIGNED_INT;
- st->chans[0].scan_type.realbits = st->info->resolution;
- st->chans[0].scan_type.storagebits = st->info->resolution > 16 ? 32 : 16;
+
+ struct iio_scan_type ltc2378_scan;
+
+ ret = ltc2378_offload_buffer_setup(indio_dev, spi);
+ if (ret == -ENODEV) {
+ /* SPI offloading is unavailable. Fall back to triggered buffer. */
+ dev_dbg(dev, "triggered data capture not supported\n");
+ ltc2378_scan.format = st->info->twos_comp ? IIO_SCAN_FORMAT_SIGNED_INT :
+ IIO_SCAN_FORMAT_UNSIGNED_INT;
+ ltc2378_scan.realbits = st->info->resolution;
+ ltc2378_scan.storagebits = st->info->resolution > 16 ? 32 : 16;
+ } else if (ret) {
+ return dev_err_probe(dev, ret, "error on SPI offload setup\n");
+ } else {
+ /*
+ * Currently, the available offload hardware + DMA configuration
+ * only supports pushing 32-bit data elements to IIO buffers in
+ * CPU endianness.
+ */
+ st->chans[0].info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SAMP_FREQ);
+ st->chans[0].info_mask_shared_by_all_available = BIT(IIO_CHAN_INFO_SAMP_FREQ);
+
+ ltc2378_scan.format = st->info->twos_comp ? IIO_SCAN_FORMAT_SIGNED_INT :
+ IIO_SCAN_FORMAT_UNSIGNED_INT;
+ ltc2378_scan.realbits = st->info->resolution;
+ ltc2378_scan.storagebits = 32;
+ }
+
+ st->chans[0].scan_type = ltc2378_scan;
st->xfer.rx_buf = &st->scan.data;
st->xfer.len = BITS_TO_BYTES(st->chans[0].scan_type.storagebits);
@@ -340,3 +418,4 @@ MODULE_AUTHOR("Ioan-Daniel Pop <pop.ioan-daniel@analog.com>");
MODULE_AUTHOR("Marcelo Schmitt <marcelo.schmitt@analog.com>");
MODULE_DESCRIPTION("Analog Devices LTC2378 ADC series driver");
MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("IIO_LTC2378");
diff --git a/drivers/iio/adc/ltc2378.h b/drivers/iio/adc/ltc2378.h
index 399e8f67cd0e..e79619c12ce9 100644
--- a/drivers/iio/adc/ltc2378.h
+++ b/drivers/iio/adc/ltc2378.h
@@ -9,7 +9,10 @@
#define __DRIVERS_IIO_ADC_LTC2378_H__
#include <linux/iio/iio.h>
+#include <linux/pwm.h>
#include <linux/spi/spi.h>
+#include <linux/spi/offload/consumer.h>
+#include <linux/spi/offload/types.h>
#include <linux/types.h>
#include <linux/units.h>
@@ -20,6 +23,8 @@
struct ltc2378_chip_info {
const char *name;
int resolution;
+ unsigned int max_sample_rate_hz;
+ unsigned int tconv_ns;
bool twos_comp; /* Output code is 2's complement or straight binary */
};
@@ -30,6 +35,15 @@ struct ltc2378_state {
struct spi_transfer xfer;
struct iio_chan_spec chans[2]; /* 1 physical chan + 1 timestamp chan */
int ref_uV;
+ unsigned int cnv_Hz;
+ struct pwm_waveform cnv_wf;
+ struct spi_offload *offload;
+ struct spi_offload_trigger *offload_trigger;
+ struct spi_message offload_msg;
+ struct spi_transfer offload_xfer;
+ struct spi_offload_trigger_config offload_trigger_config;
+ struct pwm_device *cnv_trigger;
+ int sample_freq_range[3];
/*
* DMA (thus cache coherency maintenance) requires the
@@ -44,4 +58,56 @@ struct ltc2378_state {
} scan __aligned(IIO_DMA_MINALIGN);
};
+#define LTC2378_WRITE_RAW LTC2378_WRITE_RAW_PTR
+
+#define LTC2378_READ_AVAIL LTC2378_READ_AVAIL_PTR
+
+#ifdef CONFIG_LTC2378_OFFLOAD_BUFFER
+
+int ltc2378_offload_buffer_setup(struct iio_dev *indio_dev, struct spi_device *spi);
+
+int ltc2378_get_sampling_frequency(struct ltc2378_state *st, int *val);
+
+int ltc2378_set_sampling_frequency(struct ltc2378_state *st, int freq_Hz);
+
+int ltc2378_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
+ int val, int val2, long mask);
+
+int ltc2378_read_avail(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
+ const int **vals, int *type, int *length, long mask);
+
+#define LTC2378_WRITE_RAW_PTR (<c2378_write_raw)
+
+#define LTC2378_READ_AVAIL_PTR (<c2378_read_avail)
+
+#else /* CONFIG_IIO_LTC2378_LIB_OFFLOAD_BUFFER */
+
+#define LTC2378_WRITE_RAW_PTR (NULL)
+
+#define LTC2378_READ_AVAIL_PTR (NULL)
+
+static inline int ltc2378_offload_buffer_setup(struct iio_dev *indio_dev,
+ struct spi_device *spi)
+{
+ return -ENODEV;
+}
+
+int ltc2378_get_sampling_frequency(struct ltc2378_state *st, int *val)
+{
+ return -EOPNOTSUPP;
+}
+
+int ltc2378_set_sampling_frequency(struct ltc2378_state *st, int freq_Hz);
+{
+ return -EOPNOTSUPP;
+}
+
+int ltc2378_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
+ int val, int val2, long mask)
+{
+ return -EOPNOTSUPP;
+}
+
+#endif
+
#endif /* __DRIVERS_IIO_ADC_LTC2378_H__ */
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 4/4] iio: adc: ltc2378: Enable triggered buffer data capture
2026-05-28 15:02 [PATCH v2 0/4] iio: adc: Add support for LTC2378 and similar ADCs Marcelo Schmitt
` (2 preceding siblings ...)
2026-05-28 15:04 ` [PATCH v2 3/4] iio: adc: ltc2378: Enable high-speed data capture Marcelo Schmitt
@ 2026-05-28 15:04 ` Marcelo Schmitt
2026-05-28 17:21 ` sashiko-bot
3 siblings, 1 reply; 9+ messages in thread
From: Marcelo Schmitt @ 2026-05-28 15:04 UTC (permalink / raw)
To: linux-iio, devicetree, linux-kernel
Cc: jic23, nuno.sa, Michael.Hennerich, dlechner, andy, robh, krzk+dt,
conor+dt, pop.ioan-daniel, marcelo.schmitt1
Enable users to run triggered data captures with LTC2378 and similar ADCs.
Signed-off-by: Marcelo Schmitt <marcelo.schmitt@analog.com>
---
Change log v1 -> v2:
- Initialize init scan_type according to buffer selection.
- Use IIO_CHAN_SOFT_TIMESTAMP directly on right hand of assignment.
drivers/iio/adc/Kconfig | 1 +
drivers/iio/adc/ltc2378.c | 35 ++++++++++++++++++++++++++++++++++-
2 files changed, 35 insertions(+), 1 deletion(-)
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 5f7038b5e731..65887c8f3171 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -945,6 +945,7 @@ config LTC2378
depends on GPIOLIB || PWM
select IIO_BUFFER
select LTC2378_OFFLOAD_BUFFER if SPI_OFFLOAD && PWM && IIO_BUFFER
+ select IIO_TRIGGERED_BUFFER
help
Say yes here to build support for Analog Devices LTC2378-20 and
similar analog to digital converters.
diff --git a/drivers/iio/adc/ltc2378.c b/drivers/iio/adc/ltc2378.c
index 6b01d8e96cc6..514645b847f0 100644
--- a/drivers/iio/adc/ltc2378.c
+++ b/drivers/iio/adc/ltc2378.c
@@ -18,8 +18,12 @@
#include <linux/regulator/consumer.h>
#include <linux/spi/spi.h>
#include <linux/types.h>
+#include <linux/units.h>
+#include <linux/iio/buffer.h>
#include <linux/iio/iio.h>
+#include <linux/iio/trigger_consumer.h>
+#include <linux/iio/triggered_buffer.h>
#include <linux/iio/types.h>
#include "ltc2378.h"
@@ -278,6 +282,25 @@ static const struct iio_info ltc2378_iio_info = {
.read_avail = LTC2378_READ_AVAIL,
};
+static irqreturn_t ltc2378_trigger_handler(int irq, void *p)
+{
+ struct iio_poll_func *pf = p;
+ struct iio_dev *indio_dev = pf->indio_dev;
+ struct ltc2378_state *st = iio_priv(indio_dev);
+ int ret;
+
+ ret = ltc2378_convert_and_acquire(st);
+ if (ret < 0)
+ goto err_out;
+
+ iio_push_to_buffers_with_ts(indio_dev, &st->scan, sizeof(st->scan),
+ pf->timestamp);
+
+err_out:
+ iio_trigger_notify_done(indio_dev->trig);
+ return IRQ_HANDLED;
+}
+
static int ltc2378_probe(struct spi_device *spi)
{
struct device *dev = &spi->dev;
@@ -314,17 +337,27 @@ static int ltc2378_probe(struct spi_device *spi)
st->chans[0].type = IIO_VOLTAGE;
st->chans[0].info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
BIT(IIO_CHAN_INFO_SCALE);
+ st->chans[0].scan_index = 0;
struct iio_scan_type ltc2378_scan;
ret = ltc2378_offload_buffer_setup(indio_dev, spi);
if (ret == -ENODEV) {
/* SPI offloading is unavailable. Fall back to triggered buffer. */
- dev_dbg(dev, "triggered data capture not supported\n");
ltc2378_scan.format = st->info->twos_comp ? IIO_SCAN_FORMAT_SIGNED_INT :
IIO_SCAN_FORMAT_UNSIGNED_INT;
ltc2378_scan.realbits = st->info->resolution;
ltc2378_scan.storagebits = st->info->resolution > 16 ? 32 : 16;
+
+ ret = devm_iio_triggered_buffer_setup(dev, indio_dev,
+ &iio_pollfunc_store_time,
+ <c2378_trigger_handler,
+ NULL);
+ if (ret)
+ return ret;
+
+ /* Add timestamp channel */
+ st->chans[num_iio_chans++] = IIO_CHAN_SOFT_TIMESTAMP(1);
} else if (ret) {
return dev_err_probe(dev, ret, "error on SPI offload setup\n");
} else {
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/4] iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs
2026-05-28 15:03 ` [PATCH v2 2/4] iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs Marcelo Schmitt
@ 2026-05-28 15:51 ` sashiko-bot
0 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-05-28 15:51 UTC (permalink / raw)
To: Marcelo Schmitt; +Cc: conor+dt, devicetree, robh
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] The driver fails to right-shift >16-bit ADC data read via SPI into a 32-bit word, corrupting both the raw value and its sign extension.
- [Medium] The Kconfig specifies a spurious dependency on `PWM`, which allows the driver to compile without `GPIOLIB` and silently fail to trigger ADC conversions in direct mode.
- [Medium] The driver explicitly forces `bits_per_word` to 32 or 16, which arbitrarily restricts compatibility with many SPI controllers.
- [Low] Using the `IIO_DEV_ACQUIRE_DIRECT_MODE` cleanup macro inside a `switch` case without a block scope causes a compiler error.
--
commit ffe34f72dbe517b28d007ae50c98ce8f89d8b64c
Author: Marcelo Schmitt <marcelo.schmitt@analog.com>
iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs
This commit adds support for the Analog Devices LTC2378-20 and similar
analog-to-digital converters to the IIO subsystem.
> diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
> --- a/drivers/iio/adc/Kconfig
> +++ b/drivers/iio/adc/Kconfig
> @@ -939,6 +939,18 @@ config LTC2309
>
> +config LTC2378
> + tristate "Analog Devices LTC2378 ADC driver"
> + depends on SPI
> + depends on GPIOLIB || PWM
[Severity: Medium]
Does this spurious dependency on PWM allow the driver to compile without
GPIOLIB?
If GPIOLIB is disabled, devm_gpiod_get_optional() will return NULL. When
ltc2378_convert_and_acquire() subsequently calls gpiod_set_value_cansleep(),
it will silently do nothing.
Will this prevent the ADC conversions from being initiated in direct mode,
causing the SPI transfer to silently read stale data or zeroes?
[ ... ]
> diff --git a/drivers/iio/adc/ltc2378.c b/drivers/iio/adc/ltc2378.c
> --- /dev/null
> +++ b/drivers/iio/adc/ltc2378.c
[ ... ]
> +static int ltc2378_channel_single_read(const struct iio_chan_spec *chan,
> + struct ltc2378_state *st, int *val)
> +{
> + const struct iio_scan_type *scan_type = &chan->scan_type;
> + u32 sample;
> + int ret;
> +
> + ret = ltc2378_convert_and_acquire(st);
> + if (ret)
> + return ret;
> +
> + if (scan_type->realbits > 16)
> + sample = st->scan.data.sample_buf32;
> + else
> + sample = st->scan.data.sample_buf16;
> +
> + if (scan_type->format == IIO_SCAN_FORMAT_SIGNED_INT)
> + *val = sign_extend32(sample, scan_type->realbits - 1);
[Severity: High]
Are we missing a right-shift for >16-bit ADCs?
When bits_per_word is set to 32, the SPI controller clocks in 32 bits
MSB-first and stores them in CPU native endianness. The valid 18 or 20 bits
will occupy the upper bits of the 32-bit word, leaving trailing zeroes.
Doesn't passing this unshifted sample directly to sign_extend32() evaluate
the wrong bit for the sign extension, and also magnify the returned value
by a factor of 2^14?
Should we also populate scan_type.shift in the probe function so userspace
buffer consumers don't end up reading unshifted garbage?
> + else
> + *val = sample;
> +
> + return 0;
> +}
> +
> +static int ltc2378_read_raw(struct iio_dev *indio_dev,
> + const struct iio_chan_spec *chan,
> + int *val, int *val2, long info)
> +{
> + struct ltc2378_state *st = iio_priv(indio_dev);
> + int ret;
> +
> + switch (info) {
> + case IIO_CHAN_INFO_RAW:
> + IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
[Severity: Low]
Does this missing block scope cause a compiler error?
The IIO_DEV_ACQUIRE_DIRECT_MODE macro expands to a cleanup variable
declaration. Because the switch case lacks an enclosing block, jumping to
case IIO_CHAN_INFO_SCALE skips the variable's initialization, which compilers
like GCC reject.
While I noticed this is fixed in a later patch in the series, doesn't this
break git bisectability by causing this specific commit to fail compilation?
> + if (IIO_DEV_ACQUIRE_FAILED(claim))
> + return -EBUSY;
> +
> + ret = ltc2378_channel_single_read(chan, st, val);
[ ... ]
> +static int ltc2378_probe(struct spi_device *spi)
> +{
[ ... ]
> + st->chans[0].scan_type.realbits = st->info->resolution;
> + st->chans[0].scan_type.storagebits = st->info->resolution > 16 ? 32 : 16;
> +
> + st->xfer.rx_buf = &st->scan.data;
> + st->xfer.len = BITS_TO_BYTES(st->chans[0].scan_type.storagebits);
> + st->xfer.bits_per_word = st->info->resolution > 16 ? 32 : 16;
[Severity: Medium]
Does forcing bits_per_word to 32 or 16 arbitrarily restrict compatibility
with standard SPI controllers?
Some standard SPI controllers (like the bcm2835 SPI) only support 8-bit
transfers. When 32-bit or 16-bit words are requested, the SPI core's
__spi_validate() checks the controller capabilities and fails the transfer
with -EINVAL.
Would it be safer to use standard 8-bit transfers and reconstruct the word
using get_unaligned_be32()?
> +
> + indio_dev->channels = st->chans;
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1779976379.git.marcelo.schmitt@analog.com?part=2
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 3/4] iio: adc: ltc2378: Enable high-speed data capture
2026-05-28 15:04 ` [PATCH v2 3/4] iio: adc: ltc2378: Enable high-speed data capture Marcelo Schmitt
@ 2026-05-28 16:39 ` sashiko-bot
0 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-05-28 16:39 UTC (permalink / raw)
To: Marcelo Schmitt; +Cc: robh, conor+dt, devicetree
Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] LTC2378_OFFLOAD_BUFFER incorrectly forces tristate dependencies to be built-in and fails to propagate constraints, causing Kconfig warnings and linker errors.
- [High] Incorrect continuation conditions in hardware configuration loops cause infinite loops or unconditional failures at high sample rates.
- [High] Fallback stub functions in the header contain syntax errors and lack static inline specifiers.
- [High] Direct read SPI transfers execute unintended 32-bit transactions for 16-bit ADCs when offload is supported.
- [High] Uninitialized kernel stack memory is leaked to user-space via the IIO sysfs interface.
- [High] `ltc2378_read_avail` fails to populate the `length` parameter for `IIO_AVAIL_RANGE`, leading to undefined behavior.
- [Medium] dev_err_probe is used to log an error when an optional SPI offload resource is unavailable, polluting kernel logs with false positives.
--
commit 1c9aa766f95e380ea9e11f4b2b8e9cbf0718b8f9
Author: Marcelo Schmitt <marcelo.schmitt@analog.com>
Subject: iio: adc: ltc2378: Enable high-speed data capture
Make use of SPI transfer offloading to speed up data capture, enabling data
acquisition at faster sample rates (up to 2 MSPS).
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
[ ... ]
> +
> +config LTC2378_OFFLOAD_BUFFER
> + bool
> + select SPI_OFFLOAD_TRIGGER_PWM
> + select IIO_BUFFER_DMA
> + select IIO_BUFFER_DMAENGINE
> + help
> + Say yes here to build support for high speed data capture with LTC2378
[Severity: High]
Should LTC2378_OFFLOAD_BUFFER be a tristate or use depends on instead of
select? Since it is a bool, unconditionally selecting tristate options like
IIO_BUFFER_DMA can force them to be compiled as built-in even when the parent
module is built as a module. Selecting them without inheriting their
dependencies can also cause Kconfig warnings.
diff --git a/drivers/iio/adc/ltc2378-offload-buffer.c b/drivers/iio/adc/ltc2378-offload-buffer.c
--- /dev/null
+++ b/drivers/iio/adc/ltc2378-offload-buffer.c
[ ... ]
> +static int ltc2378_update_conversion_rate(struct ltc2378_state *st, int freq_Hz)
> +{
[ ... ]
> + do {
> + cnv_wf.duty_length_ns = target;
> + ret = pwm_round_waveform_might_sleep(st->cnv_trigger, &cnv_wf);
> + if (ret)
> + return ret;
> + target += 10; /* Increment by PWM duty cycle period */
> + } while (cnv_wf.duty_length_ns < LTC2378_TCNV_HIGH_NS || count++ < 100);
[Severity: High]
Does this loop condition evaluate correctly? Due to short-circuit evaluation
in C, if the left side of the || is true, count++ is never executed, which
might lead to an infinite loop if the PWM hardware caps the value. If the left
side is false, the loop will run exactly 100 more times, artificially
inflating the target.
[ ... ]
> + do {
> + config->periodic.offset_ns = offload_offset_ns;
> + ret = spi_offload_trigger_validate(st->offload_trigger, config);
> + if (ret)
> + return ret;
> + offload_offset_ns += 10;
> + } while (config->periodic.offset_ns < min_read_offset || count++ < 100);
[Severity: High]
This loop condition has the same short-circuit evaluation issue as above.
[ ... ]
> +int ltc2378_read_avail(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
> + const int **vals, int *type, int *length, long mask)
> +{
> + struct ltc2378_state *st = iio_priv(indio_dev);
> +
> + switch (mask) {
> + case IIO_CHAN_INFO_SAMP_FREQ:
> + *vals = st->sample_freq_range;
> + *type = IIO_VAL_INT;
> + return IIO_AVAIL_RANGE;
[Severity: High]
Does ltc2378_read_avail need to set the *length pointer here? The IIO core
expects the driver to populate it with the number of elements in the returned
array. Leaving it unset might lead to out-of-bounds memory accesses when the
core processes the range.
[ ... ]
> +int ltc2378_offload_buffer_setup(struct iio_dev *indio_dev, struct spi_device *spi)
> +{
> + struct ltc2378_state *st = iio_priv(indio_dev);
> + struct device *dev = &spi->dev;
> + int ret;
> +
> + st->offload = devm_spi_offload_get(dev, spi, <c2378_offload_config);
> + ret = PTR_ERR_OR_ZERO(st->offload);
> + if (ret)
> + return dev_err_probe(dev, ret, "failed to get offload\n");
[Severity: Medium]
Since SPI offload is an optional feature, if devm_spi_offload_get() returns
-ENODEV, will dev_err_probe() log a false positive error message to the
system log?
diff --git a/drivers/iio/adc/ltc2378.c b/drivers/iio/adc/ltc2378.c
--- a/drivers/iio/adc/ltc2378.c
+++ b/drivers/iio/adc/ltc2378.c
[ ... ]
> @@ -261,10 +314,35 @@ static int ltc2378_probe(struct spi_device *spi)
> st->chans[0].type = IIO_VOLTAGE;
> st->chans[0].info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
> BIT(IIO_CHAN_INFO_SCALE);
> - st->chans[0].scan_type.format = st->info->twos_comp ? IIO_SCAN_FORMAT_SIGNED_INT :
> - IIO_SCAN_FORMAT_UNSIGNED_INT;
> - st->chans[0].scan_type.realbits = st->info->resolution;
> - st->chans[0].scan_type.storagebits = st->info->resolution > 16 ? 32 : 16;
> +
> + struct iio_scan_type ltc2378_scan;
> +
> + ret = ltc2378_offload_buffer_setup(indio_dev, spi);
[Severity: High]
Could this leak uninitialized stack memory to user-space? The ltc2378_scan
structure is allocated on the stack and only partially initialized before
being assigned to the IIO channel. Uninitialized fields and padding might
retain garbage data that gets exposed via sysfs attributes.
[ ... ]
> + st->chans[0].scan_type = ltc2378_scan;
>
> st->xfer.rx_buf = &st->scan.data;
> st->xfer.len = BITS_TO_BYTES(st->chans[0].scan_type.storagebits);
> st->xfer.bits_per_word = st->info->resolution > 16 ? 32 : 16;
[Severity: High]
When SPI offload is available, storagebits is forced to 32, meaning
st->xfer.len is set to 4 bytes. Since xfer.bits_per_word remains 16 for a
16-bit ADC, could this cause non-offloaded direct reads to execute two
16-bit word reads per transaction, violating the SPI hardware protocol?
diff --git a/drivers/iio/adc/ltc2378.h b/drivers/iio/adc/ltc2378.h
--- a/drivers/iio/adc/ltc2378.h
+++ b/drivers/iio/adc/ltc2378.h
[ ... ]
> +#else /* CONFIG_IIO_LTC2378_LIB_OFFLOAD_BUFFER */
> +
> +#define LTC2378_WRITE_RAW_PTR (NULL)
> +
> +#define LTC2378_READ_AVAIL_PTR (NULL)
> +
> +static inline int ltc2378_offload_buffer_setup(struct iio_dev *indio_dev,
> + struct spi_device *spi)
> +{
> + return -ENODEV;
> +}
> +
> +int ltc2378_get_sampling_frequency(struct ltc2378_state *st, int *val)
> +{
> + return -EOPNOTSUPP;
> +}
> +
> +int ltc2378_set_sampling_frequency(struct ltc2378_state *st, int freq_Hz);
> +{
> + return -EOPNOTSUPP;
> +}
> +
> +int ltc2378_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
> + int val, int val2, long mask)
> +{
> + return -EOPNOTSUPP;
> +}
> +
> +#endif
[Severity: High]
Are there build issues with these fallback functions? The function
ltc2378_set_sampling_frequency has a trailing semicolon after its parameter
list. Additionally, these functions lack the static inline qualifier, which
might cause multiple definition linker errors if the header is included in
multiple files.
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1779976379.git.marcelo.schmitt@analog.com?part=3
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 1/4] dt-bindings: iio: adc: Add ltc2378
2026-05-28 15:03 ` [PATCH v2 1/4] dt-bindings: iio: adc: Add ltc2378 Marcelo Schmitt
@ 2026-05-28 16:55 ` Conor Dooley
0 siblings, 0 replies; 9+ messages in thread
From: Conor Dooley @ 2026-05-28 16:55 UTC (permalink / raw)
To: Marcelo Schmitt
Cc: linux-iio, devicetree, linux-kernel, jic23, nuno.sa,
Michael.Hennerich, dlechner, andy, robh, krzk+dt, conor+dt,
pop.ioan-daniel, marcelo.schmitt1
[-- Attachment #1: Type: text/plain, Size: 545 bytes --]
On Thu, May 28, 2026 at 12:03:16PM -0300, Marcelo Schmitt wrote:
> Document how to describe LTC2378-20 and similar ADCs in device tree.
>
> Signed-off-by: Marcelo Schmitt <marcelo.schmitt@analog.com>
> ---
> Change log v1 -> v2:
> - Use unevaluatedProperties:false with spi-peripheral-props.yaml reference/include.
> - Use unique compatibles (no fallbacks).
Please note why these do not use fallbacks in your commit message.
With that explanation,
Acked-by: Conor Dooley <conor.dooley@microchip.com>
pw-bot: changes-requested
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 4/4] iio: adc: ltc2378: Enable triggered buffer data capture
2026-05-28 15:04 ` [PATCH v2 4/4] iio: adc: ltc2378: Enable triggered buffer " Marcelo Schmitt
@ 2026-05-28 17:21 ` sashiko-bot
0 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-05-28 17:21 UTC (permalink / raw)
To: Marcelo Schmitt; +Cc: conor+dt, devicetree, robh
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The `ltc2378_scan` structure is allocated on the stack but not fully initialized, leaving fields like `endianness`, `shift`, and `repeat` with garbage values. This leads to out-of-bounds array reads in the IIO core, data corruption, and potential information disclosure.
--
commit 9e45014ec9c648942b0872f2dcaabf867779b73e
Author: Marcelo Schmitt <marcelo.schmitt@analog.com>
iio: adc: ltc2378: Enable triggered buffer data capture
This commit adds support for triggered buffer data captures to the
LTC2378 ADC driver. It implements a trigger handler and configures the
scan type for both SPI offload and fallback triggered buffer cases.
> diff --git a/drivers/iio/adc/ltc2378.c b/drivers/iio/adc/ltc2378.c
> index 6b01d8e96cc60..514645b847f0b 100644
> --- a/drivers/iio/adc/ltc2378.c
> +++ b/drivers/iio/adc/ltc2378.c
[ ... ]
> @@ -314,17 +337,27 @@ static int ltc2378_probe(struct spi_device *spi)
> st->chans[0].type = IIO_VOLTAGE;
> st->chans[0].info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
> BIT(IIO_CHAN_INFO_SCALE);
> + st->chans[0].scan_index = 0;
>
> struct iio_scan_type ltc2378_scan;
>
> ret = ltc2378_offload_buffer_setup(indio_dev, spi);
> if (ret == -ENODEV) {
> /* SPI offloading is unavailable. Fall back to triggered buffer. */
> - dev_dbg(dev, "triggered data capture not supported\n");
> ltc2378_scan.format = st->info->twos_comp ? IIO_SCAN_FORMAT_SIGNED_INT :
> IIO_SCAN_FORMAT_UNSIGNED_INT;
> ltc2378_scan.realbits = st->info->resolution;
> ltc2378_scan.storagebits = st->info->resolution > 16 ? 32 : 16;
[Severity: High]
Does this leave fields in ltc2378_scan uninitialized?
It looks like ltc2378_scan is allocated on the stack but not fully
initialized. Only format, realbits, and storagebits are assigned here,
leaving fields like endianness, shift, and repeat with stack garbage.
When st->chans[0].scan_type is later assigned from this struct, the
uninitialized endianness field could cause an out-of-bounds array read in
iio_show_scan_type() when indexing into iio_endian_prefix.
The garbage repeat value could also result in miscalculated IIO buffer
chunk sizes.
Could ltc2378_scan be zero-initialized to prevent these issues?
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1779976379.git.marcelo.schmitt@analog.com?part=4
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-05-28 17:21 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-28 15:02 [PATCH v2 0/4] iio: adc: Add support for LTC2378 and similar ADCs Marcelo Schmitt
2026-05-28 15:03 ` [PATCH v2 1/4] dt-bindings: iio: adc: Add ltc2378 Marcelo Schmitt
2026-05-28 16:55 ` Conor Dooley
2026-05-28 15:03 ` [PATCH v2 2/4] iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs Marcelo Schmitt
2026-05-28 15:51 ` sashiko-bot
2026-05-28 15:04 ` [PATCH v2 3/4] iio: adc: ltc2378: Enable high-speed data capture Marcelo Schmitt
2026-05-28 16:39 ` sashiko-bot
2026-05-28 15:04 ` [PATCH v2 4/4] iio: adc: ltc2378: Enable triggered buffer " Marcelo Schmitt
2026-05-28 17:21 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox