* [PATCH v11 0/6] iio: adc: ad4691: add driver for AD4691 multichannel SAR ADC family
From: Radu Sabau via B4 Relay @ 2026-05-15 13:31 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau, Conor Dooley
This series adds support for the Analog Devices AD4691 family of
high-speed, low-power multichannel successive approximation register
(SAR) ADCs with an SPI-compatible serial interface.
The family includes:
- AD4691: 16-channel, 500 kSPS
- AD4692: 16-channel, 1 MSPS
- AD4693: 8-channel, 500 kSPS
- AD4694: 8-channel, 1 MSPS
The devices support two operating modes, auto-detected from the device
tree:
- CNV Burst Mode: external PWM drives CNV independently of SPI;
DATA_READY on a GP pin signals end of conversion
- Manual Mode: CNV tied to SPI CS; each SPI transfer reads
the previous conversion result and starts the
next (pipelined N+1 scheme)
A new driver is warranted rather than extending ad4695: the AD4691
data path uses an accumulator-register model — results are read from
AVG_IN registers, with ACC_MASK, ADC_SETUP, DEVICE_SETUP, and
GPIO_MODE registers controlling the sequencer — none of which exist
in AD4695. CNV Burst Mode (PWM drives CNV independently of SPI) and
Manual Mode (pipelined N+1 transfers) also have no equivalent in
AD4695's command-embedded single-cycle protocol.
The series is structured as follows:
1/6 - DT bindings (YAML schema) and MAINTAINERS entry
2/6 - Initial driver: register map via custom regmap callbacks,
IIO read_raw/write_raw, both operating modes, single-channel
reads via internal oscillator (Autonomous Mode)
3/6 - Triggered buffer support: IRQ-driven (DATA_READY on a GP pin
selected via interrupt-names) for CNV Burst Mode; external IIO
trigger for Manual Mode to handle the pipelined N+1 SPI protocol
4/6 - SPI Engine offload support: DMA-backed high-throughput
capture path using the SPI offload subsystem
5/6 - Per-channel oversampling ratio support for CNV Burst Mode
6/6 - Driver documentation (Documentation/iio/ad4691.rst)
Datasheets:
https://www.analog.com/en/products/ad4691.html
https://www.analog.com/en/products/ad4692.html
https://www.analog.com/en/products/ad4693.html
https://www.analog.com/en/products/ad4694.html
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
Changes in v11:
- initial driver: fix commit message — IIO_CHAN_INFO_SAMP_FREQ is
info_mask_separate throughout the series, not info_mask_shared_by_all
- initial driver: readable_reg / volatile_reg: replace open switch ranges
for multi-byte sparse arrays with stride checks; intermediate (unaligned)
addresses are now excluded so debugfs cannot trigger cross-boundary reads
- initial driver: add comment in ad4691_get_sampling_freq noting that
AD4691_OSC_FREQ_REG is non-volatile and served from regcache; no lock
is needed
- triggered buffer: restore .endianness = IIO_BE on AD4691_CHANNEL
scan_type; accidentally dropped in v10
- triggered buffer: add early break in both iio_for_each_active_channel
loops to skip the soft timestamp scan index; prevents out-of-bounds
writes into scan_tx[] and scan_xfers[]
- triggered buffer: fix DMA aliasing in manual mode preenable — set
rx_buf = NULL for the first transfer (pipeline residual) instead of
aliasing it to vals[0] alongside the second transfer
- triggered buffer: add cs_change_delay of 430 ns on channel transfers
to satisfy the minimum CNV high time requirement
- triggered buffer: remove cs_change=1 from the state-reset transfer;
must not be set on the final transfer of a SPI message
- triggered buffer: move enable_irq() from the trigger handler into a
reenable callback on ad4691_trigger_ops, closing the race between
enable_irq and iio_trigger_notify_done; fix reenable return type
(void, not int)
- triggered buffer: use two separate iio_info structs so that
validate_trigger (iio_validate_own_trigger) is enforced only in CNV
burst mode; manual mode must accept external triggers
- triggered buffer: add comment explaining STATE_RESET_ALL sequencing
in CNV burst mode
- triggered buffer: fix STD_SEQ_CONFIG write in both preenable paths —
apply & GENMASK(15, 0) to strip the soft timestamp bit before writing,
matching the existing acc_mask computation
- oversampling: fix commit message — writing oversampling_ratio snaps
target_osc_freq_Hz to preserve integer sampling_frequency read-back;
the two attributes are not orthogonal
- docs: add missing Buffer data format section covering the __be16
software path and the CPU-native offload DMA path
- Link to v10: https://lore.kernel.org/r/20260511-ad4692-multichannel-sar-adc-driver-v10-0-e1fbb1744e38@analog.com
Changes in v10:
- initial driver: depends on REGULATOR || COMPILE_TEST
- triggered buffer: fix vals[] layout — index vals[] with slot counter k,
not channel index i; fixes sparse active_scan_mask producing garbage in
userspace buffer
- triggered buffer: add comment to cnv_burst_buffer_postenable explaining
why sampling_enable()/enable_irq() cannot be called from preenable
- triggered buffer + offload: scan_tx changed from __be16 to u16;
non-offload path uses put_unaligned_be16() (bits_per_word=8); offload
path uses plain native u16 assignments (bits_per_word=16); also fixes
byte-order bug in manual preenable: command byte was in the low byte,
now correctly shifted to the high byte
- oversampling: remove incorrect iio_for_each_active_channel() timestamp
guards; active_scan_mask never includes the timestamp channel
- Link to v9: https://lore.kernel.org/r/20260430-ad4692-multichannel-sar-adc-driver-v9-0-33e439e4fb87@analog.com
Changes in v9:
- devm_regulator_get_enable() → devm_regulator_get_enable_optional() for
vdd-supply. The non-optional variant silently returns a dummy regulator
(ret=0) when the supply is absent from DT, so st->ldo_en was never set
and the internal LDO was never enabled when only ldo-in-supply was provided.
- struct ad4691_channel_info (factoring channels + num_channels out of
struct ad4691_chip_info into a sw_info pointer) is now introduced in
commit 1 instead of commit 2. It is a pure struct cleanup with no
relation to triggered buffers.
- channels and manual_channels fields in struct ad4691_channel_info
are now annotated with __counted_by_ptr(num_channels).
- Link to v8: https://lore.kernel.org/r/20260416-ad4692-multichannel-sar-adc-driver-v8-0-c415bd048fa3@analog.com
Changes in v8:
- dt-bindings: add commit message note explaining why four separate
compatible strings are required (channel count and max rate both
differ between variants);
- initial driver: sizeof(tx) instead of literal 2 in ad4691_reg_read;
U8_MAX/U16_MAX instead of 0xFF/0xFFFF in ad4691_reg_write
- initial driver: extract ad4691_samp_freq_start() helper
- initial driver: fix regulator model — vdd-supply (external 1.8V,
internal LDO disabled) and ldo-in-supply (feeds internal LDO) are
mutually exclusive; add vdd-supply to binding and driver
- initial driver: add comment in ad4691_reset explaining why
devm_reset_control_get_optional_exclusive_deasserted() cannot be
used (datasheet requires ≥300 µs reset pulse)
- initial driver: REF_CTRL and OSC_FREQ_REG: regmap_update_bits /
regmap_assign_bits → regmap_write (reserved bits are 0 at reset)
- initial driver: use dev instead of &spi->dev in devm_iio_device_alloc
- triggered buffer: scan_tx: add __aligned(IIO_DMA_MINALIGN);
scan struct: IIO_DECLARE_DMA_BUFFER_WITH_TS(__be16, vals, 16)
- triggered buffer: full memset of scan_xfers and scan_tx in both
preenable functions; move buffer-dma.h / buffer-dmaengine.h to
commit 4; spi_optimize_message fail path: return ret directly in
cnv_burst_buffer_preenable; reduce devm_iio_trigger_alloc wrapping
- SPI offload: drop AD4691_OFFLOAD_BITS_PER_WORD; use local
bpw = channels[0].scan_type.realbits; num_channels: ARRAY_SIZE - 1
- SPI offload: rename offload_state.spi → .offload; remove spurious
STD_SEQ_CONFIG write from cnv_burst_offload predisable; extract
local acc_mask variable for ACC_MASK_REG write
- SPI offload: sampling_frequency_store: IIO_DEV_ACQUIRE_DIRECT_MODE
for auto-release; remove explicit iio_device_release_direct calls
- oversampling: in_voltageN_sampling_frequency now represents the
effective output rate (osc_freq / osr[N]), matching ad4695
- oversampling: in_voltageN_sampling_frequency_available computed
dynamically from the channel's current OSR; only oscillator entries
divisible by osr[N] shown as effective rates; list becomes sparser
as OSR increases, capping at max_rate / osr[N]
- oversampling: writing sampling_frequency snaps down to the largest
oscillator entry ≤ freq * osr[N] that is divisible by osr[N],
guaranteeing integer read-back; writing oversampling_ratio stores
the new depth only — target_osc_freq_Hz unchanged; the two
attributes are orthogonal
- oversampling: ad4691_write_osc_freq() called from
ad4691_enter_conversion_mode() after manual mode early return,
covering all CNV burst buffer enable paths
- oversampling: (osr + 1) oscillator period wait in single_shot_read
(osr for accumulation, +1 pipeline margin)
- docs: new commit — Documentation/iio/ad4691.rst, userspace-facing
only; oversampling section describes effective-rate SF semantics;
LDO supply section corrected (vdd-supply vs ldo-in-supply)
- Link to v7: https://lore.kernel.org/r/20260409-ad4692-multichannel-sar-adc-driver-v7-0-be375d4df2c5@analog.com
Changes in v7:
- Fix CNV burst triggered-buffer preenable: the state-reset value
transfer had tx_buf assigned the return value of cpu_to_be16()
(an integer) instead of a pointer to a buffer, which would cause
a kernel oops on buffer enable; extend scan_tx[] from 17 to 18
entries to hold the extra slot and fix the pointer assignment
- Extend memset in ad4691_cnv_burst_buffer_preenable to cover the
two state-reset transfer slots (previously left with stale data
across buffer enable/disable cycles if the active channel count
changed)
- Fix format specifier %u -> %lu for NSEC_PER_SEC in
sampling_frequency_show (NSEC_PER_SEC is unsigned long on 32-bit)
- Fix missing iio_device_release_direct() on spi_offload_trigger_-
validate() error path in sampling_frequency_store
- Correct SPI offload commit message: the implementation uses 16-bit
SPI frames (bits_per_word=16, len=2), not 32-bit; storagebits
remains 16 (not promoted to 32); there is no shift=16 for manual
mode; ad4691_manual_channels[] hides IIO_CHAN_INFO_OVERSAMPLING_-
RATIO (not applicable in manual mode), not encodes shift=16
- Link to v6: https://lore.kernel.org/r/20260403-ad4692-multichannel-sar-adc-driver-v6-0-fa2a01a57c4e@analog.com
Changes in v6:
- Replace device.h with dev_printk.h + device/devres.h; add array_size.h
- Rename osc_freqs[] → osc_freqs_Hz[] with explicit [0xN] index designators
- Move loop variable into for() declaration in set_sampling_freq
- Convert multi-line block comment to single-line in single_shot_read
- Replace (u16)~ cast with ~BIT() & GENMASK(15, 0) for ACC_MASK_REG write;
GENMASK(15, 0) is still needed, otherwise maximum value condition line
in reg_write() would fail.
- Extract osc_idx/period_us temporaries in single_shot_read; add comment
- Use devm_regulator_bulk_get_enable() for avdd + vio supplies
- Reformat reset_gpio_probe() comment; remove (GPIOD_OUT_HIGH) detail
- Extract REF_CTRL value into temporary before regmap_update_bits
- Use regmap_assign_bits for OSC_FREQ_REG in config
- Remove ad4691_free_scan_bufs NULL assignments; they are not checked.
- Replace indio_dev->masklength with iio_get_masklength() throughout
- Fix spi_optimize_message error path to use goto err in preenable
- Add iio_buffer_enabled() guard in sampling_frequency_store and
set_oversampling_ratio
- Move ad4691_gpio_setup call from ad4691_config into
setup_triggered_buffer after IRQ lookup; remove duplicate
fwnode_irq_get_byname loop
- Replace oversampling ratio search loop with is_power_of_2 + ilog2
- Link to v5: https://lore.kernel.org/r/20260327-ad4692-multichannel-sar-adc-driver-v5-0-11f789de47b8@analog.com
Changes in v5:
- Reorder datasheets numerically
- Fix interrupt-names: use enum with minItems/maxItems
- Remove if/then block requiring interrupts — driver detail, not hardware constraint
- Remove redundant .shift = 0 from channel macro
- Write max_rate comparison as 1 * HZ_PER_MHZ
- Invert set_sampling_freq loop to use continue
- Fix fsleep() line break; remove blank line in read_raw
- Reorder supply init: vio immediately after avdd
- Move comment rewrites and OSC_FREQ_REG condition into the base driver patch
- Add bit-15 READ comment in reg_read
- Rewrite ldo-in handling with cleaner if/else-if pattern
- Drop redundant refbuf_en = false; invert if (!rst) in reset
- Drop reset_control_assert() — GPIO already asserted at probe
- Use regmap_update_bits/assign_bits in config
- Remove tab-column alignment of state struct members
- Declare osc_freqs[] as const int, eliminating explicit casts
- Drop obvious AUTONOMOUS mode comment
- Rename ACC_COUNT_LIMIT → ACC_DEPTH_IN to match datasheet
- Use bitmap_weight()/bitmap_read() for active_scan_mask access;
add #include <linux/bitmap.h>
- Fix channel macro line-continuation tab alignment
- Use IIO_CHAN_SOFT_TIMESTAMP(8) for 8-channel variants
- Use aligned_s64 ts in scan struct
- Add comment explaining start-index removal in set_sampling_freq
- Remove trailing comma after NULL in buffer_attrs[]
- Add IRQF_NO_AUTOEN rationale comment
- Remove unreachable manual_mode guards in sampling_frequency_show/store
- Remove st->trig; use indio_dev->trig directly
- Move max_speed_hz param to the offload patch where it is used
- Use DIV_ROUND_UP for CNV period; use compound pwm_state initializer
- Move offload fields into a separately allocated sub-struct
- Build TX words via u8* byte-fill; fixes sparse __be32 warnings
- Add three scan types (NORMAL/OFFLOAD_CNV/OFFLOAD_MANUAL) with
get_current_scan_type; triggered buffer path uses storagebits=16
- Fix IIO_CHAN_INFO_SCALE: use iio_get_current_scan_type() for realbits
- Add MODULE_IMPORT_NS("IIO_DMAENGINE_BUFFER")
- Add Documentation/iio/ad4691.rst
- Link to v4: https://lore.kernel.org/r/20260320-ad4692-multichannel-sar-adc-driver-v4-0-052c1050507a@analog.com
Changes in v4:
- dt-bindings: add avdd-supply (required) and ldo-in-supply (optional);
rename vref-supply → ref-supply, vrefin-supply → refin-supply;
corrected reset-gpios polarity (active-high → active-low); remove
clocks and pwm-names; extend interrupts to up to 4 GP pins with
interrupt-names "gp0".."gp3"; reduce #trigger-source-cells to
const: 1 (GP pin number); add gpio-controller / #gpio-cells = <2>;
drop adi,ad4691.h header; update binding examples
- driver: rename CNV Clock Mode → CNV Burst Mode throughout
- driver: add avdd-supply (required) and ldo-in-supply; track ref vs.
refin supply for REFBUF_EN; set LDO_EN in DEVICE_SETUP when ldo-in
is present; add software reset fallback via SPI_CONFIG_A register
- driver: merge ACC_MASK1_REG / ACC_MASK2_REG into ACC_MASK_REG with
a single ADDR_DESCENDING 16-bit SPI write
- driver: remove clocks usage; set PWM rate directly without ref clock
- driver: rename chip info structs (ad4691_chip_info etc.); rename
*chip → *info in state struct; replace adc_mode enum with manual_mode
bool; replace ktime sampling_period with u32 cnv_period_ns
- driver: move IIO_CHAN_INFO_SAMP_FREQ to info_mask_separate with an
available list for the internal oscillator frequency
- driver: use regcache MAPLE instead of RBTREE
- triggered buffer: derive DATA_READY GP pin from interrupt-names in
firmware ("gp0".."gp3") instead of assuming GP0
- triggered buffer: use regmap_update_bits for DEVICE_SETUP mode toggle
to avoid clobbering LDO_EN when toggling MANUAL_MODE bit
- triggered buffer: split buffer setup ops into separate Manual and
CNV Burst variants (mirrors offload path structure)
- SPI offload: promote channel storagebits from 16 to 32 to match DMA
word size; introduce ad4691_manual_channels[] with shift=16 (data in
upper 16 bits of the 32-bit word); update triggered-buffer paths to
the same layout for consistency
- SPI offload: derive GP pin from trigger-source args[0] instead of
hardcoding GP0; split offload buffer setup ops per mode
- replace put_unaligned_be32() + FIELD_PREP() with cpu_to_be32() and
plain bit-shift ops for SPI offload message construction
- multiple reviewer-requested code style and correctness fixes
(Andy Shevchenko, Nuno Sá, Uwe Kleine-König, David Lechner)
- Link to v3: https://lore.kernel.org/r/20260313-ad4692-multichannel-sar-adc-driver-v3-0-b4d14d81a181@analog.com
Changes in v3:
- Replace GPIO reset handling with reset controller framework
- Replace two regmap_write() calls for ACC_MASK1/ACC_MASK2 with regmap_bulk_write()
- Move conv_us declaration closer to its first use
- Derive spi_device/dev from regmap instead of storing st->spi
- ad4691_trigger_handler(): use guard(mutex)() and iio_for_each_active_channel()
- ad4691_setup_triggered_buffer(): return -ENOMEM/-ENOENT directly instead of
wrapping in dev_err_probe(); fix fwnode_irq_get() check (irq <= 0 → irq < 0)
- Add GENMASK defines for SPI offload 32-bit message layout; replace manual
bit-shifts with put_unaligned_be32() + FIELD_PREP()
- Use DIV_ROUND_CLOSEST_ULL() instead of div64_u64()
- ad4691_set_sampling_freq(): fix indentation; drop unnecessary else after return
- ad4691_probe(): use PTR_ERR_OR_ZERO() for devm_spi_offload_get()
- Link to v2: https://lore.kernel.org/r/20260310-ad4692-multichannel-sar-adc-driver-v2-0-d9bb8aeb5e17@analog.com
Changes in v2:
- Drop adi,spi-mode DT property; operating mode now auto-detected
from pwms presence (CNV Clock Mode if present, Manual Mode if not)
- Reduce from 5 operating modes to 2 (CNV Clock Mode, Manual Mode);
Autonomous, SPI Burst and CNV Burst modes removed as user-selectable
modes; Autonomous Mode is now the internal idle/single-shot state
- Single-shot read_raw always uses internal oscillator (Autonomous
Mode), independent of the configured buffer mode
- Replace bulk regulator API with devm_regulator_get_enable() and
devm_regulator_get_enable_read_voltage()
- Use guard(mutex) and IIO_DEV_ACQUIRE_DIRECT_MODE scoped helpers
- Replace enum + indexed chip_info array with named chip_info structs
- Remove product_id field and hardware ID check from probe
- Factor IIO_CHAN_INFO_RAW body into ad4691_single_shot_read() helper
- Use fwnode_irq_get(dev_fwnode(dev), 0); drop interrupt-names from
DT binding
- Use devm_clk_get_enabled(dev, NULL); drop clock-names from DT
binding
- Use spi_write_then_read() for DMA-safe register writes
- Use put_unaligned_be16() for SPI header construction
- fsleep() instead of usleep_range() in single-shot path
- storagebits 24->32 for manual-mode channels (uniform DMA layout)
- Collect full scan into vals[16], single iio_push_to_buffers_with_ts()
- Use pf->timestamp instead of iio_get_time_ns() in trigger handler
- Remove IRQF_TRIGGER_FALLING (comes from firmware/DT)
- Fix offload xfer array size ([17]: N channels + 1 state reset)
- Drop third DT binding example per reviewer request
- Link to v1: https://lore.kernel.org/r/20260305-ad4692-multichannel-sar-adc-driver-v1-0-336229a8dcc7@analog.com
---
Radu Sabau (6):
dt-bindings: iio: adc: add AD4691 family
iio: adc: ad4691: add initial driver for AD4691 family
iio: adc: ad4691: add triggered buffer support
iio: adc: ad4691: add SPI offload support
iio: adc: ad4691: add oversampling support
docs: iio: adc: ad4691: add driver documentation
.../devicetree/bindings/iio/adc/adi,ad4691.yaml | 180 ++
Documentation/iio/ad4691.rst | 225 +++
Documentation/iio/index.rst | 1 +
MAINTAINERS | 9 +
drivers/iio/adc/Kconfig | 16 +
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ad4691.c | 2077 ++++++++++++++++++++
7 files changed, 2509 insertions(+)
---
base-commit: 11439c4635edd669ae435eec308f4ab8a0804808
change-id: 20260302-ad4692-multichannel-sar-adc-driver-78e4d44d24b2
Best regards,
--
Radu Sabau <radu.sabau@analog.com>
^ permalink raw reply
* [PATCH v11 1/6] dt-bindings: iio: adc: add AD4691 family
From: Radu Sabau via B4 Relay @ 2026-05-15 13:31 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau, Conor Dooley
In-Reply-To: <20260515-ad4692-multichannel-sar-adc-driver-v11-0-eab27d852ac2@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add DT bindings for the Analog Devices AD4691 family of multichannel
SAR ADCs (AD4691, AD4692, AD4693, AD4694).
The binding describes the hardware connections:
- Power domains: avdd-supply (required), vio-supply, ref-supply or
refin-supply (external reference; the REFIN path enables the
internal reference buffer). Digital core VDD is supplied either
externally via vdd-supply, or generated by the on-chip LDO fed
from ldo-in-supply; the two are mutually exclusive and one must
be present.
- Optional PWM on the CNV pin selects CNV Burst Mode; when absent,
Manual Mode is assumed with CNV tied to SPI CS.
- An optional reset GPIO (reset-gpios) for hardware reset.
- Up to four GP pins (gp0..gp3) usable as interrupt sources,
identified in firmware via interrupt-names "gp0".."gp3".
- gpio-controller with #gpio-cells = <2> for GP pin GPIO usage.
- #trigger-source-cells = <1>: one cell selecting the GP pin number
(0-3) used as the SPI offload trigger source.
Two binding examples are provided: CNV Burst Mode with SPI offload
(DMA data acquisition driven by DATA_READY on a GP pin), and Manual
Mode for CPU-driven triggered-buffer or single-shot capture.
The four variants are not compatible with each other: AD4691/AD4692 have
16 analog input channels while AD4693/AD4694 have 8, and AD4691/AD4693
top out at 500 kSPS while AD4692/AD4694 reach 1 MSPS. These differences
in channel count and maximum sample rate require distinct compatible
strings so the driver can select the correct channel configuration and
rate limits.
Acked-by: Conor Dooley <conor.dooley@microchip.com>
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
.../devicetree/bindings/iio/adc/adi,ad4691.yaml | 180 +++++++++++++++++++++
MAINTAINERS | 7 +
2 files changed, 187 insertions(+)
diff --git a/Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml b/Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
new file mode 100644
index 000000000000..af28a0c1cfa9
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
@@ -0,0 +1,180 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/iio/adc/adi,ad4691.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Analog Devices AD4691 Family Multichannel SAR ADCs
+
+maintainers:
+ - Radu Sabau <radu.sabau@analog.com>
+
+description: |
+ The AD4691 family are high-speed, low-power, multichannel successive
+ approximation register (SAR) analog-to-digital converters (ADCs) with
+ an SPI-compatible serial interface. The ADC supports CNV Burst Mode,
+ where an external PWM drives the CNV pin, and Manual Mode, where CNV
+ is directly tied to the SPI chip-select.
+
+ Datasheets:
+ * https://www.analog.com/en/products/ad4691.html
+ * https://www.analog.com/en/products/ad4692.html
+ * https://www.analog.com/en/products/ad4693.html
+ * https://www.analog.com/en/products/ad4694.html
+
+$ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+ compatible:
+ enum:
+ - adi,ad4691
+ - adi,ad4692
+ - adi,ad4693
+ - adi,ad4694
+
+ reg:
+ maxItems: 1
+
+ spi-max-frequency:
+ maximum: 40000000
+
+ spi-cpol: true
+ spi-cpha: true
+
+ avdd-supply:
+ description: Analog power supply (4.5V to 5.5V).
+
+ vdd-supply:
+ description:
+ External 1.8V digital core supply. When present, the internal LDO is
+ disabled (LDO_EN = 0). Mutually exclusive with ldo-in-supply.
+
+ ldo-in-supply:
+ description:
+ LDO input supply (2.4V to 5.5V). When present and vdd-supply is absent,
+ the internal LDO generates 1.8V VDD from this input (LDO_EN = 1).
+ Mutually exclusive with vdd-supply.
+
+ vio-supply:
+ description: I/O voltage supply (1.71V to 1.89V or VDD).
+
+ ref-supply:
+ description: External reference voltage supply (2.4V to 5.25V).
+
+ refin-supply:
+ description: Internal reference buffer input supply.
+
+ reset-gpios:
+ description:
+ GPIO line controlling the hardware reset pin (active-low).
+ maxItems: 1
+
+ pwms:
+ description:
+ PWM connected to the CNV pin. When present, selects CNV Burst Mode where
+ the PWM drives the conversion rate. When absent, Manual Mode is used
+ (CNV tied to SPI CS).
+ maxItems: 1
+
+ interrupts:
+ description:
+ Interrupt lines connected to the ADC GP pins. Each GP pin can be
+ physically wired to an interrupt-capable input on the SoC.
+ maxItems: 4
+
+ interrupt-names:
+ description: Names of the interrupt lines, matching the GP pin names.
+ minItems: 1
+ maxItems: 4
+ items:
+ enum:
+ - gp0
+ - gp1
+ - gp2
+ - gp3
+
+ gpio-controller: true
+
+ '#gpio-cells':
+ const: 2
+
+ '#trigger-source-cells':
+ description:
+ This node can act as a trigger source. The single cell in a consumer
+ reference specifies the GP pin number (0-3) used as the trigger output.
+ const: 1
+
+required:
+ - compatible
+ - reg
+ - avdd-supply
+ - vio-supply
+
+allOf:
+ # vdd-supply and ldo-in-supply are mutually exclusive, one is required:
+ # either an external 1.8V VDD is provided or the internal LDO is fed from
+ # ldo-in-supply to generate VDD.
+ - oneOf:
+ - required:
+ - vdd-supply
+ - required:
+ - ldo-in-supply
+ # ref-supply and refin-supply are mutually exclusive, one is required
+ - oneOf:
+ - required:
+ - ref-supply
+ - required:
+ - refin-supply
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ /* AD4692 in CNV Burst Mode with SPI offload */
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ adc@0 {
+ compatible = "adi,ad4692";
+ reg = <0>;
+ spi-cpol;
+ spi-cpha;
+ spi-max-frequency = <40000000>;
+
+ avdd-supply = <&avdd_supply>;
+ ldo-in-supply = <&avdd_supply>;
+ vio-supply = <&vio_supply>;
+ ref-supply = <&ref_5v>;
+
+ reset-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;
+
+ pwms = <&pwm_gen 0 0>;
+
+ #trigger-source-cells = <1>;
+ };
+ };
+
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ /* AD4692 in Manual Mode (CNV tied to SPI CS) */
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ adc@0 {
+ compatible = "adi,ad4692";
+ reg = <0>;
+ spi-cpol;
+ spi-cpha;
+ spi-max-frequency = <31250000>;
+
+ avdd-supply = <&avdd_supply>;
+ ldo-in-supply = <&avdd_supply>;
+ vio-supply = <&vio_supply>;
+ refin-supply = <&refin_supply>;
+
+ reset-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 61bf550fd37c..438ca850fa1c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1484,6 +1484,13 @@ W: https://ez.analog.com/linux-software-drivers
F: Documentation/devicetree/bindings/iio/adc/adi,ad4170-4.yaml
F: drivers/iio/adc/ad4170-4.c
+ANALOG DEVICES INC AD4691 DRIVER
+M: Radu Sabau <radu.sabau@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,ad4691.yaml
+
ANALOG DEVICES INC AD4695 DRIVER
M: Michael Hennerich <michael.hennerich@analog.com>
M: Nuno Sá <nuno.sa@analog.com>
--
2.43.0
^ permalink raw reply related
* [PATCH v11 2/6] iio: adc: ad4691: add initial driver for AD4691 family
From: Radu Sabau via B4 Relay @ 2026-05-15 13:31 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260515-ad4692-multichannel-sar-adc-driver-v11-0-eab27d852ac2@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add support for the Analog Devices AD4691 family of high-speed,
low-power multichannel SAR ADCs: AD4691 (16-ch, 500 kSPS),
AD4692 (16-ch, 1 MSPS), AD4693 (8-ch, 500 kSPS) and
AD4694 (8-ch, 1 MSPS).
The driver implements a custom regmap layer over raw SPI to handle the
device's mixed 1/2/3/4-byte register widths and uses the standard IIO
read_raw/write_raw interface for single-channel reads.
The chip idles in Autonomous Mode so that single-shot read_raw can use
the internal oscillator without disturbing the hardware configuration.
Three voltage supply domains are managed: avdd (required), vio, and a
reference supply on either the REF pin (ref-supply, external buffer)
or the REFIN pin (refin-supply, uses the on-chip reference buffer;
REFBUF_EN is set accordingly). Hardware reset is performed via
the reset controller framework; a software reset through SPI_CONFIG_A
is used as fallback when no hardware reset is available.
Accumulator channel masking for single-shot reads uses ACC_MASK_REG via
an ADDR_DESCENDING SPI write, which covers both mask bytes in a single
16-bit transfer.
IIO_CHAN_INFO_SAMP_FREQ is exposed as info_mask_separate. The oscillator
is shared hardware — writing any channel's sampling_frequency attribute
sets it for all others — but per-channel attributes are used throughout
the series to avoid an ABI change when per-channel oversampling ratios
are introduced in a later commit, at which point the effective output
rate (osc_freq / osr[N]) becomes genuinely per-channel.
Reviewed-by: David Lechner <dlechner@baylibre.com>
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
MAINTAINERS | 1 +
drivers/iio/adc/Kconfig | 12 +
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ad4691.c | 756 +++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 770 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 438ca850fa1c..24e4502b8292 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1490,6 +1490,7 @@ L: linux-iio@vger.kernel.org
S: Supported
W: https://ez.analog.com/linux-software-drivers
F: Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
+F: drivers/iio/adc/ad4691.c
ANALOG DEVICES INC AD4695 DRIVER
M: Michael Hennerich <michael.hennerich@analog.com>
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 60038ae8dfc4..5e601a87e5f3 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -139,6 +139,18 @@ config AD4170_4
To compile this driver as a module, choose M here: the module will be
called ad4170-4.
+config AD4691
+ tristate "Analog Devices AD4691 Family ADC Driver"
+ depends on SPI
+ depends on REGULATOR || COMPILE_TEST
+ select REGMAP
+ help
+ Say yes here to build support for Analog Devices AD4691 Family MuxSAR
+ SPI analog to digital converters (ADC).
+
+ To compile this driver as a module, choose M here: the module will be
+ called ad4691.
+
config AD4695
tristate "Analog Device AD4695 ADC Driver"
depends on SPI
diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile
index c76550415ff1..4ac1ea09d773 100644
--- a/drivers/iio/adc/Makefile
+++ b/drivers/iio/adc/Makefile
@@ -16,6 +16,7 @@ obj-$(CONFIG_AD4080) += ad4080.o
obj-$(CONFIG_AD4130) += ad4130.o
obj-$(CONFIG_AD4134) += ad4134.o
obj-$(CONFIG_AD4170_4) += ad4170-4.o
+obj-$(CONFIG_AD4691) += ad4691.o
obj-$(CONFIG_AD4695) += ad4695.o
obj-$(CONFIG_AD4851) += ad4851.o
obj-$(CONFIG_AD7091R) += ad7091r-base.o
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
new file mode 100644
index 000000000000..ba77e1bfef16
--- /dev/null
+++ b/drivers/iio/adc/ad4691.c
@@ -0,0 +1,756 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2024-2026 Analog Devices, Inc.
+ * Author: Radu Sabau <radu.sabau@analog.com>
+ */
+#include <linux/array_size.h>
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/cleanup.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device/devres.h>
+#include <linux/err.h>
+#include <linux/limits.h>
+#include <linux/math.h>
+#include <linux/module.h>
+#include <linux/mod_devicetable.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/reset.h>
+#include <linux/spi/spi.h>
+#include <linux/units.h>
+#include <linux/unaligned.h>
+
+#include <linux/iio/iio.h>
+
+#define AD4691_VREF_uV_MIN 2400000
+#define AD4691_VREF_uV_MAX 5250000
+#define AD4691_VREF_2P5_uV_MAX 2750000
+#define AD4691_VREF_3P0_uV_MAX 3250000
+#define AD4691_VREF_3P3_uV_MAX 3750000
+#define AD4691_VREF_4P096_uV_MAX 4500000
+
+#define AD4691_SPI_CONFIG_A_REG 0x000
+#define AD4691_SW_RESET (BIT(7) | BIT(0))
+
+#define AD4691_STATUS_REG 0x014
+#define AD4691_CLAMP_STATUS1_REG 0x01A
+#define AD4691_CLAMP_STATUS2_REG 0x01B
+#define AD4691_DEVICE_SETUP 0x020
+#define AD4691_LDO_EN BIT(4)
+#define AD4691_REF_CTRL 0x021
+#define AD4691_REF_CTRL_MASK GENMASK(4, 2)
+#define AD4691_REFBUF_EN BIT(0)
+#define AD4691_OSC_FREQ_REG 0x023
+#define AD4691_OSC_FREQ_MASK GENMASK(3, 0)
+#define AD4691_STD_SEQ_CONFIG 0x025
+#define AD4691_SPARE_CONTROL 0x02A
+
+#define AD4691_OSC_EN_REG 0x180
+#define AD4691_STATE_RESET_REG 0x181
+#define AD4691_STATE_RESET_ALL 0x01
+#define AD4691_ADC_SETUP 0x182
+#define AD4691_ADC_MODE_MASK GENMASK(1, 0)
+#define AD4691_AUTONOMOUS_MODE 0x02
+/*
+ * ACC_MASK_REG covers both mask bytes via ADDR_DESCENDING SPI: writing a
+ * 16-bit BE value to 0x185 auto-decrements to 0x184 for the second byte.
+ */
+#define AD4691_ACC_MASK_REG 0x185
+#define AD4691_ACC_DEPTH_IN(n) (0x186 + (n))
+#define AD4691_GPIO_MODE1_REG 0x196
+#define AD4691_GPIO_MODE2_REG 0x197
+#define AD4691_GPIO_READ 0x1A0
+#define AD4691_ACC_STATUS_FULL1_REG 0x1B0
+#define AD4691_ACC_STATUS_FULL2_REG 0x1B1
+#define AD4691_ACC_STATUS_OVERRUN1_REG 0x1B2
+#define AD4691_ACC_STATUS_OVERRUN2_REG 0x1B3
+#define AD4691_ACC_STATUS_SAT1_REG 0x1B4
+#define AD4691_ACC_STATUS_SAT2_REG 0x1BE
+#define AD4691_ACC_SAT_OVR_REG(n) (0x1C0 + (n))
+#define AD4691_AVG_IN(n) (0x201 + (2 * (n)))
+#define AD4691_AVG_STS_IN(n) (0x222 + (3 * (n)))
+#define AD4691_ACC_IN(n) (0x252 + (3 * (n)))
+#define AD4691_ACC_STS_DATA(n) (0x283 + (4 * (n)))
+
+static const char * const ad4691_supplies[] = { "avdd", "vio" };
+
+enum ad4691_ref_ctrl {
+ AD4691_VREF_2P5 = 0,
+ AD4691_VREF_3P0 = 1,
+ AD4691_VREF_3P3 = 2,
+ AD4691_VREF_4P096 = 3,
+ AD4691_VREF_5P0 = 4,
+};
+
+struct ad4691_channel_info {
+ const struct iio_chan_spec *channels __counted_by_ptr(num_channels);
+ unsigned int num_channels;
+};
+
+struct ad4691_chip_info {
+ const char *name;
+ unsigned int max_rate;
+ const struct ad4691_channel_info *sw_info;
+};
+
+#define AD4691_CHANNEL(ch) \
+ { \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) \
+ | BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate_available = \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
+ .channel = ch, \
+ .scan_index = ch, \
+ .scan_type = { \
+ .sign = 'u', \
+ .realbits = 16, \
+ .storagebits = 16, \
+ }, \
+ }
+
+static const struct iio_chan_spec ad4691_channels[] = {
+ AD4691_CHANNEL(0),
+ AD4691_CHANNEL(1),
+ AD4691_CHANNEL(2),
+ AD4691_CHANNEL(3),
+ AD4691_CHANNEL(4),
+ AD4691_CHANNEL(5),
+ AD4691_CHANNEL(6),
+ AD4691_CHANNEL(7),
+ AD4691_CHANNEL(8),
+ AD4691_CHANNEL(9),
+ AD4691_CHANNEL(10),
+ AD4691_CHANNEL(11),
+ AD4691_CHANNEL(12),
+ AD4691_CHANNEL(13),
+ AD4691_CHANNEL(14),
+ AD4691_CHANNEL(15),
+};
+
+static const struct iio_chan_spec ad4693_channels[] = {
+ AD4691_CHANNEL(0),
+ AD4691_CHANNEL(1),
+ AD4691_CHANNEL(2),
+ AD4691_CHANNEL(3),
+ AD4691_CHANNEL(4),
+ AD4691_CHANNEL(5),
+ AD4691_CHANNEL(6),
+ AD4691_CHANNEL(7),
+};
+
+/*
+ * Internal oscillator frequency table. Index is the OSC_FREQ_REG[3:0] value.
+ * Index 0 (1 MHz) is only valid for AD4692/AD4694; AD4691/AD4693 support
+ * up to 500 kHz and use index 1 as their highest valid rate.
+ */
+static const int ad4691_osc_freqs_Hz[] = {
+ [0x0] = 1000000,
+ [0x1] = 500000,
+ [0x2] = 400000,
+ [0x3] = 250000,
+ [0x4] = 200000,
+ [0x5] = 167000,
+ [0x6] = 133000,
+ [0x7] = 125000,
+ [0x8] = 100000,
+ [0x9] = 50000,
+ [0xA] = 25000,
+ [0xB] = 12500,
+ [0xC] = 10000,
+ [0xD] = 5000,
+ [0xE] = 2500,
+ [0xF] = 1250,
+};
+
+static const struct ad4691_channel_info ad4691_sw_info = {
+ .channels = ad4691_channels,
+ .num_channels = ARRAY_SIZE(ad4691_channels),
+};
+
+static const struct ad4691_channel_info ad4693_sw_info = {
+ .channels = ad4693_channels,
+ .num_channels = ARRAY_SIZE(ad4693_channels),
+};
+
+static const struct ad4691_chip_info ad4691_chip_info = {
+ .name = "ad4691",
+ .max_rate = 500 * HZ_PER_KHZ,
+ .sw_info = &ad4691_sw_info,
+};
+
+static const struct ad4691_chip_info ad4692_chip_info = {
+ .name = "ad4692",
+ .max_rate = 1 * HZ_PER_MHZ,
+ .sw_info = &ad4691_sw_info,
+};
+
+static const struct ad4691_chip_info ad4693_chip_info = {
+ .name = "ad4693",
+ .max_rate = 500 * HZ_PER_KHZ,
+ .sw_info = &ad4693_sw_info,
+};
+
+static const struct ad4691_chip_info ad4694_chip_info = {
+ .name = "ad4694",
+ .max_rate = 1 * HZ_PER_MHZ,
+ .sw_info = &ad4693_sw_info,
+};
+
+struct ad4691_state {
+ const struct ad4691_chip_info *info;
+ struct regmap *regmap;
+ int vref_uV;
+ bool refbuf_en;
+ bool ldo_en;
+ /*
+ * Synchronize access to members of the driver state, and ensure
+ * atomicity of consecutive SPI operations.
+ */
+ struct mutex lock;
+};
+
+static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
+{
+ struct spi_device *spi = context;
+ u8 tx[2], rx[4];
+ int ret;
+
+ /* Set bit 15 to mark the operation as READ. */
+ put_unaligned_be16(0x8000 | reg, tx);
+
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_SPARE_CONTROL ... AD4691_ACC_MASK_REG - 1:
+ case AD4691_ACC_MASK_REG + 1 ... AD4691_ACC_SAT_OVR_REG(15):
+ ret = spi_write_then_read(spi, tx, sizeof(tx), rx, 1);
+ if (ret)
+ return ret;
+ *val = rx[0];
+ return 0;
+ case AD4691_ACC_MASK_REG:
+ case AD4691_STD_SEQ_CONFIG:
+ case AD4691_AVG_IN(0) ... AD4691_AVG_IN(15):
+ ret = spi_write_then_read(spi, tx, sizeof(tx), rx, 2);
+ if (ret)
+ return ret;
+ *val = get_unaligned_be16(rx);
+ return 0;
+ case AD4691_AVG_STS_IN(0) ... AD4691_AVG_STS_IN(15):
+ case AD4691_ACC_IN(0) ... AD4691_ACC_IN(15):
+ ret = spi_write_then_read(spi, tx, sizeof(tx), rx, 3);
+ if (ret)
+ return ret;
+ *val = get_unaligned_be24(rx);
+ return 0;
+ case AD4691_ACC_STS_DATA(0) ... AD4691_ACC_STS_DATA(15):
+ ret = spi_write_then_read(spi, tx, sizeof(tx), rx, 4);
+ if (ret)
+ return ret;
+ *val = get_unaligned_be32(rx);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_reg_write(void *context, unsigned int reg, unsigned int val)
+{
+ struct spi_device *spi = context;
+ u8 tx[4];
+
+ put_unaligned_be16(reg, tx);
+
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_SPARE_CONTROL ... AD4691_ACC_MASK_REG - 1:
+ case AD4691_ACC_MASK_REG + 1 ... AD4691_GPIO_MODE2_REG:
+ if (val > U8_MAX)
+ return -EINVAL;
+ tx[2] = val;
+ return spi_write_then_read(spi, tx, 3, NULL, 0);
+ case AD4691_ACC_MASK_REG:
+ case AD4691_STD_SEQ_CONFIG:
+ if (val > U16_MAX)
+ return -EINVAL;
+ put_unaligned_be16(val, &tx[2]);
+ return spi_write_then_read(spi, tx, 4, NULL, 0);
+ default:
+ return -EINVAL;
+ }
+}
+
+static bool ad4691_volatile_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case AD4691_STATUS_REG:
+ case AD4691_CLAMP_STATUS1_REG:
+ case AD4691_CLAMP_STATUS2_REG:
+ case AD4691_GPIO_READ:
+ case AD4691_ACC_STATUS_FULL1_REG ... AD4691_ACC_STATUS_SAT2_REG:
+ case AD4691_ACC_SAT_OVR_REG(0) ... AD4691_ACC_SAT_OVR_REG(15):
+ return true;
+ default:
+ break;
+ }
+
+ /*
+ * Multi-byte registers have non-unit strides; only accept base
+ * addresses to prevent debugfs from triggering reads that cross
+ * register boundaries.
+ */
+ if (reg >= AD4691_AVG_IN(0) && reg <= AD4691_AVG_IN(15))
+ return (reg - AD4691_AVG_IN(0)) % 2 == 0;
+ if (reg >= AD4691_AVG_STS_IN(0) && reg <= AD4691_AVG_STS_IN(15))
+ return (reg - AD4691_AVG_STS_IN(0)) % 3 == 0;
+ if (reg >= AD4691_ACC_IN(0) && reg <= AD4691_ACC_IN(15))
+ return (reg - AD4691_ACC_IN(0)) % 3 == 0;
+ if (reg >= AD4691_ACC_STS_DATA(0) && reg <= AD4691_ACC_STS_DATA(15))
+ return (reg - AD4691_ACC_STS_DATA(0)) % 4 == 0;
+
+ return false;
+}
+
+static bool ad4691_readable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_SPARE_CONTROL ... AD4691_ACC_SAT_OVR_REG(15):
+ case AD4691_STD_SEQ_CONFIG:
+ return true;
+ default:
+ break;
+ }
+
+ /* Multi-byte registers: only accept base addresses (see volatile_reg). */
+ if (reg >= AD4691_AVG_IN(0) && reg <= AD4691_AVG_IN(15))
+ return (reg - AD4691_AVG_IN(0)) % 2 == 0;
+ if (reg >= AD4691_AVG_STS_IN(0) && reg <= AD4691_AVG_STS_IN(15))
+ return (reg - AD4691_AVG_STS_IN(0)) % 3 == 0;
+ if (reg >= AD4691_ACC_IN(0) && reg <= AD4691_ACC_IN(15))
+ return (reg - AD4691_ACC_IN(0)) % 3 == 0;
+ if (reg >= AD4691_ACC_STS_DATA(0) && reg <= AD4691_ACC_STS_DATA(15))
+ return (reg - AD4691_ACC_STS_DATA(0)) % 4 == 0;
+
+ return false;
+}
+
+static bool ad4691_writeable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_STD_SEQ_CONFIG:
+ case AD4691_SPARE_CONTROL ... AD4691_GPIO_MODE2_REG:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static const struct regmap_config ad4691_regmap_config = {
+ .reg_bits = 16,
+ .val_bits = 32,
+ .reg_read = ad4691_reg_read,
+ .reg_write = ad4691_reg_write,
+ .volatile_reg = ad4691_volatile_reg,
+ .readable_reg = ad4691_readable_reg,
+ .writeable_reg = ad4691_writeable_reg,
+ .max_register = AD4691_ACC_STS_DATA(15),
+ .cache_type = REGCACHE_MAPLE,
+};
+
+/*
+ * Index 0 in ad4691_osc_freqs_Hz is 1 MHz — valid only for AD4692/AD4694
+ * (max_rate == 1 MHz). AD4691/AD4693 cap at 500 kHz so their valid range
+ * starts at index 1.
+ */
+static unsigned int ad4691_samp_freq_start(const struct ad4691_chip_info *info)
+{
+ return (info->max_rate == 1 * HZ_PER_MHZ) ? 0 : 1;
+}
+
+static int ad4691_get_sampling_freq(struct ad4691_state *st, int *val)
+{
+ unsigned int reg_val;
+ int ret;
+
+ /*
+ * AD4691_OSC_FREQ_REG is non-volatile and written during
+ * ad4691_config(), so regmap returns the cached value here without
+ * touching the SPI bus. No lock is needed.
+ */
+ ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
+ if (ret)
+ return ret;
+
+ *val = ad4691_osc_freqs_Hz[FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val)];
+ return IIO_VAL_INT;
+}
+
+static int ad4691_set_sampling_freq(struct iio_dev *indio_dev, int freq)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int start = ad4691_samp_freq_start(st->info);
+
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ for (unsigned int i = start; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
+ if (ad4691_osc_freqs_Hz[i] != freq)
+ continue;
+ return regmap_update_bits(st->regmap, AD4691_OSC_FREQ_REG,
+ AD4691_OSC_FREQ_MASK, i);
+ }
+
+ return -EINVAL;
+}
+
+static int ad4691_read_avail(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ const int **vals, int *type,
+ int *length, long mask)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int start = ad4691_samp_freq_start(st->info);
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ *vals = &ad4691_osc_freqs_Hz[start];
+ *type = IIO_VAL_INT;
+ *length = ARRAY_SIZE(ad4691_osc_freqs_Hz) - start;
+ return IIO_AVAIL_LIST;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_single_shot_read(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int *val)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int reg_val, osc_idx, period_us;
+ int ret;
+
+ guard(mutex)(&st->lock);
+
+ /* Use AUTONOMOUS mode for single-shot reads. */
+ ret = regmap_write(st->regmap, AD4691_STATE_RESET_REG,
+ AD4691_STATE_RESET_ALL);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
+ BIT(chan->channel));
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
+ ~BIT(chan->channel) & GENMASK(15, 0));
+ if (ret)
+ return ret;
+
+ ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_OSC_EN_REG, 1);
+ if (ret)
+ return ret;
+
+ osc_idx = FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val);
+ /* Wait 2 oscillator periods for the conversion to complete. */
+ period_us = DIV_ROUND_UP(2UL * USEC_PER_SEC, ad4691_osc_freqs_Hz[osc_idx]);
+ fsleep(period_us);
+
+ ret = regmap_write(st->regmap, AD4691_OSC_EN_REG, 0);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(st->regmap, AD4691_AVG_IN(chan->channel), ®_val);
+ if (ret)
+ return ret;
+
+ *val = reg_val;
+
+ ret = regmap_write(st->regmap, AD4691_STATE_RESET_REG, AD4691_STATE_RESET_ALL);
+ if (ret)
+ return ret;
+
+ return IIO_VAL_INT;
+}
+
+static int ad4691_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int *val,
+ int *val2, long info)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ switch (info) {
+ case IIO_CHAN_INFO_RAW: {
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ return ad4691_single_shot_read(indio_dev, chan, val);
+ }
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ return ad4691_get_sampling_freq(st, val);
+ case IIO_CHAN_INFO_SCALE:
+ *val = st->vref_uV / (MICRO / MILLI);
+ *val2 = chan->scan_type.realbits;
+ return IIO_VAL_FRACTIONAL_LOG2;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_write_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int val, int val2, long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ return ad4691_set_sampling_freq(indio_dev, val);
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_reg_access(struct iio_dev *indio_dev, unsigned int reg,
+ unsigned int writeval, unsigned int *readval)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ guard(mutex)(&st->lock);
+
+ if (readval)
+ return regmap_read(st->regmap, reg, readval);
+
+ return regmap_write(st->regmap, reg, writeval);
+}
+
+static const struct iio_info ad4691_info = {
+ .read_raw = &ad4691_read_raw,
+ .write_raw = &ad4691_write_raw,
+ .read_avail = &ad4691_read_avail,
+ .debugfs_reg_access = &ad4691_reg_access,
+};
+
+static int ad4691_regulator_setup(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ int ret;
+
+ ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(ad4691_supplies),
+ ad4691_supplies);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to get and enable supplies\n");
+
+ /*
+ * vdd-supply and ldo-in-supply are mutually exclusive:
+ * vdd-supply present → external 1.8V VDD; disable internal LDO.
+ * vdd-supply absent → enable internal LDO fed from ldo-in-supply.
+ * Having both simultaneously is strongly inadvisable per the datasheet.
+ */
+ ret = devm_regulator_get_enable_optional(dev, "vdd");
+ if (ret == -ENODEV) {
+ ret = devm_regulator_get_enable(dev, "ldo-in");
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "Failed to get and enable LDO-IN\n");
+ st->ldo_en = true;
+ } else if (ret) {
+ return dev_err_probe(dev, ret, "Failed to get and enable VDD\n");
+ }
+
+ st->vref_uV = devm_regulator_get_enable_read_voltage(dev, "ref");
+ if (st->vref_uV == -ENODEV) {
+ st->vref_uV = devm_regulator_get_enable_read_voltage(dev, "refin");
+ st->refbuf_en = true;
+ }
+ if (st->vref_uV < 0)
+ return dev_err_probe(dev, st->vref_uV,
+ "Failed to get reference supply\n");
+
+ if (st->vref_uV < AD4691_VREF_uV_MIN || st->vref_uV > AD4691_VREF_uV_MAX)
+ return dev_err_probe(dev, -EINVAL,
+ "vref(%d) must be in the range [%u...%u]\n",
+ st->vref_uV, AD4691_VREF_uV_MIN,
+ AD4691_VREF_uV_MAX);
+
+ return 0;
+}
+
+static int ad4691_reset(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ struct reset_control *rst;
+
+ rst = devm_reset_control_get_optional_exclusive(dev, NULL);
+ if (IS_ERR(rst))
+ return dev_err_probe(dev, PTR_ERR(rst), "Failed to get reset\n");
+
+ if (rst) {
+ /*
+ * Assert the reset line before sleeping to guarantee a proper
+ * reset pulse on every probe, including driver reloads where
+ * the line may already be deasserted (reset_control_put() does
+ * not re-assert on release).
+ * devm_reset_control_get_optional_exclusive_deasserted() cannot
+ * be used because it deasserts immediately without delay; the
+ * datasheet (Table 5) requires a ≥300 µs reset pulse width
+ * before deassertion.
+ */
+ reset_control_assert(rst);
+ fsleep(300);
+ return reset_control_deassert(rst);
+ }
+
+ /* No hardware reset available, fall back to software reset. */
+ return regmap_write(st->regmap, AD4691_SPI_CONFIG_A_REG,
+ AD4691_SW_RESET);
+}
+
+static int ad4691_config(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ enum ad4691_ref_ctrl ref_val;
+ unsigned int val;
+ int ret;
+
+ switch (st->vref_uV) {
+ case AD4691_VREF_uV_MIN ... AD4691_VREF_2P5_uV_MAX:
+ ref_val = AD4691_VREF_2P5;
+ break;
+ case AD4691_VREF_2P5_uV_MAX + 1 ... AD4691_VREF_3P0_uV_MAX:
+ ref_val = AD4691_VREF_3P0;
+ break;
+ case AD4691_VREF_3P0_uV_MAX + 1 ... AD4691_VREF_3P3_uV_MAX:
+ ref_val = AD4691_VREF_3P3;
+ break;
+ case AD4691_VREF_3P3_uV_MAX + 1 ... AD4691_VREF_4P096_uV_MAX:
+ ref_val = AD4691_VREF_4P096;
+ break;
+ case AD4691_VREF_4P096_uV_MAX + 1 ... AD4691_VREF_uV_MAX:
+ ref_val = AD4691_VREF_5P0;
+ break;
+ default:
+ return dev_err_probe(dev, -EINVAL,
+ "Unsupported vref voltage: %d uV\n",
+ st->vref_uV);
+ }
+
+ val = FIELD_PREP(AD4691_REF_CTRL_MASK, ref_val);
+ if (st->refbuf_en)
+ val |= AD4691_REFBUF_EN;
+
+ ret = regmap_write(st->regmap, AD4691_REF_CTRL, val);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write REF_CTRL\n");
+
+ ret = regmap_assign_bits(st->regmap, AD4691_DEVICE_SETUP,
+ AD4691_LDO_EN, st->ldo_en);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write DEVICE_SETUP\n");
+
+ /*
+ * Set the internal oscillator to the highest rate this chip supports.
+ * Index 0 (1 MHz) exceeds the 500 kHz max of AD4691/AD4693, so those
+ * chips start at index 1 (500 kHz).
+ */
+ ret = regmap_write(st->regmap, AD4691_OSC_FREQ_REG,
+ ad4691_samp_freq_start(st->info));
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write OSC_FREQ\n");
+
+ ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
+ AD4691_ADC_MODE_MASK, AD4691_AUTONOMOUS_MODE);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write ADC_SETUP\n");
+
+ return 0;
+}
+
+static int ad4691_probe(struct spi_device *spi)
+{
+ struct device *dev = &spi->dev;
+ struct iio_dev *indio_dev;
+ struct ad4691_state *st;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ st = iio_priv(indio_dev);
+ st->info = spi_get_device_match_data(spi);
+ if (!st->info)
+ return -ENODEV;
+
+ ret = devm_mutex_init(dev, &st->lock);
+ if (ret)
+ return ret;
+
+ st->regmap = devm_regmap_init(dev, NULL, spi, &ad4691_regmap_config);
+ if (IS_ERR(st->regmap))
+ return dev_err_probe(dev, PTR_ERR(st->regmap),
+ "Failed to initialize regmap\n");
+
+ ret = ad4691_regulator_setup(st);
+ if (ret)
+ return ret;
+
+ ret = ad4691_reset(st);
+ if (ret)
+ return ret;
+
+ ret = ad4691_config(st);
+ if (ret)
+ return ret;
+
+ indio_dev->name = st->info->name;
+ indio_dev->info = &ad4691_info;
+ indio_dev->modes = INDIO_DIRECT_MODE;
+
+ indio_dev->channels = st->info->sw_info->channels;
+ indio_dev->num_channels = st->info->sw_info->num_channels;
+
+ return devm_iio_device_register(dev, indio_dev);
+}
+
+static const struct of_device_id ad4691_of_match[] = {
+ { .compatible = "adi,ad4691", .data = &ad4691_chip_info },
+ { .compatible = "adi,ad4692", .data = &ad4692_chip_info },
+ { .compatible = "adi,ad4693", .data = &ad4693_chip_info },
+ { .compatible = "adi,ad4694", .data = &ad4694_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ad4691_of_match);
+
+static const struct spi_device_id ad4691_id[] = {
+ { "ad4691", (kernel_ulong_t)&ad4691_chip_info },
+ { "ad4692", (kernel_ulong_t)&ad4692_chip_info },
+ { "ad4693", (kernel_ulong_t)&ad4693_chip_info },
+ { "ad4694", (kernel_ulong_t)&ad4694_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(spi, ad4691_id);
+
+static struct spi_driver ad4691_driver = {
+ .driver = {
+ .name = "ad4691",
+ .of_match_table = ad4691_of_match,
+ },
+ .probe = ad4691_probe,
+ .id_table = ad4691_id,
+};
+module_spi_driver(ad4691_driver);
+
+MODULE_AUTHOR("Radu Sabau <radu.sabau@analog.com>");
+MODULE_DESCRIPTION("Analog Devices AD4691 Family ADC Driver");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related
* [PATCH v11 3/6] iio: adc: ad4691: add triggered buffer support
From: Radu Sabau via B4 Relay @ 2026-05-15 13:31 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260515-ad4692-multichannel-sar-adc-driver-v11-0-eab27d852ac2@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add buffered capture support using the IIO triggered buffer framework.
CNV Burst Mode: the GP pin identified by interrupt-names in the device
tree is configured as DATA_READY output. The IRQ handler stops
conversions and fires the IIO trigger; the trigger handler executes a
pre-built SPI message that reads all active channels from the AVG_IN
accumulator registers and then resets accumulator state and restarts
conversions for the next cycle.
Manual Mode: CNV is tied to SPI CS so each transfer simultaneously
reads the previous result and starts the next conversion (pipelined
N+1 scheme). At preenable time a pre-built, optimised SPI message of
N+1 transfers is constructed (N channel reads plus one NOOP to drain
the pipeline). The trigger handler executes the message in a single
spi_sync() call and collects the results. An external trigger (e.g.
iio-trig-hrtimer) is required to drive the trigger at the desired
sample rate.
Both modes share the same trigger handler and push a complete scan —
one big-endian 16-bit (__be16) slot per active channel, densely packed
in scan_index order, followed by a timestamp.
The CNV Burst Mode sampling frequency (PWM period) is exposed as a
buffer-level attribute via IIO_DEVICE_ATTR.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/Kconfig | 2 +
drivers/iio/adc/ad4691.c | 592 +++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 580 insertions(+), 14 deletions(-)
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 5e601a87e5f3..484363458658 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -143,6 +143,8 @@ config AD4691
tristate "Analog Devices AD4691 Family ADC Driver"
depends on SPI
depends on REGULATOR || COMPILE_TEST
+ select IIO_BUFFER
+ select IIO_TRIGGERED_BUFFER
select REGMAP
help
Say yes here to build support for Analog Devices AD4691 Family MuxSAR
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
index ba77e1bfef16..bf27d5f33a49 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -5,24 +5,35 @@
*/
#include <linux/array_size.h>
#include <linux/bitfield.h>
-#include <linux/bitops.h>
+#include <linux/bitmap.h>
#include <linux/cleanup.h>
#include <linux/delay.h>
#include <linux/dev_printk.h>
#include <linux/device/devres.h>
+#include <linux/dmaengine.h>
#include <linux/err.h>
+#include <linux/interrupt.h>
+#include <linux/kstrtox.h>
#include <linux/limits.h>
#include <linux/math.h>
#include <linux/module.h>
#include <linux/mod_devicetable.h>
+#include <linux/property.h>
+#include <linux/pwm.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
#include <linux/reset.h>
+#include <linux/string.h>
#include <linux/spi/spi.h>
#include <linux/units.h>
#include <linux/unaligned.h>
+#include <linux/iio/buffer.h>
#include <linux/iio/iio.h>
+#include <linux/iio/sysfs.h>
+#include <linux/iio/trigger.h>
+#include <linux/iio/triggered_buffer.h>
+#include <linux/iio/trigger_consumer.h>
#define AD4691_VREF_uV_MIN 2400000
#define AD4691_VREF_uV_MAX 5250000
@@ -31,6 +42,9 @@
#define AD4691_VREF_3P3_uV_MAX 3750000
#define AD4691_VREF_4P096_uV_MAX 4500000
+#define AD4691_CNV_DUTY_CYCLE_NS 380
+#define AD4691_CNV_HIGH_TIME_NS 430
+
#define AD4691_SPI_CONFIG_A_REG 0x000
#define AD4691_SW_RESET (BIT(7) | BIT(0))
@@ -38,6 +52,7 @@
#define AD4691_CLAMP_STATUS1_REG 0x01A
#define AD4691_CLAMP_STATUS2_REG 0x01B
#define AD4691_DEVICE_SETUP 0x020
+#define AD4691_MANUAL_MODE BIT(2)
#define AD4691_LDO_EN BIT(4)
#define AD4691_REF_CTRL 0x021
#define AD4691_REF_CTRL_MASK GENMASK(4, 2)
@@ -45,13 +60,18 @@
#define AD4691_OSC_FREQ_REG 0x023
#define AD4691_OSC_FREQ_MASK GENMASK(3, 0)
#define AD4691_STD_SEQ_CONFIG 0x025
+#define AD4691_SEQ_ALL_CHANNELS_OFF 0x00
#define AD4691_SPARE_CONTROL 0x02A
+#define AD4691_NOOP 0x00
+#define AD4691_ADC_CHAN(ch) ((0x10 + (ch)) << 3)
+
#define AD4691_OSC_EN_REG 0x180
#define AD4691_STATE_RESET_REG 0x181
#define AD4691_STATE_RESET_ALL 0x01
#define AD4691_ADC_SETUP 0x182
#define AD4691_ADC_MODE_MASK GENMASK(1, 0)
+#define AD4691_CNV_BURST_MODE 0x01
#define AD4691_AUTONOMOUS_MODE 0x02
/*
* ACC_MASK_REG covers both mask bytes via ADDR_DESCENDING SPI: writing a
@@ -61,6 +81,8 @@
#define AD4691_ACC_DEPTH_IN(n) (0x186 + (n))
#define AD4691_GPIO_MODE1_REG 0x196
#define AD4691_GPIO_MODE2_REG 0x197
+#define AD4691_GP_MODE_MASK GENMASK(3, 0)
+#define AD4691_GP_MODE_DATA_READY 0x06
#define AD4691_GPIO_READ 0x1A0
#define AD4691_ACC_STATUS_FULL1_REG 0x1B0
#define AD4691_ACC_STATUS_FULL2_REG 0x1B1
@@ -110,6 +132,7 @@ struct ad4691_chip_info {
.sign = 'u', \
.realbits = 16, \
.storagebits = 16, \
+ .endianness = IIO_BE, \
}, \
}
@@ -130,6 +153,7 @@ static const struct iio_chan_spec ad4691_channels[] = {
AD4691_CHANNEL(13),
AD4691_CHANNEL(14),
AD4691_CHANNEL(15),
+ IIO_CHAN_SOFT_TIMESTAMP(16),
};
static const struct iio_chan_spec ad4693_channels[] = {
@@ -141,6 +165,17 @@ static const struct iio_chan_spec ad4693_channels[] = {
AD4691_CHANNEL(5),
AD4691_CHANNEL(6),
AD4691_CHANNEL(7),
+ IIO_CHAN_SOFT_TIMESTAMP(8),
+};
+
+static const struct ad4691_channel_info ad4691_sw_info = {
+ .channels = ad4691_channels,
+ .num_channels = ARRAY_SIZE(ad4691_channels),
+};
+
+static const struct ad4691_channel_info ad4693_sw_info = {
+ .channels = ad4693_channels,
+ .num_channels = ARRAY_SIZE(ad4693_channels),
};
/*
@@ -167,15 +202,7 @@ static const int ad4691_osc_freqs_Hz[] = {
[0xF] = 1250,
};
-static const struct ad4691_channel_info ad4691_sw_info = {
- .channels = ad4691_channels,
- .num_channels = ARRAY_SIZE(ad4691_channels),
-};
-
-static const struct ad4691_channel_info ad4693_sw_info = {
- .channels = ad4693_channels,
- .num_channels = ARRAY_SIZE(ad4693_channels),
-};
+static const char * const ad4691_gp_names[] = { "gp0", "gp1", "gp2", "gp3" };
static const struct ad4691_chip_info ad4691_chip_info = {
.name = "ad4691",
@@ -204,7 +231,14 @@ static const struct ad4691_chip_info ad4694_chip_info = {
struct ad4691_state {
const struct ad4691_chip_info *info;
struct regmap *regmap;
+ struct spi_device *spi;
+
+ struct pwm_device *conv_trigger;
+ int irq;
int vref_uV;
+ u32 cnv_period_ns;
+
+ bool manual_mode;
bool refbuf_en;
bool ldo_en;
/*
@@ -212,8 +246,56 @@ struct ad4691_state {
* atomicity of consecutive SPI operations.
*/
struct mutex lock;
+ /*
+ * Per-buffer-enable lifetime resources:
+ * Manual Mode - a pre-built SPI message that clocks out N+1
+ * transfers in one go.
+ * CNV Burst Mode - a pre-built SPI message that clocks out 2*N
+ * transfers in one go.
+ */
+ struct spi_message scan_msg;
+ /*
+ * max 16 + 1 NOOP (manual) or 2*16 + 1 state-reset (CNV burst).
+ */
+ struct spi_transfer scan_xfers[34];
+ /*
+ * CNV burst: 16 AVG_IN addresses = 16. Manual: 16 channel cmds +
+ * 1 NOOP = 17. Stored as native u16; put_unaligned_be16() fills each
+ * slot so the SPI controller (bits_per_word=8) sends bytes MSB-first.
+ */
+ u16 scan_tx[17] __aligned(IIO_DMA_MINALIGN);
+ /*
+ * CNV burst state-reset: 4-byte write [addr_hi, addr_lo,
+ * STATE_RESET_ALL, OSC_EN=1]. CS is asserted throughout, so
+ * ADDR_DESCENDING writes byte[3]=1 to OSC_EN_REG (0x180) as a
+ * deliberate side-write, keeping the oscillator enabled. Shared
+ * with the offload path (mutually exclusive at probe).
+ */
+ u8 scan_tx_reset[4] __aligned(IIO_DMA_MINALIGN);
+ /*
+ * Scan buffer: one BE16 slot per active channel, plus timestamp.
+ * DMA-aligned because scan_xfers point rx_buf directly into vals[].
+ */
+ IIO_DECLARE_DMA_BUFFER_WITH_TS(__be16, vals, 16);
};
+/*
+ * Configure the given GP pin (0-3) as DATA_READY output.
+ * GP0/GP1 → GPIO_MODE1_REG, GP2/GP3 → GPIO_MODE2_REG.
+ * Even pins occupy bits [3:0], odd pins bits [7:4].
+ */
+static int ad4691_gpio_setup(struct ad4691_state *st, unsigned int gp_num)
+{
+ unsigned int bit_off = gp_num % 2;
+ unsigned int reg_off = gp_num / 2;
+ unsigned int shift = 4 * bit_off;
+
+ return regmap_update_bits(st->regmap,
+ AD4691_GPIO_MODE1_REG + reg_off,
+ AD4691_GP_MODE_MASK << shift,
+ AD4691_GP_MODE_DATA_READY << shift);
+}
+
static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
{
struct spi_device *spi = context;
@@ -534,13 +616,405 @@ static int ad4691_reg_access(struct iio_dev *indio_dev, unsigned int reg,
return regmap_write(st->regmap, reg, writeval);
}
-static const struct iio_info ad4691_info = {
+static int ad4691_set_pwm_freq(struct ad4691_state *st, unsigned int freq)
+{
+ if (!freq)
+ return -EINVAL;
+
+ st->cnv_period_ns = DIV_ROUND_UP(NSEC_PER_SEC, freq);
+ return 0;
+}
+
+static int ad4691_sampling_enable(struct ad4691_state *st, bool enable)
+{
+ struct pwm_state conv_state = {
+ .period = st->cnv_period_ns,
+ .duty_cycle = AD4691_CNV_DUTY_CYCLE_NS,
+ .polarity = PWM_POLARITY_NORMAL,
+ .enabled = enable,
+ };
+
+ return pwm_apply_might_sleep(st->conv_trigger, &conv_state);
+}
+
+/*
+ * ad4691_enter_conversion_mode - Switch the chip to its buffer conversion mode.
+ *
+ * Configures the ADC hardware registers for the mode selected at probe
+ * (CNV_BURST or MANUAL). Called from buffer preenable before starting
+ * sampling. The chip is in AUTONOMOUS mode during idle (for read_raw).
+ */
+static int ad4691_enter_conversion_mode(struct ad4691_state *st)
+{
+ int ret;
+
+ if (st->manual_mode)
+ return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
+ AD4691_MANUAL_MODE, AD4691_MANUAL_MODE);
+
+ ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
+ AD4691_ADC_MODE_MASK, AD4691_CNV_BURST_MODE);
+ if (ret)
+ return ret;
+
+ return regmap_write(st->regmap, AD4691_STATE_RESET_REG,
+ AD4691_STATE_RESET_ALL);
+}
+
+/*
+ * ad4691_exit_conversion_mode - Return the chip to AUTONOMOUS mode.
+ *
+ * Called from buffer postdisable to restore the chip to the
+ * idle state used by read_raw. Clears the sequencer and resets state.
+ */
+static int ad4691_exit_conversion_mode(struct ad4691_state *st)
+{
+ if (st->manual_mode)
+ return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
+ AD4691_MANUAL_MODE, 0);
+
+ return regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
+ AD4691_ADC_MODE_MASK, AD4691_AUTONOMOUS_MODE);
+}
+
+static int ad4691_manual_buffer_preenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int k, i;
+ int ret;
+
+ memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
+ memset(st->scan_tx, 0, sizeof(st->scan_tx));
+
+ spi_message_init(&st->scan_msg);
+
+ k = 0;
+ iio_for_each_active_channel(indio_dev, i) {
+ if (i >= indio_dev->num_channels - 1)
+ break; /* skip soft timestamp */
+ /*
+ * Channel-select command occupies the first (high) byte of the
+ * 16-bit DIN frame; the second byte is a don't-care zero pad.
+ * put_unaligned_be16() writes [cmd, 0x00] in memory so the
+ * SPI controller sends the command byte first on the wire.
+ */
+ put_unaligned_be16((u16)(AD4691_ADC_CHAN(i) << 8), &st->scan_tx[k]);
+ st->scan_xfers[k].tx_buf = &st->scan_tx[k];
+ /*
+ * The pipeline means xfer[0] receives the residual from the
+ * previous sequence, not a valid sample. Discard it (rx_buf=NULL)
+ * to avoid aliasing vals[0] across two concurrent DMA mappings.
+ * xfer[1] (or the NOOP when only one channel is active) writes
+ * the real ch[0] result to vals[0]. Subsequent transfers write
+ * into vals[k-1] so each result lands at the next dense slot.
+ */
+ st->scan_xfers[k].rx_buf = (k == 0) ? NULL : &st->vals[k - 1];
+ st->scan_xfers[k].len = sizeof(st->scan_tx[k]);
+ st->scan_xfers[k].cs_change = 1;
+ st->scan_xfers[k].cs_change_delay.value = AD4691_CNV_HIGH_TIME_NS;
+ st->scan_xfers[k].cs_change_delay.unit = SPI_DELAY_UNIT_NSECS;
+ spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
+ k++;
+ }
+
+ /* Final NOOP transfer retrieves the last channel's result. */
+ st->scan_xfers[k].tx_buf = &st->scan_tx[k]; /* scan_tx[k] == 0 == NOOP */
+ st->scan_xfers[k].rx_buf = &st->vals[k - 1];
+ st->scan_xfers[k].len = sizeof(st->scan_tx[k]);
+ spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
+
+ ret = spi_optimize_message(st->spi, &st->scan_msg);
+ if (ret)
+ return ret;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret) {
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ad4691_manual_buffer_postdisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ ret = ad4691_exit_conversion_mode(st);
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+}
+
+static const struct iio_buffer_setup_ops ad4691_manual_buffer_setup_ops = {
+ .preenable = &ad4691_manual_buffer_preenable,
+ .postdisable = &ad4691_manual_buffer_postdisable,
+};
+
+static int ad4691_cnv_burst_buffer_preenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int acc_mask, std_seq_config;
+ unsigned int k, i;
+ int ret;
+
+ memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
+ memset(st->scan_tx, 0, sizeof(st->scan_tx));
+
+ spi_message_init(&st->scan_msg);
+
+ /*
+ * Each AVG_IN read needs two transfers: a 2-byte address write phase
+ * followed by a 2-byte data read phase. CS toggles between channels
+ * (cs_change=1 on the read phase of all but the last channel).
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, i) {
+ if (i >= indio_dev->num_channels - 1)
+ break; /* skip soft timestamp */
+ put_unaligned_be16(0x8000 | AD4691_AVG_IN(i), &st->scan_tx[k]);
+ st->scan_xfers[2 * k].tx_buf = &st->scan_tx[k];
+ st->scan_xfers[2 * k].len = sizeof(st->scan_tx[k]);
+ spi_message_add_tail(&st->scan_xfers[2 * k], &st->scan_msg);
+ st->scan_xfers[2 * k + 1].rx_buf = &st->vals[k];
+ st->scan_xfers[2 * k + 1].len = sizeof(st->scan_tx[k]);
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+ spi_message_add_tail(&st->scan_xfers[2 * k + 1], &st->scan_msg);
+ k++;
+ }
+
+ /*
+ * Append a 4-byte state-reset transfer [addr_hi, addr_lo,
+ * STATE_RESET_ALL, OSC_EN=1]. CS is asserted throughout, so
+ * ADDR_DESCENDING writes byte[3]=1 to OSC_EN_REG (0x180) as a
+ * deliberate side-write, keeping the oscillator enabled.
+ * STATE_RESET_ALL starts the next burst; the hardware does not
+ * accumulate new conversions until after a STATE_RESET pulse, so
+ * no in-progress data is lost. No cs_change here — CS must
+ * deassert normally at end of message to frame the next command.
+ */
+ put_unaligned_be16(AD4691_STATE_RESET_REG, st->scan_tx_reset);
+ st->scan_tx_reset[2] = AD4691_STATE_RESET_ALL;
+ st->scan_tx_reset[3] = 1;
+ st->scan_xfers[2 * k].tx_buf = st->scan_tx_reset;
+ st->scan_xfers[2 * k].len = sizeof(st->scan_tx_reset);
+ spi_message_add_tail(&st->scan_xfers[2 * k], &st->scan_msg);
+
+ ret = spi_optimize_message(st->spi, &st->scan_msg);
+ if (ret)
+ return ret;
+
+ std_seq_config = bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)) & GENMASK(15, 0);
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG, std_seq_config);
+ if (ret)
+ goto err_unoptimize;
+
+ acc_mask = ~std_seq_config & GENMASK(15, 0);
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG, acc_mask);
+ if (ret)
+ goto err_unoptimize;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret)
+ goto err_unoptimize;
+
+ return 0;
+
+err_unoptimize:
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+}
+
+static int ad4691_cnv_burst_buffer_postenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ /*
+ * Start the PWM and unmask the IRQ here in postenable, not in
+ * preenable. The IIO core attaches the trigger poll function between
+ * preenable and postenable; enabling sampling or unmasking the IRQ
+ * before that point risks a DATA_READY assertion landing before the
+ * poll function is registered. iio_trigger_poll() would drop the
+ * event, disable_irq_nosync() would fire, and enable_irq() would
+ * never be called, leaving the IRQ permanently masked.
+ */
+ ret = ad4691_sampling_enable(st, true);
+ if (ret)
+ return ret;
+
+ enable_irq(st->irq);
+ return 0;
+}
+
+static int ad4691_cnv_burst_buffer_predisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ disable_irq(st->irq);
+ return ad4691_sampling_enable(st, false);
+}
+
+static int ad4691_cnv_burst_buffer_postdisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ ret = ad4691_exit_conversion_mode(st);
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+}
+
+static const struct iio_buffer_setup_ops ad4691_cnv_burst_buffer_setup_ops = {
+ .preenable = &ad4691_cnv_burst_buffer_preenable,
+ .postenable = &ad4691_cnv_burst_buffer_postenable,
+ .predisable = &ad4691_cnv_burst_buffer_predisable,
+ .postdisable = &ad4691_cnv_burst_buffer_postdisable,
+};
+
+static ssize_t sampling_frequency_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct iio_dev *indio_dev = dev_to_iio_dev(dev);
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ return sysfs_emit(buf, "%lu\n", NSEC_PER_SEC / st->cnv_period_ns);
+}
+
+static ssize_t sampling_frequency_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t len)
+{
+ struct iio_dev *indio_dev = dev_to_iio_dev(dev);
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int freq;
+ int ret;
+
+ ret = kstrtouint(buf, 10, &freq);
+ if (ret)
+ return ret;
+
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ ret = ad4691_set_pwm_freq(st, freq);
+ if (ret)
+ return ret;
+
+ return len;
+}
+
+static IIO_DEVICE_ATTR_RW(sampling_frequency, 0);
+
+static const struct iio_dev_attr *ad4691_buffer_attrs[] = {
+ &iio_dev_attr_sampling_frequency,
+ NULL
+};
+
+static irqreturn_t ad4691_irq(int irq, void *private)
+{
+ struct iio_dev *indio_dev = private;
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ /*
+ * Disable the IRQ before calling iio_trigger_poll(). The IRQ is
+ * re-enabled via the trigger .reenable callback, which the IIO core
+ * calls inside iio_trigger_notify_done() once use_count reaches zero.
+ * Re-enabling here (before notify_done) would race: a DATA_READY
+ * between enable_irq() and notify_done() calls iio_trigger_poll()
+ * while use_count > 0, dropping the event and permanently masking
+ * the IRQ.
+ *
+ * IRQF_ONESHOT masks the hardware line for the duration of this
+ * threaded handler; disable_irq_nosync() keeps the IRQ disabled even
+ * after IRQF_ONESHOT unmasks on return.
+ */
+ disable_irq_nosync(st->irq);
+ iio_trigger_poll(indio_dev->trig);
+
+ return IRQ_HANDLED;
+}
+
+static void ad4691_trigger_reenable(struct iio_trigger *trig)
+{
+ struct ad4691_state *st = iio_trigger_get_drvdata(trig);
+
+ enable_irq(st->irq);
+}
+
+static const struct iio_trigger_ops ad4691_trigger_ops = {
+ .reenable = ad4691_trigger_reenable,
+ .validate_device = iio_trigger_validate_own_device,
+};
+
+static int ad4691_read_scan(struct iio_dev *indio_dev, s64 ts)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ guard(mutex)(&st->lock);
+
+ ret = spi_sync(st->spi, &st->scan_msg);
+ if (ret)
+ return ret;
+
+ /*
+ * rx_buf pointers in scan_xfers point directly into scan.vals, so no
+ * copy is needed. The scan_msg already includes a STATE_RESET at the
+ * end (appended in preenable), so no explicit reset is needed here.
+ */
+ iio_push_to_buffers_with_ts(indio_dev, st->vals, sizeof(st->vals), ts);
+ return 0;
+}
+
+static irqreturn_t ad4691_trigger_handler(int irq, void *p)
+{
+ struct iio_poll_func *pf = p;
+ struct iio_dev *indio_dev = pf->indio_dev;
+
+ ad4691_read_scan(indio_dev, pf->timestamp);
+ iio_trigger_notify_done(indio_dev->trig);
+ return IRQ_HANDLED;
+}
+
+/*
+ * CNV burst mode: only allow our own trigger (driven by DATA_READY IRQ).
+ * Manual mode: external triggers (e.g. iio-trig-hrtimer) must be allowed
+ * because manual mode has no DATA_READY IRQ to fire the internal trigger.
+ * iio_trigger_ops.validate_device = iio_trigger_validate_own_device is
+ * correct in both modes — it prevents other devices from hijacking our
+ * internal trigger; the distinction here is only for iio_info.validate_trigger.
+ */
+static const struct iio_info ad4691_cnv_burst_info = {
+ .read_raw = &ad4691_read_raw,
+ .write_raw = &ad4691_write_raw,
+ .read_avail = &ad4691_read_avail,
+ .debugfs_reg_access = &ad4691_reg_access,
+ .validate_trigger = iio_validate_own_trigger,
+};
+
+static const struct iio_info ad4691_manual_info = {
.read_raw = &ad4691_read_raw,
.write_raw = &ad4691_write_raw,
.read_avail = &ad4691_read_avail,
.debugfs_reg_access = &ad4691_reg_access,
};
+static int ad4691_pwm_setup(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+
+ st->conv_trigger = devm_pwm_get(dev, "cnv");
+ if (IS_ERR(st->conv_trigger))
+ return dev_err_probe(dev, PTR_ERR(st->conv_trigger),
+ "Failed to get CNV PWM\n");
+
+ return ad4691_set_pwm_freq(st, st->info->max_rate);
+}
+
static int ad4691_regulator_setup(struct ad4691_state *st)
{
struct device *dev = regmap_get_device(st->regmap);
@@ -623,6 +1097,22 @@ static int ad4691_config(struct ad4691_state *st)
unsigned int val;
int ret;
+ /*
+ * Determine buffer conversion mode from DT: if a PWM is provided it
+ * drives the CNV pin (CNV_BURST_MODE); otherwise CNV is tied to CS
+ * and each SPI transfer triggers a conversion (MANUAL_MODE).
+ * Both modes idle in AUTONOMOUS mode so that read_raw can use the
+ * internal oscillator without disturbing the hardware configuration.
+ */
+ if (device_property_present(dev, "pwms")) {
+ st->manual_mode = false;
+ ret = ad4691_pwm_setup(st);
+ if (ret)
+ return ret;
+ } else {
+ st->manual_mode = true;
+ }
+
switch (st->vref_uV) {
case AD4691_VREF_uV_MIN ... AD4691_VREF_2P5_uV_MAX:
ref_val = AD4691_VREF_2P5;
@@ -676,6 +1166,79 @@ static int ad4691_config(struct ad4691_state *st)
return 0;
}
+static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
+ struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ struct iio_trigger *trig;
+ unsigned int i;
+ int irq, ret;
+
+ indio_dev->channels = st->info->sw_info->channels;
+ indio_dev->num_channels = st->info->sw_info->num_channels;
+ indio_dev->info = st->manual_mode ? &ad4691_manual_info : &ad4691_cnv_burst_info;
+
+ trig = devm_iio_trigger_alloc(dev, "%s-dev%d", indio_dev->name,
+ iio_device_id(indio_dev));
+ if (!trig)
+ return -ENOMEM;
+
+ trig->ops = &ad4691_trigger_ops;
+ iio_trigger_set_drvdata(trig, st);
+
+ ret = devm_iio_trigger_register(dev, trig);
+ if (ret)
+ return dev_err_probe(dev, ret, "IIO trigger register failed\n");
+
+ indio_dev->trig = iio_trigger_get(trig);
+
+ if (st->manual_mode)
+ return devm_iio_triggered_buffer_setup(dev, indio_dev,
+ &iio_pollfunc_store_time,
+ &ad4691_trigger_handler,
+ &ad4691_manual_buffer_setup_ops);
+
+ /*
+ * The GP pin named in interrupt-names asserts at end-of-conversion.
+ * The IRQ handler stops conversions and fires the IIO trigger so
+ * the trigger handler can read and push the sample to the buffer.
+ * The IRQ is kept disabled until the buffer is enabled.
+ */
+ irq = -ENXIO;
+ for (i = 0; i < ARRAY_SIZE(ad4691_gp_names); i++) {
+ irq = fwnode_irq_get_byname(dev_fwnode(dev),
+ ad4691_gp_names[i]);
+ if (irq > 0 || irq == -EPROBE_DEFER)
+ break;
+ }
+ if (irq < 0)
+ return dev_err_probe(dev, irq, "failed to get GP interrupt\n");
+
+ st->irq = irq;
+
+ ret = ad4691_gpio_setup(st, i);
+ if (ret)
+ return ret;
+
+ /*
+ * IRQ is kept disabled until the buffer is enabled to prevent
+ * spurious DATA_READY events before the SPI message is set up.
+ */
+ ret = devm_request_threaded_irq(dev, irq, NULL,
+ &ad4691_irq,
+ IRQF_ONESHOT | IRQF_NO_AUTOEN,
+ indio_dev->name, indio_dev);
+ if (ret)
+ return ret;
+
+ return devm_iio_triggered_buffer_setup_ext(dev, indio_dev,
+ &iio_pollfunc_store_time,
+ &ad4691_trigger_handler,
+ IIO_BUFFER_DIRECTION_IN,
+ &ad4691_cnv_burst_buffer_setup_ops,
+ ad4691_buffer_attrs);
+}
+
static int ad4691_probe(struct spi_device *spi)
{
struct device *dev = &spi->dev;
@@ -688,6 +1251,7 @@ static int ad4691_probe(struct spi_device *spi)
return -ENOMEM;
st = iio_priv(indio_dev);
+ st->spi = spi;
st->info = spi_get_device_match_data(spi);
if (!st->info)
return -ENODEV;
@@ -714,11 +1278,11 @@ static int ad4691_probe(struct spi_device *spi)
return ret;
indio_dev->name = st->info->name;
- indio_dev->info = &ad4691_info;
indio_dev->modes = INDIO_DIRECT_MODE;
- indio_dev->channels = st->info->sw_info->channels;
- indio_dev->num_channels = st->info->sw_info->num_channels;
+ ret = ad4691_setup_triggered_buffer(indio_dev, st);
+ if (ret)
+ return ret;
return devm_iio_device_register(dev, indio_dev);
}
--
2.43.0
^ permalink raw reply related
* [PATCH v11 6/6] docs: iio: adc: ad4691: add driver documentation
From: Radu Sabau via B4 Relay @ 2026-05-15 13:31 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260515-ad4692-multichannel-sar-adc-driver-v11-0-eab27d852ac2@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add RST documentation for the AD4691 family ADC driver covering
supported devices, IIO channels, operating modes, oversampling,
reference voltage, LDO supply, reset, GP pins, SPI offload support,
and buffer data format.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
Documentation/iio/ad4691.rst | 225 +++++++++++++++++++++++++++++++++++++++++++
Documentation/iio/index.rst | 1 +
MAINTAINERS | 1 +
3 files changed, 227 insertions(+)
diff --git a/Documentation/iio/ad4691.rst b/Documentation/iio/ad4691.rst
new file mode 100644
index 000000000000..84492ef7a5d6
--- /dev/null
+++ b/Documentation/iio/ad4691.rst
@@ -0,0 +1,225 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+=============
+AD4691 driver
+=============
+
+ADC driver for Analog Devices Inc. AD4691 family of multichannel SAR ADCs.
+The module name is ``ad4691``.
+
+
+Supported devices
+=================
+
+The following chips are supported by this driver:
+
+* `AD4691 <https://www.analog.com/en/products/ad4691.html>`_ — 16-channel, 500 kSPS
+* `AD4692 <https://www.analog.com/en/products/ad4692.html>`_ — 16-channel, 1 MSPS
+* `AD4693 <https://www.analog.com/en/products/ad4693.html>`_ — 8-channel, 500 kSPS
+* `AD4694 <https://www.analog.com/en/products/ad4694.html>`_ — 8-channel, 1 MSPS
+
+
+IIO channels
+============
+
+Each physical ADC input maps to one IIO voltage channel. The AD4691 and AD4692
+expose 16 channels (``voltage0`` through ``voltage15``); the AD4693 and AD4694
+expose 8 channels (``voltage0`` through ``voltage7``).
+
+All channels share a common scale (``in_voltage_scale``), derived from the
+reference voltage. Each channel independently exposes:
+
+* ``in_voltageN_raw`` — single-shot ADC result
+* ``in_voltageN_sampling_frequency`` — per-channel effective output rate,
+ defined as the internal oscillator frequency divided by the channel's
+ oversampling ratio. Writing this attribute selects the nearest achievable
+ rate for the current OSR; the value read back reflects the actual rate after
+ snapping to the closest valid oscillator entry.
+* ``in_voltageN_sampling_frequency_available`` — list of achievable effective
+ rates for the channel's current oversampling ratio. The list updates
+ dynamically when the oversampling ratio changes.
+
+The following attributes are only available in CNV Burst Mode:
+
+* ``in_voltageN_oversampling_ratio`` — per-channel hardware oversampling depth;
+ see `Oversampling`_ below.
+* ``in_voltageN_oversampling_ratio_available`` — valid ratios: 1, 2, 4, 8, 16,
+ 32.
+
+
+Operating modes
+===============
+
+The driver supports two operating modes, selected automatically from the
+device tree at probe time.
+
+Manual Mode
+-----------
+
+Selected when no ``pwms`` property is present in the device tree. The CNV pin
+is tied to the SPI chip-select: every CS assertion triggers a conversion and
+returns the previous result. A user-defined IIO trigger (e.g. hrtimer trigger)
+drives the buffer.
+
+Oversampling is not supported in Manual Mode.
+
+CNV Burst Mode
+--------------
+
+Selected when a ``pwms`` property is present in the device tree. A PWM drives
+the CNV pin at the configured conversion rate. A GP pin wired to the SoC and
+declared in the device tree signals DATA_READY at the end of each burst,
+triggering a readout of all active channel results into the IIO buffer.
+
+The buffer output rate is controlled by the ``sampling_frequency`` attribute
+on the IIO buffer. In practice the PWM rate should be set low enough to allow
+the SPI readout to complete before the next conversion burst begins.
+
+Autonomous Mode (idle / single-shot)
+-------------------------------------
+
+When the IIO buffer is disabled, ``in_voltageN_raw`` reads perform a single
+conversion on the requested channel using the internal oscillator. The
+oscillator is started and stopped around each read to save power.
+
+
+Oversampling
+============
+
+In CNV Burst Mode each channel has an independent hardware accumulator that
+averages a configurable number of successive conversions. The result is always
+returned as a 16-bit mean, so ``realbits`` and ``storagebits`` are unaffected
+by the oversampling ratio. Valid ratios are 1, 2, 4, 8, 16 and 32; the default
+is 1 (no averaging). Oversampling is not supported in Manual Mode.
+
+.. code-block:: bash
+
+ # Set oversampling ratio to 16 on channel 0
+ echo 16 > /sys/bus/iio/devices/iio:device0/in_voltage0_oversampling_ratio
+
+ # Read the resulting effective sampling frequency
+ cat /sys/bus/iio/devices/iio:device0/in_voltage0_sampling_frequency
+
+Writing ``oversampling_ratio`` stores the new depth for that channel and
+snaps the internal oscillator to the largest valid table entry that is both
+less than or equal to ``old_effective_rate × new_osr`` and evenly divisible
+by ``new_osr``. This preserves an integer read-back of
+``in_voltageN_sampling_frequency`` after the change and keeps the oscillator
+as close as possible to the previous effective rate.
+
+All channels share one internal oscillator. Writing ``sampling_frequency`` for
+any channel updates the oscillator and therefore affects the effective rate
+read back from all other channels.
+
+
+Reference voltage
+=================
+
+The driver supports two reference configurations, mutually exclusive:
+
+* **External reference** (``ref-supply``): a voltage between 2.4 V and 5.25 V
+ supplied externally.
+* **Buffered internal reference** (``refin-supply``): an internal reference
+ buffer is enabled by the driver.
+
+Exactly one of ``ref-supply`` or ``refin-supply`` must be present in the
+device tree. The reference voltage determines the full-scale range reported
+via ``in_voltage_scale``.
+
+
+LDO supply
+==========
+
+The chip contains an internal LDO that powers part of the analog front-end.
+The supply configuration is mutually exclusive:
+
+* **External VDD** (``vdd-supply``): an external 1.8 V supply is used directly;
+ the internal LDO is disabled.
+* **Internal LDO** (``ldo-in-supply``): the internal LDO is enabled and fed
+ from the ``ldo-in`` regulator. Use this when no external 1.8 V VDD is present.
+
+Exactly one of ``vdd-supply`` or ``ldo-in-supply`` must be provided.
+
+
+Reset
+=====
+
+The driver supports two reset mechanisms:
+
+* **Hardware reset** (``reset-gpios`` in device tree): asserted at probe by
+ the reset controller framework.
+* **Software reset** (fallback when ``reset-gpios`` is absent): written
+ automatically at probe.
+
+
+GP pins and interrupts
+======================
+
+The chip exposes up to four general-purpose (GP) pins. In CNV Burst Mode
+(non-offload), one GP pin must be wired to an interrupt-capable SoC input and
+declared in the device tree using the ``interrupts`` and ``interrupt-names``
+properties. The ``interrupt-names`` value identifies which GP pin is used
+(``"gp0"`` through ``"gp3"``).
+
+Example device tree fragment::
+
+ adc@0 {
+ compatible = "adi,ad4692";
+ ...
+ interrupts = <17 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-parent = <&gpio0>;
+ interrupt-names = "gp0";
+ };
+
+
+SPI offload support
+===================
+
+When a SPI offload engine (e.g. the AXI SPI Engine) is present, the driver
+uses DMA-backed transfers for CPU-independent, high-throughput data capture.
+SPI offload is detected automatically at probe; if no offload hardware is
+available the driver falls back to the software triggered-buffer path.
+
+Two SPI offload sub-modes exist:
+
+CNV Burst offload
+-----------------
+
+Used when a ``pwms`` property is present and SPI offload is available. The PWM
+drives CNV at the configured rate; on DATA_READY the offload engine reads all
+active channel results and streams them directly to the IIO DMA buffer with no
+CPU involvement. The GP pin used as DATA_READY trigger is supplied by the
+trigger-source consumer at buffer enable time; no ``interrupt-names`` entry is
+required.
+
+Manual offload
+--------------
+
+Used when no ``pwms`` property is present and SPI offload is available. A
+periodic SPI offload trigger controls the conversion rate and the offload engine
+streams results directly to the IIO DMA buffer.
+
+The ``sampling_frequency`` attribute on the IIO buffer controls the trigger
+rate (in Hz). The initial rate is 100 kHz.
+
+Oversampling is not supported in Manual Mode.
+
+
+Buffer data format
+==================
+
+The sample format in the IIO buffer depends on whether SPI offload is in use.
+
+Software triggered-buffer path (no SPI offload)
+------------------------------------------------
+
+Each active channel occupies one 16-bit big-endian slot (``storagebits=16``,
+``endianness=be``). Active channels are packed densely in scan-index order,
+followed by a 64-bit software timestamp appended by the IIO core.
+
+SPI offload path
+----------------
+
+Each active channel occupies one 16-bit CPU-native slot (``storagebits=16``,
+``endianness=cpu``). The SPI offload engine streams 16-bit words directly from
+the SPI Engine into the DMA buffer; no software timestamp is appended.
diff --git a/Documentation/iio/index.rst b/Documentation/iio/index.rst
index ba3e609c6a13..007e0a1fcc5a 100644
--- a/Documentation/iio/index.rst
+++ b/Documentation/iio/index.rst
@@ -23,6 +23,7 @@ Industrial I/O Kernel Drivers
ad4000
ad4030
ad4062
+ ad4691
ad4695
ad7191
ad7380
diff --git a/MAINTAINERS b/MAINTAINERS
index 24e4502b8292..875ea2455d91 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1490,6 +1490,7 @@ L: linux-iio@vger.kernel.org
S: Supported
W: https://ez.analog.com/linux-software-drivers
F: Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
+F: Documentation/iio/ad4691.rst
F: drivers/iio/adc/ad4691.c
ANALOG DEVICES INC AD4695 DRIVER
--
2.43.0
^ permalink raw reply related
* [PATCH v11 5/6] iio: adc: ad4691: add oversampling support
From: Radu Sabau via B4 Relay @ 2026-05-15 13:31 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260515-ad4692-multichannel-sar-adc-driver-v11-0-eab27d852ac2@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add per-channel oversampling ratio (OSR) support for CNV burst mode.
The accumulator depth register (ACC_DEPTH_IN) is programmed with the
selected OSR at buffer enable time and before each single-shot read.
Supported OSR values: 1, 2, 4, 8, 16, 32.
Introduce AD4691_MANUAL_CHANNEL() for manual mode channels, which do
not expose the oversampling_ratio attribute since OSR is not applicable
in that mode. A separate manual_channels array is added to
struct ad4691_channel_info and selected at probe time.
in_voltageN_sampling_frequency represents the effective output rate for
channel N, defined as osc_freq / osr[N]. The chip has one internal
oscillator shared by all channels; each channel independently
accumulates osr[N] oscillator cycles before producing a result.
Writing sampling_frequency computes needed_osc = freq * osr[N] and
snaps down to the largest oscillator table entry that satisfies both
osc <= needed_osc and osc % osr[N] == 0, guaranteeing an exact integer
read-back. The result is stored in target_osc_freq_Hz and written to
OSC_FREQ_REG at buffer enable and single-shot time, so sampling_frequency
and oversampling_ratio can be set in any order.
in_voltageN_sampling_frequency_available is computed dynamically from
the channel's current OSR, listing only oscillator table entries that
divide evenly by osr[N], expressed as effective rates. The list becomes
sparser as OSR increases, capping at max_rate / osr[N].
Writing oversampling_ratio stores the new OSR for that channel and snaps
target_osc_freq_Hz to the largest oscillator table entry that is both
<= old_effective_rate * new_osr and evenly divisible by new_osr. This
preserves an integer read-back of in_voltageN_sampling_frequency after
the OSR change while keeping the oscillator as close as possible to the
previous effective rate.
OSR defaults to 1 (no accumulation) for all channels.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/ad4691.c | 381 ++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 343 insertions(+), 38 deletions(-)
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
index 25f7a6939b0f..39244e0e4a2d 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -25,6 +25,7 @@
#include <linux/reset.h>
#include <linux/string.h>
#include <linux/spi/spi.h>
+#include <linux/types.h>
#include <linux/spi/offload/consumer.h>
#include <linux/spi/offload/provider.h>
#include <linux/units.h>
@@ -117,6 +118,7 @@ enum ad4691_ref_ctrl {
struct ad4691_channel_info {
const struct iio_chan_spec *channels __counted_by_ptr(num_channels);
+ const struct iio_chan_spec *manual_channels __counted_by_ptr(num_channels);
unsigned int num_channels;
};
@@ -127,12 +129,39 @@ struct ad4691_chip_info {
const struct ad4691_channel_info *offload_info;
};
+/* CNV burst mode channel — exposes oversampling ratio. */
#define AD4691_CHANNEL(ch) \
{ \
.type = IIO_VOLTAGE, \
.indexed = 1, \
- .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) \
- | BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO) | \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate_available = \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO) | \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
+ .channel = ch, \
+ .scan_index = ch, \
+ .scan_type = { \
+ .sign = 'u', \
+ .realbits = 16, \
+ .storagebits = 16, \
+ .endianness = IIO_BE, \
+ }, \
+ }
+
+/*
+ * Manual mode channel — no oversampling ratio attribute. OSR is not
+ * supported in manual mode; ACC_DEPTH_IN is not configured during manual
+ * buffer enable.
+ */
+#define AD4691_MANUAL_CHANNEL(ch) \
+ { \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
.info_mask_separate_available = \
BIT(IIO_CHAN_INFO_SAMP_FREQ), \
.info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
@@ -151,8 +180,33 @@ struct ad4691_chip_info {
* bits into native 16-bit words before DMA, so samples are in
* CPU-native byte order (IIO_CPU). storagebits=16 matches the 16-bit
* DMA word size.
+ *
+ * CNV burst offload configures ACC_DEPTH_IN per channel, so the
+ * oversampling_ratio attribute is exposed. Manual offload does not;
+ * use AD4691_OFFLOAD_MANUAL_CHANNEL for that path.
*/
#define AD4691_OFFLOAD_CHANNEL(ch) \
+ { \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) \
+ | BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO) \
+ | BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate_available = \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO) \
+ | BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
+ .channel = ch, \
+ .scan_index = ch, \
+ .scan_type = { \
+ .sign = 'u', \
+ .realbits = 16, \
+ .storagebits = 16, \
+ }, \
+ }
+
+/* Manual offload — same IIO_CPU layout but no oversampling_ratio attribute. */
+#define AD4691_OFFLOAD_MANUAL_CHANNEL(ch) \
{ \
.type = IIO_VOLTAGE, \
.indexed = 1, \
@@ -236,23 +290,91 @@ static const struct iio_chan_spec ad4693_offload_channels[] = {
AD4691_OFFLOAD_CHANNEL(7),
};
+static const struct iio_chan_spec ad4691_manual_channels[] = {
+ AD4691_MANUAL_CHANNEL(0),
+ AD4691_MANUAL_CHANNEL(1),
+ AD4691_MANUAL_CHANNEL(2),
+ AD4691_MANUAL_CHANNEL(3),
+ AD4691_MANUAL_CHANNEL(4),
+ AD4691_MANUAL_CHANNEL(5),
+ AD4691_MANUAL_CHANNEL(6),
+ AD4691_MANUAL_CHANNEL(7),
+ AD4691_MANUAL_CHANNEL(8),
+ AD4691_MANUAL_CHANNEL(9),
+ AD4691_MANUAL_CHANNEL(10),
+ AD4691_MANUAL_CHANNEL(11),
+ AD4691_MANUAL_CHANNEL(12),
+ AD4691_MANUAL_CHANNEL(13),
+ AD4691_MANUAL_CHANNEL(14),
+ AD4691_MANUAL_CHANNEL(15),
+ IIO_CHAN_SOFT_TIMESTAMP(16),
+};
+
+static const struct iio_chan_spec ad4693_manual_channels[] = {
+ AD4691_MANUAL_CHANNEL(0),
+ AD4691_MANUAL_CHANNEL(1),
+ AD4691_MANUAL_CHANNEL(2),
+ AD4691_MANUAL_CHANNEL(3),
+ AD4691_MANUAL_CHANNEL(4),
+ AD4691_MANUAL_CHANNEL(5),
+ AD4691_MANUAL_CHANNEL(6),
+ AD4691_MANUAL_CHANNEL(7),
+ IIO_CHAN_SOFT_TIMESTAMP(8),
+};
+
+static const struct iio_chan_spec ad4691_offload_manual_channels[] = {
+ AD4691_OFFLOAD_MANUAL_CHANNEL(0),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(1),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(2),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(3),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(4),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(5),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(6),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(7),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(8),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(9),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(10),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(11),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(12),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(13),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(14),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(15),
+};
+
+static const struct iio_chan_spec ad4693_offload_manual_channels[] = {
+ AD4691_OFFLOAD_MANUAL_CHANNEL(0),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(1),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(2),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(3),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(4),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(5),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(6),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(7),
+};
+
+static const int ad4691_oversampling_ratios[] = { 1, 2, 4, 8, 16, 32 };
+
static const struct ad4691_channel_info ad4691_sw_info = {
.channels = ad4691_channels,
+ .manual_channels = ad4691_manual_channels,
.num_channels = ARRAY_SIZE(ad4691_channels),
};
static const struct ad4691_channel_info ad4693_sw_info = {
.channels = ad4693_channels,
+ .manual_channels = ad4693_manual_channels,
.num_channels = ARRAY_SIZE(ad4693_channels),
};
static const struct ad4691_channel_info ad4691_offload_info = {
.channels = ad4691_offload_channels,
+ .manual_channels = ad4691_offload_manual_channels,
.num_channels = ARRAY_SIZE(ad4691_offload_channels),
};
static const struct ad4691_channel_info ad4693_offload_info = {
.channels = ad4693_offload_channels,
+ .manual_channels = ad4693_offload_manual_channels,
.num_channels = ARRAY_SIZE(ad4693_offload_channels),
};
@@ -325,6 +447,19 @@ struct ad4691_state {
int irq;
int vref_uV;
u32 cnv_period_ns;
+ /*
+ * Snapped oscillator frequency (Hz) shared by all channels. Set when
+ * sampling_frequency or oversampling_ratio is written; written to
+ * OSC_FREQ_REG at buffer enable and single-shot time so both attributes
+ * can be set in any order. Reading in_voltageN_sampling_frequency
+ * returns target_osc_freq_Hz / osr[N] — the effective rate for that
+ * channel given its oversampling ratio.
+ */
+ u32 target_osc_freq_Hz;
+ /* Per-channel oversampling ratio; always 1 in manual mode. */
+ u8 osr[16];
+ /* Scratch buffer for read_avail SAMP_FREQ; content is OSR-dependent. */
+ int samp_freq_avail[16][ARRAY_SIZE(ad4691_osc_freqs_Hz)];
bool manual_mode;
bool refbuf_en;
@@ -398,8 +533,7 @@ static bool ad4691_offload_trigger_match(struct spi_offload_trigger *trigger,
enum spi_offload_trigger_type type,
u64 *args, u32 nargs)
{
- return type == SPI_OFFLOAD_TRIGGER_DATA_READY &&
- nargs == 1 && args[0] <= 3;
+ return type == SPI_OFFLOAD_TRIGGER_DATA_READY && nargs == 1 && args[0] <= 3;
}
static int ad4691_offload_trigger_request(struct spi_offload_trigger *trigger,
@@ -578,6 +712,16 @@ static const struct regmap_config ad4691_regmap_config = {
.cache_type = REGCACHE_MAPLE,
};
+/* Write target_osc_freq_Hz to OSC_FREQ_REG. Called at use time. */
+static int ad4691_write_osc_freq(struct ad4691_state *st)
+{
+ for (unsigned int i = 0; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
+ if (ad4691_osc_freqs_Hz[i] == st->target_osc_freq_Hz)
+ return regmap_write(st->regmap, AD4691_OSC_FREQ_REG, i);
+ }
+ return -EINVAL;
+}
+
/*
* Index 0 in ad4691_osc_freqs_Hz is 1 MHz — valid only for AD4692/AD4694
* (max_rate == 1 MHz). AD4691/AD4693 cap at 500 kHz so their valid range
@@ -588,41 +732,65 @@ static unsigned int ad4691_samp_freq_start(const struct ad4691_chip_info *info)
return (info->max_rate == 1 * HZ_PER_MHZ) ? 0 : 1;
}
-static int ad4691_get_sampling_freq(struct ad4691_state *st, int *val)
+/*
+ * Find the largest oscillator table entry that is both <= needed_osc and
+ * evenly divisible by osr (guaranteeing an integer effective rate on
+ * read-back). Returns 0 if no such entry exists in the chip's valid range.
+ */
+static unsigned int ad4691_find_osc_freq(struct ad4691_state *st,
+ unsigned int needed_osc,
+ unsigned int osr)
{
- unsigned int reg_val;
- int ret;
+ unsigned int start = ad4691_samp_freq_start(st->info);
- /*
- * AD4691_OSC_FREQ_REG is non-volatile and written during
- * ad4691_config(), so regmap returns the cached value here without
- * touching the SPI bus. No lock is needed.
- */
- ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
- if (ret)
- return ret;
+ for (unsigned int i = start; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
+ if ((unsigned int)ad4691_osc_freqs_Hz[i] > needed_osc)
+ continue;
+ if (ad4691_osc_freqs_Hz[i] % osr)
+ continue;
+ return ad4691_osc_freqs_Hz[i];
+ }
+ return 0;
+}
- *val = ad4691_osc_freqs_Hz[FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val)];
+static int ad4691_get_sampling_freq(struct ad4691_state *st, u8 osr, int *val)
+{
+ *val = st->target_osc_freq_Hz / osr;
return IIO_VAL_INT;
}
-static int ad4691_set_sampling_freq(struct iio_dev *indio_dev, int freq)
+static int ad4691_set_sampling_freq(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int freq)
{
struct ad4691_state *st = iio_priv(indio_dev);
- unsigned int start = ad4691_samp_freq_start(st->info);
+ unsigned int osr, found;
IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
if (IIO_DEV_ACQUIRE_FAILED(claim))
return -EBUSY;
- for (unsigned int i = start; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
- if (ad4691_osc_freqs_Hz[i] != freq)
- continue;
- return regmap_update_bits(st->regmap, AD4691_OSC_FREQ_REG,
- AD4691_OSC_FREQ_MASK, i);
- }
+ /*
+ * Read osr under st->lock: osr[chan] and target_osc_freq_Hz are
+ * modified together under the lock; reading after acquiring it ensures
+ * we see a consistent snapshot with no concurrent write racing us.
+ */
+ guard(mutex)(&st->lock);
+ osr = st->osr[chan->channel];
- return -EINVAL;
+ if (freq <= 0 || (unsigned int)freq > st->info->max_rate / osr)
+ return -EINVAL;
+
+ found = ad4691_find_osc_freq(st, (unsigned int)freq * osr, osr);
+ if (!found)
+ return -EINVAL;
+
+ /*
+ * Store the snapped oscillator frequency; OSC_FREQ_REG is written at
+ * buffer enable and single-shot time so that sampling_frequency and
+ * oversampling_ratio can be set in any order.
+ */
+ st->target_osc_freq_Hz = found;
+ return 0;
}
static int ad4691_read_avail(struct iio_dev *indio_dev,
@@ -634,10 +802,46 @@ static int ad4691_read_avail(struct iio_dev *indio_dev,
unsigned int start = ad4691_samp_freq_start(st->info);
switch (mask) {
- case IIO_CHAN_INFO_SAMP_FREQ:
- *vals = &ad4691_osc_freqs_Hz[start];
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ unsigned int osr;
+ int n = 0;
+
+ /*
+ * Hold the lock while reading osr[chan] and populating the
+ * scratch buffer: a concurrent oversampling_ratio write modifies
+ * both target_osc_freq_Hz and osr[] under the lock, so we must
+ * read osr atomically with respect to that write. The scratch
+ * buffer is per-channel, so concurrent reads on different
+ * channels do not race; concurrent reads on the same channel
+ * would compute identical values, but holding the lock avoids
+ * the formal data race.
+ */
+ scoped_guard(mutex, &st->lock) {
+ osr = st->osr[chan->channel];
+
+ /*
+ * Only oscillator frequencies evenly divisible by the
+ * channel's OSR yield an integer effective rate; expose
+ * those as effective rates (osc / osr) so the user works
+ * entirely in output-sample space.
+ */
+ for (unsigned int i = start;
+ i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
+ if (ad4691_osc_freqs_Hz[i] % osr)
+ continue;
+ st->samp_freq_avail[chan->channel][n++] =
+ ad4691_osc_freqs_Hz[i] / osr;
+ }
+ }
+ *vals = st->samp_freq_avail[chan->channel];
*type = IIO_VAL_INT;
- *length = ARRAY_SIZE(ad4691_osc_freqs_Hz) - start;
+ *length = n;
+ return IIO_AVAIL_LIST;
+ }
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
+ *vals = ad4691_oversampling_ratios;
+ *type = IIO_VAL_INT;
+ *length = ARRAY_SIZE(ad4691_oversampling_ratios);
return IIO_AVAIL_LIST;
default:
return -EINVAL;
@@ -648,7 +852,7 @@ static int ad4691_single_shot_read(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan, int *val)
{
struct ad4691_state *st = iio_priv(indio_dev);
- unsigned int reg_val, osc_idx, period_us;
+ unsigned int reg_val, period_us;
int ret;
guard(mutex)(&st->lock);
@@ -669,7 +873,12 @@ static int ad4691_single_shot_read(struct iio_dev *indio_dev,
if (ret)
return ret;
- ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
+ ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(chan->channel),
+ st->osr[chan->channel]);
+ if (ret)
+ return ret;
+
+ ret = ad4691_write_osc_freq(st);
if (ret)
return ret;
@@ -677,9 +886,12 @@ static int ad4691_single_shot_read(struct iio_dev *indio_dev,
if (ret)
return ret;
- osc_idx = FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val);
- /* Wait 2 oscillator periods for the conversion to complete. */
- period_us = DIV_ROUND_UP(2UL * USEC_PER_SEC, ad4691_osc_freqs_Hz[osc_idx]);
+ /*
+ * Wait osr + 1 oscillator periods: osr for accumulation, +1 for the
+ * pipeline margin (one extra period ensures the final result is ready).
+ */
+ period_us = DIV_ROUND_UP((st->osr[chan->channel] + 1) * USEC_PER_SEC,
+ st->target_osc_freq_Hz);
fsleep(period_us);
ret = regmap_write(st->regmap, AD4691_OSC_EN_REG, 0);
@@ -713,8 +925,21 @@ static int ad4691_read_raw(struct iio_dev *indio_dev,
return ad4691_single_shot_read(indio_dev, chan, val);
}
- case IIO_CHAN_INFO_SAMP_FREQ:
- return ad4691_get_sampling_freq(st, val);
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ /*
+ * Read target_osc_freq_Hz and osr[chan] under st->lock to get a
+ * consistent snapshot: write_raw for SAMP_FREQ or OSR modifies
+ * both fields under the lock, so a concurrent read without the
+ * lock could observe a new oscillator frequency with the old OSR.
+ */
+ guard(mutex)(&st->lock);
+ return ad4691_get_sampling_freq(st, st->osr[chan->channel], val);
+ }
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO: {
+ guard(mutex)(&st->lock);
+ *val = st->osr[chan->channel];
+ return IIO_VAL_INT;
+ }
case IIO_CHAN_INFO_SCALE:
*val = st->vref_uV / (MICRO / MILLI);
*val2 = chan->scan_type.realbits;
@@ -728,9 +953,48 @@ static int ad4691_write_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int val, int val2, long mask)
{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
switch (mask) {
case IIO_CHAN_INFO_SAMP_FREQ:
- return ad4691_set_sampling_freq(indio_dev, val);
+ return ad4691_set_sampling_freq(indio_dev, chan, val);
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO: {
+ unsigned int old_effective, found;
+ bool valid = false;
+
+ for (unsigned int i = 0; i < ARRAY_SIZE(ad4691_oversampling_ratios); i++) {
+ if (ad4691_oversampling_ratios[i] == val) {
+ valid = true;
+ break;
+ }
+ }
+ if (!valid)
+ return -EINVAL;
+
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ /*
+ * Hold st->lock while computing the new oscillator frequency
+ * and updating both target_osc_freq_Hz and osr[chan] atomically:
+ * read_raw for SAMP_FREQ reads both fields under the lock and
+ * must see a consistent pair (new osc ↔ new osr).
+ *
+ * Snap target_osc_freq_Hz to the largest table entry that is
+ * both <= old_effective * new_osr and evenly divisible by
+ * new_osr, preserving an integer read-back of
+ * in_voltageN_sampling_frequency after the OSR change.
+ */
+ guard(mutex)(&st->lock);
+ old_effective = st->target_osc_freq_Hz / st->osr[chan->channel];
+ found = ad4691_find_osc_freq(st, old_effective * (unsigned int)val, val);
+ if (!found)
+ return -EINVAL;
+ st->target_osc_freq_Hz = found;
+ st->osr[chan->channel] = val;
+ return 0;
+ }
default:
return -EINVAL;
}
@@ -785,6 +1049,10 @@ static int ad4691_enter_conversion_mode(struct ad4691_state *st)
return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
AD4691_MANUAL_MODE, AD4691_MANUAL_MODE);
+ ret = ad4691_write_osc_freq(st);
+ if (ret)
+ return ret;
+
ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
AD4691_ADC_MODE_MASK, AD4691_CNV_BURST_MODE);
if (ret)
@@ -948,6 +1216,14 @@ static int ad4691_cnv_burst_buffer_preenable(struct iio_dev *indio_dev)
if (ret)
goto err_unoptimize;
+ iio_for_each_active_channel(indio_dev, i) {
+ if (i >= indio_dev->num_channels - 1)
+ break; /* skip soft timestamp */
+ ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(i), st->osr[i]);
+ if (ret)
+ goto err_unoptimize;
+ }
+
ret = ad4691_enter_conversion_mode(st);
if (ret)
goto err_unoptimize;
@@ -1126,6 +1402,14 @@ static int ad4691_cnv_burst_offload_buffer_postenable(struct iio_dev *indio_dev)
if (ret)
return ret;
+ iio_for_each_active_channel(indio_dev, bit) {
+ if (bit >= indio_dev->num_channels)
+ break; /* defensive guard; offload channels have no soft timestamp */
+ ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(bit), st->osr[bit]);
+ if (ret)
+ return ret;
+ }
+
ret = ad4691_enter_conversion_mode(st);
if (ret)
return ret;
@@ -1524,6 +1808,8 @@ static int ad4691_config(struct ad4691_state *st)
if (ret)
return dev_err_probe(dev, ret, "Failed to write OSC_FREQ\n");
+ st->target_osc_freq_Hz = ad4691_osc_freqs_Hz[ad4691_samp_freq_start(st->info)];
+
ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
AD4691_ADC_MODE_MASK, AD4691_AUTONOMOUS_MODE);
if (ret)
@@ -1540,7 +1826,14 @@ static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
unsigned int i;
int irq, ret;
- indio_dev->channels = st->info->sw_info->channels;
+ /*
+ * Manual mode exposes channels without the oversampling_ratio attribute
+ * because ACC_DEPTH_IN is not configured in manual mode.
+ */
+ if (st->manual_mode)
+ indio_dev->channels = st->info->sw_info->manual_channels;
+ else
+ indio_dev->channels = st->info->sw_info->channels;
indio_dev->num_channels = st->info->sw_info->num_channels;
indio_dev->info = st->manual_mode ? &ad4691_manual_info : &ad4691_cnv_burst_info;
@@ -1621,7 +1914,18 @@ static int ad4691_setup_offload(struct iio_dev *indio_dev,
offload->offload = spi_offload;
st->offload = offload;
- indio_dev->channels = st->info->offload_info->channels;
+ /*
+ * CNV burst offload exposes oversampling_ratio (ACC_DEPTH_IN is
+ * configured per channel at buffer enable). Manual offload does not
+ * configure ACC_DEPTH_IN, so it uses a separate channel array
+ * without the oversampling_ratio attribute. Both paths use IIO_CPU
+ * (no .endianness annotation) because bits_per_word=16 causes the
+ * SPI Engine to produce native 16-bit DMA words.
+ */
+ if (st->manual_mode)
+ indio_dev->channels = st->info->offload_info->manual_channels;
+ else
+ indio_dev->channels = st->info->offload_info->channels;
indio_dev->num_channels = st->info->offload_info->num_channels;
/*
* Offload path uses DMA directly; no IIO trigger is involved, so
@@ -1695,6 +1999,7 @@ static int ad4691_probe(struct spi_device *spi)
st->info = spi_get_device_match_data(spi);
if (!st->info)
return -ENODEV;
+ memset(st->osr, 1, sizeof(st->osr));
ret = devm_mutex_init(dev, &st->lock);
if (ret)
--
2.43.0
^ permalink raw reply related
* [PATCH v11 4/6] iio: adc: ad4691: add SPI offload support
From: Radu Sabau via B4 Relay @ 2026-05-15 13:31 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260515-ad4692-multichannel-sar-adc-driver-v11-0-eab27d852ac2@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add SPI offload support to enable DMA-based, CPU-independent data
acquisition using the SPI Engine offload framework.
When an SPI offload is available (devm_spi_offload_get() succeeds),
the driver registers a DMA engine IIO buffer and uses dedicated buffer
setup operations. If no offload is available the existing software
triggered buffer path is used unchanged.
Both CNV Burst Mode and Manual Mode support offload, but use different
trigger mechanisms:
CNV Burst Mode: the SPI Engine is triggered by the ADC's DATA_READY
signal on the GP pin specified by the trigger-source consumer reference
in the device tree (one cell = GP pin number 0-3). For this mode the
driver acts as both an SPI offload consumer (DMA RX stream, message
optimization) and a trigger source provider: it registers the
GP/DATA_READY output via devm_spi_offload_trigger_register() so the
offload framework can match the '#trigger-source-cells' phandle and
automatically fire the SPI Engine DMA transfer at end-of-conversion.
Manual Mode: the SPI Engine is triggered by a periodic trigger at
the configured sampling frequency. The pre-built SPI message uses
the pipelined CNV-on-CS protocol: N+1 16-bit transfers are issued
for N active channels (the first result is discarded as garbage from
the pipeline flush) and the remaining N results are captured by DMA.
All offload transfers use 16-bit frames (bits_per_word=16, len=2).
The SPI Engine assembles received bits into native 16-bit words before
DMA, so offload samples land in CPU-native byte order (IIO_CPU).
Dedicated channel arrays (AD4691_OFFLOAD_CHANNEL) reflect this: they
omit IIO_BE and carry no soft timestamp (DMA delivers data directly to
userspace). The software triggered-buffer path retains its IIO_BE
channels because bits_per_word=8 causes SPI to deliver bytes MSB-first
into memory, making the on-disk layout big-endian. Both paths use
storagebits=16 as transfers are 16 bits wide in both cases.
IIO_BUFFER_DMAENGINE is selected because the offload path uses
devm_iio_dmaengine_buffer_setup_with_handle() to allocate and
attach the DMA RX buffer to the IIO device.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/Kconfig | 2 +
drivers/iio/adc/ad4691.c | 458 ++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 457 insertions(+), 3 deletions(-)
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 484363458658..44c8dbe3ff0d 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -144,8 +144,10 @@ config AD4691
depends on SPI
depends on REGULATOR || COMPILE_TEST
select IIO_BUFFER
+ select IIO_BUFFER_DMAENGINE
select IIO_TRIGGERED_BUFFER
select REGMAP
+ select SPI_OFFLOAD
help
Say yes here to build support for Analog Devices AD4691 Family MuxSAR
SPI analog to digital converters (ADC).
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
index bf27d5f33a49..25f7a6939b0f 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -25,10 +25,14 @@
#include <linux/reset.h>
#include <linux/string.h>
#include <linux/spi/spi.h>
+#include <linux/spi/offload/consumer.h>
+#include <linux/spi/offload/provider.h>
#include <linux/units.h>
#include <linux/unaligned.h>
#include <linux/iio/buffer.h>
+#include <linux/iio/buffer-dma.h>
+#include <linux/iio/buffer-dmaengine.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/trigger.h>
@@ -44,6 +48,11 @@
#define AD4691_CNV_DUTY_CYCLE_NS 380
#define AD4691_CNV_HIGH_TIME_NS 430
+/*
+ * Conservative default for the manual offload periodic trigger. Low enough
+ * to work safely out of the box across all OSR and channel count combinations.
+ */
+#define AD4691_OFFLOAD_INITIAL_TRIGGER_HZ (100 * HZ_PER_KHZ)
#define AD4691_SPI_CONFIG_A_REG 0x000
#define AD4691_SW_RESET (BIT(7) | BIT(0))
@@ -115,6 +124,7 @@ struct ad4691_chip_info {
const char *name;
unsigned int max_rate;
const struct ad4691_channel_info *sw_info;
+ const struct ad4691_channel_info *offload_info;
};
#define AD4691_CHANNEL(ch) \
@@ -136,6 +146,30 @@ struct ad4691_chip_info {
}, \
}
+/*
+ * Offload path (bits_per_word=16): the SPI Engine assembles received
+ * bits into native 16-bit words before DMA, so samples are in
+ * CPU-native byte order (IIO_CPU). storagebits=16 matches the 16-bit
+ * DMA word size.
+ */
+#define AD4691_OFFLOAD_CHANNEL(ch) \
+ { \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) \
+ | BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate_available = \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
+ .channel = ch, \
+ .scan_index = ch, \
+ .scan_type = { \
+ .sign = 'u', \
+ .realbits = 16, \
+ .storagebits = 16, \
+ }, \
+ }
+
static const struct iio_chan_spec ad4691_channels[] = {
AD4691_CHANNEL(0),
AD4691_CHANNEL(1),
@@ -168,6 +202,40 @@ static const struct iio_chan_spec ad4693_channels[] = {
IIO_CHAN_SOFT_TIMESTAMP(8),
};
+/*
+ * Offload channel arrays: no IIO_CHAN_SOFT_TIMESTAMP because DMA delivers
+ * data directly to userspace without a software timestamp.
+ */
+static const struct iio_chan_spec ad4691_offload_channels[] = {
+ AD4691_OFFLOAD_CHANNEL(0),
+ AD4691_OFFLOAD_CHANNEL(1),
+ AD4691_OFFLOAD_CHANNEL(2),
+ AD4691_OFFLOAD_CHANNEL(3),
+ AD4691_OFFLOAD_CHANNEL(4),
+ AD4691_OFFLOAD_CHANNEL(5),
+ AD4691_OFFLOAD_CHANNEL(6),
+ AD4691_OFFLOAD_CHANNEL(7),
+ AD4691_OFFLOAD_CHANNEL(8),
+ AD4691_OFFLOAD_CHANNEL(9),
+ AD4691_OFFLOAD_CHANNEL(10),
+ AD4691_OFFLOAD_CHANNEL(11),
+ AD4691_OFFLOAD_CHANNEL(12),
+ AD4691_OFFLOAD_CHANNEL(13),
+ AD4691_OFFLOAD_CHANNEL(14),
+ AD4691_OFFLOAD_CHANNEL(15),
+};
+
+static const struct iio_chan_spec ad4693_offload_channels[] = {
+ AD4691_OFFLOAD_CHANNEL(0),
+ AD4691_OFFLOAD_CHANNEL(1),
+ AD4691_OFFLOAD_CHANNEL(2),
+ AD4691_OFFLOAD_CHANNEL(3),
+ AD4691_OFFLOAD_CHANNEL(4),
+ AD4691_OFFLOAD_CHANNEL(5),
+ AD4691_OFFLOAD_CHANNEL(6),
+ AD4691_OFFLOAD_CHANNEL(7),
+};
+
static const struct ad4691_channel_info ad4691_sw_info = {
.channels = ad4691_channels,
.num_channels = ARRAY_SIZE(ad4691_channels),
@@ -178,6 +246,16 @@ static const struct ad4691_channel_info ad4693_sw_info = {
.num_channels = ARRAY_SIZE(ad4693_channels),
};
+static const struct ad4691_channel_info ad4691_offload_info = {
+ .channels = ad4691_offload_channels,
+ .num_channels = ARRAY_SIZE(ad4691_offload_channels),
+};
+
+static const struct ad4691_channel_info ad4693_offload_info = {
+ .channels = ad4693_offload_channels,
+ .num_channels = ARRAY_SIZE(ad4693_offload_channels),
+};
+
/*
* Internal oscillator frequency table. Index is the OSC_FREQ_REG[3:0] value.
* Index 0 (1 MHz) is only valid for AD4692/AD4694; AD4691/AD4693 support
@@ -208,24 +286,34 @@ static const struct ad4691_chip_info ad4691_chip_info = {
.name = "ad4691",
.max_rate = 500 * HZ_PER_KHZ,
.sw_info = &ad4691_sw_info,
+ .offload_info = &ad4691_offload_info,
};
static const struct ad4691_chip_info ad4692_chip_info = {
.name = "ad4692",
.max_rate = 1 * HZ_PER_MHZ,
.sw_info = &ad4691_sw_info,
+ .offload_info = &ad4691_offload_info,
};
static const struct ad4691_chip_info ad4693_chip_info = {
.name = "ad4693",
.max_rate = 500 * HZ_PER_KHZ,
.sw_info = &ad4693_sw_info,
+ .offload_info = &ad4693_offload_info,
};
static const struct ad4691_chip_info ad4694_chip_info = {
.name = "ad4694",
.max_rate = 1 * HZ_PER_MHZ,
.sw_info = &ad4693_sw_info,
+ .offload_info = &ad4693_offload_info,
+};
+
+struct ad4691_offload_state {
+ struct spi_offload *offload;
+ struct spi_offload_trigger *trigger;
+ u64 trigger_hz;
};
struct ad4691_state {
@@ -260,8 +348,11 @@ struct ad4691_state {
struct spi_transfer scan_xfers[34];
/*
* CNV burst: 16 AVG_IN addresses = 16. Manual: 16 channel cmds +
- * 1 NOOP = 17. Stored as native u16; put_unaligned_be16() fills each
- * slot so the SPI controller (bits_per_word=8) sends bytes MSB-first.
+ * 1 NOOP = 17. Stored as native u16. The non-offload path fills slots
+ * with put_unaligned_be16() (bits_per_word=8, bytes go out in memory
+ * order). The offload path assigns native values directly
+ * (bits_per_word=bpw, SPI reads each slot as a native 16-bit word and
+ * shifts it out MSB-first).
*/
u16 scan_tx[17] __aligned(IIO_DMA_MINALIGN);
/*
@@ -277,6 +368,8 @@ struct ad4691_state {
* DMA-aligned because scan_xfers point rx_buf directly into vals[].
*/
IIO_DECLARE_DMA_BUFFER_WITH_TS(__be16, vals, 16);
+ /* NULL when no SPI offload hardware is present */
+ struct ad4691_offload_state *offload;
};
/*
@@ -296,6 +389,46 @@ static int ad4691_gpio_setup(struct ad4691_state *st, unsigned int gp_num)
AD4691_GP_MODE_DATA_READY << shift);
}
+static const struct spi_offload_config ad4691_offload_config = {
+ .capability_flags = SPI_OFFLOAD_CAP_TRIGGER |
+ SPI_OFFLOAD_CAP_RX_STREAM_DMA,
+};
+
+static bool ad4691_offload_trigger_match(struct spi_offload_trigger *trigger,
+ enum spi_offload_trigger_type type,
+ u64 *args, u32 nargs)
+{
+ return type == SPI_OFFLOAD_TRIGGER_DATA_READY &&
+ nargs == 1 && args[0] <= 3;
+}
+
+static int ad4691_offload_trigger_request(struct spi_offload_trigger *trigger,
+ enum spi_offload_trigger_type type,
+ u64 *args, u32 nargs)
+{
+ struct ad4691_state *st = spi_offload_trigger_get_priv(trigger);
+
+ if (nargs != 1)
+ return -EINVAL;
+
+ return ad4691_gpio_setup(st, args[0]);
+}
+
+static int ad4691_offload_trigger_validate(struct spi_offload_trigger *trigger,
+ struct spi_offload_trigger_config *config)
+{
+ if (config->type != SPI_OFFLOAD_TRIGGER_DATA_READY)
+ return -EINVAL;
+
+ return 0;
+}
+
+static const struct spi_offload_trigger_ops ad4691_offload_trigger_ops = {
+ .match = ad4691_offload_trigger_match,
+ .request = ad4691_offload_trigger_request,
+ .validate = ad4691_offload_trigger_validate,
+};
+
static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
{
struct spi_device *spi = context;
@@ -873,6 +1006,222 @@ static const struct iio_buffer_setup_ops ad4691_cnv_burst_buffer_setup_ops = {
.postdisable = &ad4691_cnv_burst_buffer_postdisable,
};
+static int ad4691_manual_offload_buffer_postenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+ struct device *dev = regmap_get_device(st->regmap);
+ struct spi_device *spi = to_spi_device(dev);
+ struct spi_offload_trigger_config config = {
+ .type = SPI_OFFLOAD_TRIGGER_PERIODIC,
+ };
+ unsigned int bpw = indio_dev->channels[0].scan_type.realbits;
+ unsigned int bit, k;
+ int ret;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret)
+ return ret;
+
+ memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
+ memset(st->scan_tx, 0, sizeof(st->scan_tx));
+
+ /*
+ * N+1 transfers for N channels. Each CS-low period triggers
+ * a conversion AND returns the previous result (pipelined).
+ * TX: [AD4691_ADC_CHAN(n), 0x00]
+ * RX: [data_hi, data_lo] (storagebits=16, shift=0)
+ * Transfer 0 RX is garbage; transfers 1..N carry real data.
+ * scan_tx is reused for TX commands (mutually exclusive with the
+ * non-offload triggered-buffer path).
+ *
+ * bits_per_word=bpw: the SPI controller reads tx_buf as a native
+ * 16-bit word and shifts it out MSB-first. Store the exact 16-bit
+ * value we want on the wire as a plain native u16 — no endianness
+ * macro — so the wire bytes are correct on both LE and BE hosts.
+ * The channel-select command is a single byte; shift it to the MSB
+ * position so SPI sends it first, with a zero pad in the LSB.
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, bit) {
+ st->scan_tx[k] = (u16)(AD4691_ADC_CHAN(bit) << 8);
+ st->scan_xfers[k].tx_buf = &st->scan_tx[k];
+ st->scan_xfers[k].len = sizeof(st->scan_tx[k]);
+ st->scan_xfers[k].bits_per_word = bpw;
+ st->scan_xfers[k].cs_change = 1;
+ st->scan_xfers[k].cs_change_delay.value = AD4691_CNV_HIGH_TIME_NS;
+ st->scan_xfers[k].cs_change_delay.unit = SPI_DELAY_UNIT_NSECS;
+ /* First transfer RX is garbage — skip it. */
+ if (k > 0)
+ st->scan_xfers[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
+ k++;
+ }
+
+ /* Final NOOP transfer retrieves the last channel's result. */
+ st->scan_xfers[k].tx_buf = &st->scan_tx[k]; /* scan_tx[k] == 0 == NOOP */
+ st->scan_xfers[k].len = sizeof(st->scan_tx[k]);
+ st->scan_xfers[k].bits_per_word = bpw;
+ st->scan_xfers[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
+ k++;
+
+ spi_message_init_with_transfers(&st->scan_msg, st->scan_xfers, k);
+ st->scan_msg.offload = offload->offload;
+
+ ret = spi_optimize_message(spi, &st->scan_msg);
+ if (ret)
+ goto err_exit_conversion;
+
+ config.periodic.frequency_hz = offload->trigger_hz;
+ ret = spi_offload_trigger_enable(offload->offload, offload->trigger, &config);
+ if (ret)
+ goto err_unoptimize;
+
+ return 0;
+
+err_unoptimize:
+ spi_unoptimize_message(&st->scan_msg);
+err_exit_conversion:
+ ad4691_exit_conversion_mode(st);
+ return ret;
+}
+
+static int ad4691_manual_offload_buffer_predisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+
+ spi_offload_trigger_disable(offload->offload, offload->trigger);
+ spi_unoptimize_message(&st->scan_msg);
+
+ return ad4691_exit_conversion_mode(st);
+}
+
+static const struct iio_buffer_setup_ops ad4691_manual_offload_buffer_setup_ops = {
+ .postenable = &ad4691_manual_offload_buffer_postenable,
+ .predisable = &ad4691_manual_offload_buffer_predisable,
+};
+
+static int ad4691_cnv_burst_offload_buffer_postenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+ struct device *dev = regmap_get_device(st->regmap);
+ struct spi_device *spi = to_spi_device(dev);
+ struct spi_offload_trigger_config config = {
+ .type = SPI_OFFLOAD_TRIGGER_DATA_READY,
+ };
+ unsigned int bpw = indio_dev->channels[0].scan_type.realbits;
+ unsigned int acc_mask, std_seq_config;
+ unsigned int bit, k;
+ int ret;
+
+ std_seq_config = bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)) & GENMASK(15, 0);
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG, std_seq_config);
+ if (ret)
+ return ret;
+
+ acc_mask = ~std_seq_config & GENMASK(15, 0);
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG, acc_mask);
+ if (ret)
+ return ret;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret)
+ return ret;
+
+ memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
+ memset(st->scan_tx, 0, sizeof(st->scan_tx));
+
+ /*
+ * Each AVG_IN register read uses two transfers:
+ * TX: [reg_hi | 0x80, reg_lo] (address phase, CS stays asserted)
+ * RX: [data_hi, data_lo] (bpw-wide data phase, storagebits=16)
+ * Both TX and RX use bits_per_word=bpw: the SPI controller reads tx_buf
+ * as a native 16-bit word and shifts it out MSB-first. Store the exact
+ * 16-bit wire value as a plain native u16 — no endianness macro — so the
+ * wire bytes are correct on both LE and BE hosts. The read-address
+ * (0x8000 | reg) is already the 16-bit value we want on the wire.
+ * scan_tx is reused for TX addresses (mutually exclusive with the
+ * non-offload triggered-buffer path).
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, bit) {
+ st->scan_tx[k] = 0x8000 | AD4691_AVG_IN(bit);
+
+ /* TX: address phase, CS stays asserted into data phase */
+ st->scan_xfers[2 * k].tx_buf = &st->scan_tx[k];
+ st->scan_xfers[2 * k].len = sizeof(st->scan_tx[k]);
+ st->scan_xfers[2 * k].bits_per_word = bpw;
+
+ /* RX: data phase, CS toggles after to delimit the next register op */
+ st->scan_xfers[2 * k + 1].len = sizeof(st->scan_tx[k]);
+ st->scan_xfers[2 * k + 1].bits_per_word = bpw;
+ st->scan_xfers[2 * k + 1].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+ k++;
+ }
+
+ /*
+ * State reset: single 4-byte write [addr_hi, addr_lo, STATE_RESET_ALL,
+ * OSC_EN=1]. ADDR_DESCENDING writes byte[3]=1 to OSC_EN_REG (0x180) as
+ * a deliberate side-write, keeping the oscillator enabled.
+ * scan_tx_reset is shared with the non-offload path (len=4 here vs
+ * len=3 there) since the two paths are mutually exclusive at probe.
+ */
+ put_unaligned_be16(AD4691_STATE_RESET_REG, st->scan_tx_reset);
+ st->scan_tx_reset[2] = AD4691_STATE_RESET_ALL;
+ st->scan_tx_reset[3] = 1;
+ st->scan_xfers[2 * k].tx_buf = st->scan_tx_reset;
+ st->scan_xfers[2 * k].len = sizeof(st->scan_tx_reset);
+ /*
+ * 4-byte u8 buffer assembled with put_unaligned_be16(); leave
+ * bits_per_word at the default (8) so bytes go out in memory order.
+ */
+
+ spi_message_init_with_transfers(&st->scan_msg, st->scan_xfers, 2 * k + 1);
+ st->scan_msg.offload = offload->offload;
+
+ ret = spi_optimize_message(spi, &st->scan_msg);
+ if (ret)
+ goto err_exit_conversion;
+
+ ret = spi_offload_trigger_enable(offload->offload, offload->trigger, &config);
+ if (ret)
+ goto err_unoptimize;
+
+ ret = ad4691_sampling_enable(st, true);
+ if (ret)
+ goto err_disable_trigger;
+
+ return 0;
+
+err_disable_trigger:
+ spi_offload_trigger_disable(offload->offload, offload->trigger);
+err_unoptimize:
+ spi_unoptimize_message(&st->scan_msg);
+err_exit_conversion:
+ ad4691_exit_conversion_mode(st);
+ return ret;
+}
+
+static int ad4691_cnv_burst_offload_buffer_predisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+
+ ad4691_sampling_enable(st, false);
+ spi_offload_trigger_disable(offload->offload, offload->trigger);
+ spi_unoptimize_message(&st->scan_msg);
+
+ return ad4691_exit_conversion_mode(st);
+}
+
+static const struct iio_buffer_setup_ops ad4691_cnv_burst_offload_buffer_setup_ops = {
+ .postenable = &ad4691_cnv_burst_offload_buffer_postenable,
+ .predisable = &ad4691_cnv_burst_offload_buffer_predisable,
+};
+
static ssize_t sampling_frequency_show(struct device *dev,
struct device_attribute *attr,
char *buf)
@@ -880,6 +1229,9 @@ static ssize_t sampling_frequency_show(struct device *dev,
struct iio_dev *indio_dev = dev_to_iio_dev(dev);
struct ad4691_state *st = iio_priv(indio_dev);
+ if (st->manual_mode && st->offload)
+ return sysfs_emit(buf, "%llu\n", READ_ONCE(st->offload->trigger_hz));
+
return sysfs_emit(buf, "%lu\n", NSEC_PER_SEC / st->cnv_period_ns);
}
@@ -900,6 +1252,20 @@ static ssize_t sampling_frequency_store(struct device *dev,
if (IIO_DEV_ACQUIRE_FAILED(claim))
return -EBUSY;
+ if (st->manual_mode && st->offload) {
+ struct spi_offload_trigger_config config = {
+ .type = SPI_OFFLOAD_TRIGGER_PERIODIC,
+ .periodic = { .frequency_hz = freq },
+ };
+
+ ret = spi_offload_trigger_validate(st->offload->trigger, &config);
+ if (ret)
+ return ret;
+
+ WRITE_ONCE(st->offload->trigger_hz, config.periodic.frequency_hz);
+ return len;
+ }
+
ret = ad4691_set_pwm_freq(st, freq);
if (ret)
return ret;
@@ -1239,9 +1605,83 @@ static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
ad4691_buffer_attrs);
}
+static int ad4691_setup_offload(struct iio_dev *indio_dev,
+ struct ad4691_state *st,
+ struct spi_offload *spi_offload)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ struct ad4691_offload_state *offload;
+ struct dma_chan *rx_dma;
+ int ret;
+
+ offload = devm_kzalloc(dev, sizeof(*offload), GFP_KERNEL);
+ if (!offload)
+ return -ENOMEM;
+
+ offload->offload = spi_offload;
+ st->offload = offload;
+
+ indio_dev->channels = st->info->offload_info->channels;
+ indio_dev->num_channels = st->info->offload_info->num_channels;
+ /*
+ * Offload path uses DMA directly; no IIO trigger is involved, so
+ * external triggers are not restricted (no validate_trigger).
+ */
+ indio_dev->info = &ad4691_manual_info;
+
+ if (st->manual_mode) {
+ offload->trigger =
+ devm_spi_offload_trigger_get(dev, offload->offload,
+ SPI_OFFLOAD_TRIGGER_PERIODIC);
+ if (IS_ERR(offload->trigger))
+ return dev_err_probe(dev, PTR_ERR(offload->trigger),
+ "Failed to get periodic offload trigger\n");
+
+ offload->trigger_hz = AD4691_OFFLOAD_INITIAL_TRIGGER_HZ;
+ } else {
+ struct spi_offload_trigger_info trigger_info = {
+ .fwnode = dev_fwnode(dev),
+ .ops = &ad4691_offload_trigger_ops,
+ .priv = st,
+ };
+
+ ret = devm_spi_offload_trigger_register(dev, &trigger_info);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "Failed to register offload trigger\n");
+
+ offload->trigger =
+ devm_spi_offload_trigger_get(dev, offload->offload,
+ SPI_OFFLOAD_TRIGGER_DATA_READY);
+ if (IS_ERR(offload->trigger))
+ return dev_err_probe(dev, PTR_ERR(offload->trigger),
+ "Failed to get DATA_READY offload trigger\n");
+ }
+
+ rx_dma = devm_spi_offload_rx_stream_request_dma_chan(dev, offload->offload);
+ if (IS_ERR(rx_dma))
+ return dev_err_probe(dev, PTR_ERR(rx_dma),
+ "Failed to get offload RX DMA channel\n");
+
+ if (st->manual_mode)
+ indio_dev->setup_ops = &ad4691_manual_offload_buffer_setup_ops;
+ else
+ indio_dev->setup_ops = &ad4691_cnv_burst_offload_buffer_setup_ops;
+
+ ret = devm_iio_dmaengine_buffer_setup_with_handle(dev, indio_dev, rx_dma,
+ IIO_BUFFER_DIRECTION_IN);
+ if (ret)
+ return ret;
+
+ indio_dev->buffer->attrs = ad4691_buffer_attrs;
+
+ return 0;
+}
+
static int ad4691_probe(struct spi_device *spi)
{
struct device *dev = &spi->dev;
+ struct spi_offload *spi_offload;
struct iio_dev *indio_dev;
struct ad4691_state *st;
int ret;
@@ -1277,10 +1717,20 @@ static int ad4691_probe(struct spi_device *spi)
if (ret)
return ret;
+ spi_offload = devm_spi_offload_get(dev, spi, &ad4691_offload_config);
+ ret = PTR_ERR_OR_ZERO(spi_offload);
+ if (ret == -ENODEV)
+ spi_offload = NULL;
+ else if (ret)
+ return dev_err_probe(dev, ret, "Failed to get SPI offload\n");
+
indio_dev->name = st->info->name;
indio_dev->modes = INDIO_DIRECT_MODE;
- ret = ad4691_setup_triggered_buffer(indio_dev, st);
+ if (spi_offload)
+ ret = ad4691_setup_offload(indio_dev, st, spi_offload);
+ else
+ ret = ad4691_setup_triggered_buffer(indio_dev, st);
if (ret)
return ret;
@@ -1318,3 +1768,5 @@ module_spi_driver(ad4691_driver);
MODULE_AUTHOR("Radu Sabau <radu.sabau@analog.com>");
MODULE_DESCRIPTION("Analog Devices AD4691 Family ADC Driver");
MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("IIO_DMA_BUFFER");
+MODULE_IMPORT_NS("IIO_DMAENGINE_BUFFER");
--
2.43.0
^ permalink raw reply related
* [GIT PULL] Updating the security-bugs document
From: Jonathan Corbet @ 2026-05-15 13:35 UTC (permalink / raw)
To: Linus Torvalds; +Cc: linux-doc, linux-kernel, Willy Tarreau, Greg Kroah-Hartman
The following changes since commit 254f49634ee16a731174d2ae34bc50bd5f45e731:
Linux 7.1-rc1 (2026-04-26 14:19:00 -0700)
are available in the Git repository at:
git://git.kernel.org/pub/scm/linux/kernel/git/docs/linux.git tags/docs-7.1-fixes
for you to fetch changes up to f2e65e4e5b4b4b9ecf43f03c3fdbe8c9a8a43a9e:
docs: threat-model: don't limit root capabilities to CAP_SYS_ADMIN (2026-05-14 06:23:44 -0600)
----------------------------------------------------------------
This is Willy Tarreau's new document clarifying the definition and handling
of security-related bugs, which we're trying to get out there quickly on
the theory that some of the bug reporters might actually read and pay
attention to it.
----------------------------------------------------------------
Jonathan Corbet (2):
docs: security-bugs: add a link to the threat-model documentation
docs: threat-model: don't limit root capabilities to CAP_SYS_ADMIN
Willy Tarreau (3):
Documentation: security-bugs: do not systematically Cc the security team
Documentation: security-bugs: explain what is and is not a security bug
Documentation: security-bugs: clarify requirements for AI-assisted reports
Documentation/process/index.rst | 1 +
Documentation/process/security-bugs.rst | 106 +++++++++++++-
Documentation/process/threat-model.rst | 235 ++++++++++++++++++++++++++++++++
3 files changed, 340 insertions(+), 2 deletions(-)
create mode 100644 Documentation/process/threat-model.rst
^ permalink raw reply
* Re: [PATCH v13 1/4] bug/kunit: Core support for suppressing warning backtraces
From: Albert Esteve @ 2026-05-15 13:36 UTC (permalink / raw)
To: Arnd Bergmann, Brendan Higgins, David Gow, Rae Moar,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Shuah Khan, Andrew Morton,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: linux-kernel, linux-arch, linux-kselftest, kunit-dev, dri-devel,
workflows, linux-riscv, linux-doc, peterz, Alessandro Carminati,
Guenter Roeck, Kees Cook
In-Reply-To: <20260515-kunit_add_support-v13-1-18ee42f96e7b@redhat.com>
On Fri, May 15, 2026 at 2:29 PM Albert Esteve <aesteve@redhat.com> wrote:
>
> From: Alessandro Carminati <acarmina@redhat.com>
>
> Some unit tests intentionally trigger warning backtraces by passing bad
> parameters to kernel API functions. Such unit tests typically check the
> return value from such calls, not the existence of the warning backtrace.
>
> Such intentionally generated warning backtraces are neither desirable
> nor useful for a number of reasons:
> - They can result in overlooked real problems.
> - A warning that suddenly starts to show up in unit tests needs to be
> investigated and has to be marked to be ignored, for example by
> adjusting filter scripts. Such filters are ad hoc because there is
> no real standard format for warnings. On top of that, such filter
> scripts would require constant maintenance.
>
> Solve the problem by providing a means to suppress warning backtraces
> originating from the current kthread while executing test code. Since
> each KUnit test runs in its own kthread, this effectively scopes
> suppression to the test that enabled it. Limit changes to generic code
> to the absolute minimum.
>
> Implementation details:
> Suppression is integrated into the existing KUnit hooks infrastructure
> in test-bug.h, reusing the kunit_running static branch for zero
> overhead when no tests are running.
>
> Suppression is checked at three points in the warning path:
> - In warn_slowpath_fmt(), the check runs before any output, fully
> suppressing both message and backtrace. This covers architectures
> without __WARN_FLAGS.
> - In __warn_printk(), the check suppresses the warning message text.
> This covers architectures that define __WARN_FLAGS but not their own
> __WARN_printf (arm64, loongarch, parisc, powerpc, riscv, sh), where
> the message is printed before the trap enters __report_bug().
> - In __report_bug(), the check runs before __warn() is called,
> suppressing the backtrace and stack dump.
>
> To avoid double-counting on architectures where both __warn_printk()
> and __report_bug() run for the same warning, kunit_is_suppressed_warning()
> takes a bool parameter: true to increment the suppression counter
> (used in warn_slowpath_fmt and __report_bug), false to check only
> (used in __warn_printk).
>
> The suppression state is dynamically allocated via kunit_kzalloc() and
> tied to the KUnit test lifecycle via kunit_add_action(), ensuring
> automatic cleanup at test exit. On cleanup, the node is removed with
> list_del_rcu() followed by synchronize_rcu() to wait for any concurrent
> RCU readers to finish. Because kunit_end_suppress_warning() (and the
> __cleanup wrapper) always runs from process context, synchronize_rcu()
> is safe. The handle memory remains valid until the test exits, so the
> suppression count can be read after the scope closes. Writer-side
> access to the global suppression list is serialized with a spinlock;
> readers use RCU. To avoid false suppression of warnings fired from
> hardware interrupt handlers (where current still points to the test
> task), the check exits early when not in task context.
>
> Two API forms are provided:
> - kunit_warning_suppress(test) { ... }: scoped, uses __cleanup for
> automatic teardown on scope exit, kunit_add_action() as safety net
> for abnormal exits (e.g. kthread_exit from failed assertions).
> Suppression handle is only accessible inside the block.
> - kunit_start/end_suppress_warning(test): direct functions returning
> an explicit handle, for retaining the handle within the test,
> or for cross-function usage.
Let me address sashiko's comments for
https://sashiko.dev/#/patchset/20260515-kunit_add_support-v13-0-18ee42f96e7b%40redhat.com?part=1
here:
1. "Is this assumption always accurate? Tests frequently acquire
spinlocks or RCU read locks."
The assumption is accurate because kunit_end_suppress_warning() and
the __cleanup wrapper fire at the closing brace of the
kunit_warning_suppress() scope. If a developer holds a spinlock or RCU
read lock when that scope closes, their test is structurally incorrect
regardless of this API. The API documentation notes that process
context is required.
2. "If kunit_start_suppress_warning() fails and returns NULL, will
this skip the entire loop body?"
Yes, intentionally. KUNIT_FAIL() is called before returning NULL, so
the test is already marked as failed at that point. Skipping the body
of a failed test is expected KUnit behavior. In patch 3,
scaling_factor is initialized to INT_MIN precisely for this reason.
3. "Does this mean the single-fire budget is consumed anyway on
non-CONFIG_GENERIC_BUG architectures?"
Yes, true, but it should only affect non-__WARN_FLAGS architectures.
If there is demand, it can be addressed in a follow-up series. It does
not affect current API users.
4. "Would GFP_KERNEL / synchronize_rcu() cause a sleep-in-atomic bug
if used in atomic context?"
Yes, and that would be a test design error. kunit_warning_suppress()
is a KUnit test API; KUnit tests run in process context by design. As
stated in `Documentation/core-api/memory-allocation.rst`, GFP_KERNEL
implies GFP_RECLAIM, which requires the calling context to be allowed
to sleep. Using it in an atomic context is incorrect regardless of
which API calls it. `kunit_kzalloc(test, ..., GFP_KERNEL)` is the
standard allocation pattern throughout KUnit itself (e.g.,
lib/kunit/assert_test.c, platform-test.c, ...), so this API follows
the same convention.
5. "Could a child kthread's task_struct be freed and reused, causing
false suppression?"
The API is designed to be called from the test task only. w->task =
current stores the caller's task_struct, and the inline comment
explains the stability guarantee: the test task cannot exit before
KUnit tears down the test. The correct pattern for child kthread is
demonstrated in backtrace_suppression_test_cross_kthread: the test
task opens and closes the suppression scope; child threads only read
the suppression state. Otherwise it is an API misuse.
>
> Signed-off-by: Guenter Roeck <linux@roeck-us.net>
> Signed-off-by: Alessandro Carminati <acarmina@redhat.com>
> Reviewed-by: Kees Cook <kees@kernel.org>
> Reviewed-by: David Gow <david@davidgow.net>
> Signed-off-by: Albert Esteve <aesteve@redhat.com>
> ---
> include/kunit/test-bug.h | 26 ++++++++++
> include/kunit/test.h | 98 ++++++++++++++++++++++++++++++++++++
> kernel/panic.c | 11 ++++
> lib/bug.c | 12 ++++-
> lib/kunit/Makefile | 3 +-
> lib/kunit/bug.c | 127 +++++++++++++++++++++++++++++++++++++++++++++++
> lib/kunit/hooks-impl.h | 2 +
> 7 files changed, 276 insertions(+), 3 deletions(-)
>
> diff --git a/include/kunit/test-bug.h b/include/kunit/test-bug.h
> index 47aa8f21ccce8..99869029fc686 100644
> --- a/include/kunit/test-bug.h
> +++ b/include/kunit/test-bug.h
> @@ -10,6 +10,7 @@
> #define _KUNIT_TEST_BUG_H
>
> #include <linux/stddef.h> /* for NULL */
> +#include <linux/types.h> /* for bool */
>
> #if IS_ENABLED(CONFIG_KUNIT)
>
> @@ -23,6 +24,7 @@ DECLARE_STATIC_KEY_FALSE(kunit_running);
> extern struct kunit_hooks_table {
> __printf(3, 4) void (*fail_current_test)(const char*, int, const char*, ...);
> void *(*get_static_stub_address)(struct kunit *test, void *real_fn_addr);
> + bool (*is_suppressed_warning)(bool count);
> } kunit_hooks;
>
> /**
> @@ -60,9 +62,33 @@ static inline struct kunit *kunit_get_current_test(void)
> } \
> } while (0)
>
> +/**
> + * kunit_is_suppressed_warning() - Check if warnings are being suppressed
> + * by the current KUnit test.
> + * @count: if true, increment the suppression counter on match.
> + *
> + * Returns true if the current task has active warning suppression.
> + * Uses the kunit_running static branch for zero overhead when no tests run.
> + *
> + * A single WARN*() may traverse multiple call sites in the warning path
> + * (e.g., __warn_printk() and __report_bug()). Pass @count = true at the
> + * primary suppression point to count each warning exactly once, and
> + * @count = false at secondary points to suppress output without
> + * inflating the count.
> + */
> +static inline bool kunit_is_suppressed_warning(bool count)
> +{
> + if (!static_branch_unlikely(&kunit_running))
> + return false;
> +
> + return kunit_hooks.is_suppressed_warning &&
> + kunit_hooks.is_suppressed_warning(count);
> +}
> +
> #else
>
> static inline struct kunit *kunit_get_current_test(void) { return NULL; }
> +static inline bool kunit_is_suppressed_warning(bool count) { return false; }
>
> #define kunit_fail_current_test(fmt, ...) do {} while (0)
>
> diff --git a/include/kunit/test.h b/include/kunit/test.h
> index 9cd1594ab697d..be71612f61655 100644
> --- a/include/kunit/test.h
> +++ b/include/kunit/test.h
> @@ -1795,4 +1795,102 @@ do { \
> // include resource.h themselves if they need it.
> #include <kunit/resource.h>
>
> +/*
> + * Warning backtrace suppression API.
> + *
> + * Suppresses WARN*() backtraces on the current task while active. Two forms
> + * are provided:
> + *
> + * - Scoped: kunit_warning_suppress(test) { ... }
> + * Suppression is active for the duration of the block. On normal exit,
> + * the for-loop increment deactivates suppression. On early exit (break,
> + * return, goto), the __cleanup attribute fires. On kthread_exit() (e.g.,
> + * a failed KUnit assertion), kunit_add_action() cleans up at test
> + * teardown. The suppression handle is only accessible inside the block,
> + * so warning counts must be checked before the block exits.
> + *
> + * - Direct: kunit_start_suppress_warning() / kunit_end_suppress_warning()
> + * The underlying functions, returning an explicit handle pointer. Use
> + * when the handle needs to be retained (e.g., for post-suppression
> + * count checks) or passed across helper functions.
> + */
> +struct kunit_suppressed_warning;
> +
> +struct kunit_suppressed_warning *
> +kunit_start_suppress_warning(struct kunit *test);
> +void kunit_end_suppress_warning(struct kunit *test,
> + struct kunit_suppressed_warning *w);
> +int kunit_suppressed_warning_count(struct kunit_suppressed_warning *w);
> +void __kunit_suppress_auto_cleanup(struct kunit_suppressed_warning **wp);
> +bool kunit_has_active_suppress_warning(void);
> +
> +/**
> + * kunit_warning_suppress() - Suppress WARN*() backtraces for the duration
> + * of a block.
> + * @test: The test context object.
> + *
> + * Scoped form of the suppression API. Suppression starts when the block is
> + * entered and ends automatically when the block exits through any path. See
> + * the section comment above for the cleanup guarantees on each exit path.
> + * Fails the test if suppression is already active; nesting is not supported.
> + *
> + * The warning count can be checked inside the block via
> + * KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(). The handle is not accessible
> + * after the block exits.
> + *
> + * Example::
> + *
> + * kunit_warning_suppress(test) {
> + * trigger_warning();
> + * KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1);
> + * }
> + */
> +#define kunit_warning_suppress(test) \
> + for (struct kunit_suppressed_warning *__kunit_suppress \
> + __cleanup(__kunit_suppress_auto_cleanup) = \
> + kunit_start_suppress_warning(test); \
> + __kunit_suppress; \
> + kunit_end_suppress_warning(test, __kunit_suppress), \
> + __kunit_suppress = NULL)
> +
> +/**
> + * KUNIT_SUPPRESSED_WARNING_COUNT() - Returns the suppressed warning count.
> + *
> + * Returns the number of WARN*() calls suppressed since the current
> + * suppression block started, or 0 if the handle is NULL. Usable inside a
> + * kunit_warning_suppress() block.
> + */
> +#define KUNIT_SUPPRESSED_WARNING_COUNT() \
> + kunit_suppressed_warning_count(__kunit_suppress)
> +
> +/**
> + * KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT() - Sets an expectation that the
> + * suppressed warning count equals
> + * @expected.
> + * @test: The test context object.
> + * @expected: an expression that evaluates to the expected warning count.
> + *
> + * Sets an expectation that the number of suppressed WARN*() calls equals
> + * @expected. This is semantically equivalent to
> + * KUNIT_EXPECT_EQ(@test, KUNIT_SUPPRESSED_WARNING_COUNT(), @expected).
> + * See KUNIT_EXPECT_EQ() for more information.
> + */
> +#define KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, expected) \
> + KUNIT_EXPECT_EQ(test, KUNIT_SUPPRESSED_WARNING_COUNT(), expected)
> +
> +/**
> + * KUNIT_ASSERT_SUPPRESSED_WARNING_COUNT() - Sets an assertion that the
> + * suppressed warning count equals
> + * @expected.
> + * @test: The test context object.
> + * @expected: an expression that evaluates to the expected warning count.
> + *
> + * Sets an assertion that the number of suppressed WARN*() calls equals
> + * @expected. This is the same as KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(),
> + * except it causes an assertion failure (see KUNIT_ASSERT_TRUE()) when the
> + * assertion is not met.
> + */
> +#define KUNIT_ASSERT_SUPPRESSED_WARNING_COUNT(test, expected) \
> + KUNIT_ASSERT_EQ(test, KUNIT_SUPPRESSED_WARNING_COUNT(), expected)
> +
> #endif /* _KUNIT_TEST_H */
> diff --git a/kernel/panic.c b/kernel/panic.c
> index 20feada5319d4..213725b612aa1 100644
> --- a/kernel/panic.c
> +++ b/kernel/panic.c
> @@ -39,6 +39,7 @@
> #include <linux/sys_info.h>
> #include <trace/events/error_report.h>
> #include <asm/sections.h>
> +#include <kunit/test-bug.h>
>
> #define PANIC_TIMER_STEP 100
> #define PANIC_BLINK_SPD 18
> @@ -1124,6 +1125,11 @@ void warn_slowpath_fmt(const char *file, int line, unsigned taint,
> bool rcu = warn_rcu_enter();
> struct warn_args args;
>
> + if (kunit_is_suppressed_warning(true)) {
> + warn_rcu_exit(rcu);
> + return;
> + }
> +
> pr_warn(CUT_HERE);
>
> if (!fmt) {
> @@ -1146,6 +1152,11 @@ void __warn_printk(const char *fmt, ...)
> bool rcu = warn_rcu_enter();
> va_list args;
>
> + if (kunit_is_suppressed_warning(false)) {
> + warn_rcu_exit(rcu);
> + return;
> + }
> +
> pr_warn(CUT_HERE);
>
> va_start(args, fmt);
> diff --git a/lib/bug.c b/lib/bug.c
> index 224f4cfa4aa31..874cb4ae4d047 100644
> --- a/lib/bug.c
> +++ b/lib/bug.c
> @@ -48,6 +48,7 @@
> #include <linux/rculist.h>
> #include <linux/ftrace.h>
> #include <linux/context_tracking.h>
> +#include <kunit/test-bug.h>
>
> extern struct bug_entry __start___bug_table[], __stop___bug_table[];
>
> @@ -209,8 +210,6 @@ static enum bug_trap_type __report_bug(struct bug_entry *bug, unsigned long buga
> return BUG_TRAP_TYPE_NONE;
> }
>
> - disable_trace_on_warning();
> -
> bug_get_file_line(bug, &file, &line);
> fmt = bug_get_format(bug);
>
> @@ -220,6 +219,15 @@ static enum bug_trap_type __report_bug(struct bug_entry *bug, unsigned long buga
> no_cut = bug->flags & BUGFLAG_NO_CUT_HERE;
> has_args = bug->flags & BUGFLAG_ARGS;
>
> + /*
> + * Before the once logic so suppressed warnings do not consume
> + * the single-fire budget of WARN_ON_ONCE().
> + */
> + if (warning && kunit_is_suppressed_warning(true))
> + return BUG_TRAP_TYPE_WARN;
> +
> + disable_trace_on_warning();
> +
> if (warning && once) {
> if (done)
> return BUG_TRAP_TYPE_WARN;
> diff --git a/lib/kunit/Makefile b/lib/kunit/Makefile
> index 656f1fa35abcc..4592f9d0aa8dd 100644
> --- a/lib/kunit/Makefile
> +++ b/lib/kunit/Makefile
> @@ -10,7 +10,8 @@ kunit-objs += test.o \
> executor.o \
> attributes.o \
> device.o \
> - platform.o
> + platform.o \
> + bug.o
>
> ifeq ($(CONFIG_KUNIT_DEBUGFS),y)
> kunit-objs += debugfs.o
> diff --git a/lib/kunit/bug.c b/lib/kunit/bug.c
> new file mode 100644
> index 0000000000000..6752b497aeefe
> --- /dev/null
> +++ b/lib/kunit/bug.c
> @@ -0,0 +1,127 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * KUnit helpers for backtrace suppression
> + *
> + * Copyright (C) 2025 Alessandro Carminati <acarmina@redhat.com>
> + * Copyright (C) 2024 Guenter Roeck <linux@roeck-us.net>
> + */
> +
> +#include <kunit/resource.h>
> +#include <linux/export.h>
> +#include <linux/rculist.h>
> +#include <linux/sched.h>
> +#include <linux/spinlock.h>
> +
> +#include "hooks-impl.h"
> +
> +struct kunit_suppressed_warning {
> + struct list_head node;
> + struct task_struct *task;
> + struct kunit *test;
> + atomic_t counter;
> +};
> +
> +static LIST_HEAD(suppressed_warnings);
> +static DEFINE_SPINLOCK(suppressed_warnings_lock);
> +
> +static void kunit_suppress_warning_remove(struct kunit_suppressed_warning *w)
> +{
> + unsigned long flags;
> +
> + spin_lock_irqsave(&suppressed_warnings_lock, flags);
> + list_del_rcu(&w->node);
> + spin_unlock_irqrestore(&suppressed_warnings_lock, flags);
> + synchronize_rcu(); /* Wait for readers to finish */
> +}
> +
> +KUNIT_DEFINE_ACTION_WRAPPER(kunit_suppress_warning_cleanup,
> + kunit_suppress_warning_remove,
> + struct kunit_suppressed_warning *);
> +
> +bool kunit_has_active_suppress_warning(void)
> +{
> + return __kunit_is_suppressed_warning_impl(false);
> +}
> +EXPORT_SYMBOL_GPL(kunit_has_active_suppress_warning);
> +
> +struct kunit_suppressed_warning *
> +kunit_start_suppress_warning(struct kunit *test)
> +{
> + struct kunit_suppressed_warning *w;
> + unsigned long flags;
> + int ret;
> +
> + if (kunit_has_active_suppress_warning()) {
> + KUNIT_FAIL(test, "Another suppression block is already active");
> + return NULL;
> + }
> +
> + w = kunit_kzalloc(test, sizeof(*w), GFP_KERNEL);
> + if (!w) {
> + KUNIT_FAIL(test, "Failed to allocate suppression handle.");
> + return NULL;
> + }
> +
> + /*
> + * Store current without taking a reference. The test task cannot
> + * exit before kunit tears down the test, so the pointer is stable
> + * for the lifetime of this handle.
> + */
> + w->task = current;
> + w->test = test;
> +
> + spin_lock_irqsave(&suppressed_warnings_lock, flags);
> + list_add_rcu(&w->node, &suppressed_warnings);
> + spin_unlock_irqrestore(&suppressed_warnings_lock, flags);
> +
> + ret = kunit_add_action_or_reset(test,
> + kunit_suppress_warning_cleanup, w);
> + if (ret) {
> + KUNIT_FAIL(test, "Failed to add suppression cleanup action.");
> + return NULL;
> + }
> +
> + return w;
> +}
> +EXPORT_SYMBOL_GPL(kunit_start_suppress_warning);
> +
> +void kunit_end_suppress_warning(struct kunit *test,
> + struct kunit_suppressed_warning *w)
> +{
> + if (!w)
> + return;
> + kunit_release_action(test, kunit_suppress_warning_cleanup, w);
> +}
> +EXPORT_SYMBOL_GPL(kunit_end_suppress_warning);
> +
> +void __kunit_suppress_auto_cleanup(struct kunit_suppressed_warning **wp)
> +{
> + if (*wp)
> + kunit_end_suppress_warning((*wp)->test, *wp);
> +}
> +EXPORT_SYMBOL_GPL(__kunit_suppress_auto_cleanup);
> +
> +int kunit_suppressed_warning_count(struct kunit_suppressed_warning *w)
> +{
> + return w ? atomic_read(&w->counter) : 0;
> +}
> +EXPORT_SYMBOL_GPL(kunit_suppressed_warning_count);
> +
> +bool __kunit_is_suppressed_warning_impl(bool count)
> +{
> + struct kunit_suppressed_warning *w;
> +
> + if (!in_task())
> + return false;
> +
> + guard(rcu)();
> + list_for_each_entry_rcu(w, &suppressed_warnings, node) {
> + if (w->task == current) {
> + if (count)
> + atomic_inc(&w->counter);
> + return true;
> + }
> + }
> +
> + return false;
> +}
> diff --git a/lib/kunit/hooks-impl.h b/lib/kunit/hooks-impl.h
> index 4e71b2d0143ba..d8720f2616925 100644
> --- a/lib/kunit/hooks-impl.h
> +++ b/lib/kunit/hooks-impl.h
> @@ -19,6 +19,7 @@ void __printf(3, 4) __kunit_fail_current_test_impl(const char *file,
> int line,
> const char *fmt, ...);
> void *__kunit_get_static_stub_address_impl(struct kunit *test, void *real_fn_addr);
> +bool __kunit_is_suppressed_warning_impl(bool count);
>
> /* Code to set all of the function pointers. */
> static inline void kunit_install_hooks(void)
> @@ -26,6 +27,7 @@ static inline void kunit_install_hooks(void)
> /* Install the KUnit hook functions. */
> kunit_hooks.fail_current_test = __kunit_fail_current_test_impl;
> kunit_hooks.get_static_stub_address = __kunit_get_static_stub_address_impl;
> + kunit_hooks.is_suppressed_warning = __kunit_is_suppressed_warning_impl;
> }
>
> #endif /* _KUNIT_HOOKS_IMPL_H */
>
> --
> 2.53.0
>
^ permalink raw reply
* [PATCH v2 0/6] misc: amd-sbi: Refactor SBTSI driver with I3C support and ioctl interface
From: Akshay Gupta @ 2026-05-15 13:45 UTC (permalink / raw)
To: linux-doc, linux-kernel, linux-hwmon
Cc: corbet, skhan, linux, arnd, gregkh, akshay.gupta,
naveenkrishna.chatradhi, Prathima.Lk, Anand.Umarji, Kevin.Tung,
Akshay Gupta
This series refactors the AMD SB-TSI (Side-Band Temperature Sensor
Interface) driver by moving the core from the hwmon subsystem into the
drivers/misc/amd-sbi framework, alongside the existing SB-RMI driver.
Registers an auxiliary device keeping hwmon sensors functionality intact.
Background:
The SB-TSI driver currently lives under drivers/hwmon/sbtsi_temp.c and
is limited to exposing temperature readings via the hwmon interface.
As AMD platforms evolve, SB-TSI access is required from multiple
consumers (hwmon, userspace via ioctl, I3C-attached devices), making
the hwmon-only placement insufficient.
This series restructures the driver into a layered design:
- tsi-core.c : core register access and ioctl/miscdevice support
- tsi.c : I2C/I3C probe and glue
- sbtsi_temp.c : hwmon sensor layer built on top of the core using aux device
Changes in this series:
1. Move core SBTSI driver probe from drivers/hwmon into drivers/misc/amd-sbi,
and registering an auxiliary device in core for hwmon subsystem probing
2. Register order follows the device ReadOrder bit so both parts latch atomically;
limit registers (temp / temp1_max / temp1_min) use the same helpers instead of
separate SMBus calls.
3. Move sbtsi register transfer to core abstraction to decouple the hwmon sensor
driver from the underlying bus transport. Preparing for I3C support in a
subsequent patch
4. Extend the driver to support SB-TSI over I3C in addition to I2C.
Both buses share the same core read/write path via sbtsi_xfer();
the is_i3c flag selects the underlying transport at probe time.
Backward compatibility with existing I2C deployments is maintained.
5. Add a miscdevice (/dev/sbtsi-<addr>) and an ioctl interface
(SBTSI_IOCTL_REG_XFER_CMD) that allows root userspace to perform
SB-TSI register read/write operations through the APML protocol,
consistent with the existing SBRMI ioctl interface.
6. Document the new SBTSI miscdevice and its ioctl in
Documentation/misc-devices/amd-sbi.rst.
Testing:
Tested on AMD Genoa/Turin/Venice BMC platforms with both I2C and I3C-attached
SB-TSI targets. hwmon sysfs attributes (tempX_input, tempX_max, etc.)
and ioctl register transfers verified against hardware.
Prathima (6):
hwmon/misc: amd-sbi: Move core sbtsi support from hwmon to misc
hwmon: sbtsi_temp: Refactor temperature register access into helpers
hwmon/misc: amd-sbi: Move sbtsi register transfer to core abstraction
misc: amd-sbi: Add support for SB-TSI over I3C
misc: amd-sbi: Add SBTSI ioctl register transfer interface
docs: misc: amd-sbi: Document SBTSI userspace interface
Documentation/misc-devices/amd-sbi.rst | 64 ++++++++
drivers/hwmon/Kconfig | 2 +-
drivers/hwmon/sbtsi_temp.c | 158 ++++++++++----------
drivers/misc/amd-sbi/Kconfig | 13 ++
drivers/misc/amd-sbi/Makefile | 3 +
drivers/misc/amd-sbi/tsi-core.c | 148 ++++++++++++++++++
drivers/misc/amd-sbi/tsi-core.h | 15 ++
drivers/misc/amd-sbi/tsi.c | 198 +++++++++++++++++++++++++
include/linux/misc/tsi.h | 63 ++++++++
include/uapi/misc/amd-apml.h | 23 +++
10 files changed, 604 insertions(+), 83 deletions(-)
create mode 100644 drivers/misc/amd-sbi/tsi-core.c
create mode 100644 drivers/misc/amd-sbi/tsi-core.h
create mode 100644 drivers/misc/amd-sbi/tsi.c
create mode 100644 include/linux/misc/tsi.h
--
2.34.1
^ permalink raw reply
* [PATCH v2 1/6] hwmon/misc: amd-sbi: Move core sbtsi support from hwmon to misc
From: Akshay Gupta @ 2026-05-15 13:45 UTC (permalink / raw)
To: linux-doc, linux-kernel, linux-hwmon
Cc: corbet, skhan, linux, arnd, gregkh, akshay.gupta,
naveenkrishna.chatradhi, Prathima.Lk, Anand.Umarji, Kevin.Tung,
Akshay Gupta
In-Reply-To: <20260515134506.397649-1-Akshay.Gupta@amd.com>
From: Prathima <Prathima.Lk@amd.com>
Move SBTSI(Side-Band Temperature Sensor Interface) core functionality out
of the hwmon-only path and into drivers/misc/amd-sbi so it can be reused
by non-hwmon consumers.
I2C probe parsing is moved from drivers/hwmon/sbtsi_temp.c
into drivers/misc/amd-sbi/tsi.c under CONFIG_AMD_SBTSI. The core driver
stores struct sbtsi_data on the bus device and registers an auxiliary
device amd-sbtsi.temp-sensor.<addr> per target.
This split prepares the driver for additional interfaces while keeping
hwmon support in hwmon subsystem on top of common SBTSI core logic.
Reviewed-by: Akshay Gupta <Akshay.Gupta@amd.com>
Signed-off-by: Prathima <Prathima.Lk@amd.com>
---
Changes since v1:
- Use auxiliary device to probe hwmon sensor instead of moving
the hwmon functionality to misc subsystem. This change is as
per feedback.
drivers/hwmon/Kconfig | 2 +-
drivers/hwmon/sbtsi_temp.c | 73 ++++---------------
drivers/misc/amd-sbi/Kconfig | 13 ++++
drivers/misc/amd-sbi/Makefile | 3 +
drivers/misc/amd-sbi/tsi.c | 129 ++++++++++++++++++++++++++++++++++
include/linux/misc/tsi.h | 34 +++++++++
6 files changed, 194 insertions(+), 60 deletions(-)
create mode 100644 drivers/misc/amd-sbi/tsi.c
create mode 100644 include/linux/misc/tsi.h
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 14e4cea48acc..6fa51e6ef6ff 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1939,7 +1939,7 @@ config SENSORS_SL28CPLD
config SENSORS_SBTSI
tristate "Emulated SB-TSI temperature sensor"
- depends on I2C
+ depends on AMD_SBTSI
help
If you say yes here you get support for emulated temperature
sensors on AMD SoCs with SB-TSI interface connected to a BMC device.
diff --git a/drivers/hwmon/sbtsi_temp.c b/drivers/hwmon/sbtsi_temp.c
index c5b2488c4c7f..28258bf49922 100644
--- a/drivers/hwmon/sbtsi_temp.c
+++ b/drivers/hwmon/sbtsi_temp.c
@@ -7,13 +7,12 @@
* Copyright (c) 2020, Kun Yi <kunyi@google.com>
*/
+#include <linux/auxiliary_bus.h>
#include <linux/err.h>
-#include <linux/i2c.h>
-#include <linux/init.h>
#include <linux/hwmon.h>
+#include <linux/init.h>
#include <linux/module.h>
-#include <linux/of.h>
-#include <linux/bitfield.h>
+#include <linux/misc/tsi.h>
/*
* SB-TSI registers only support SMBus byte data access. "_INT" registers are
@@ -22,39 +21,17 @@
*/
#define SBTSI_REG_TEMP_INT 0x01 /* RO */
#define SBTSI_REG_STATUS 0x02 /* RO */
-#define SBTSI_REG_CONFIG 0x03 /* RO */
#define SBTSI_REG_TEMP_HIGH_INT 0x07 /* RW */
#define SBTSI_REG_TEMP_LOW_INT 0x08 /* RW */
#define SBTSI_REG_TEMP_DEC 0x10 /* RW */
#define SBTSI_REG_TEMP_HIGH_DEC 0x13 /* RW */
#define SBTSI_REG_TEMP_LOW_DEC 0x14 /* RW */
-/*
- * Bit for reporting value with temperature measurement range.
- * bit == 0: Use default temperature range (0C to 255.875C).
- * bit == 1: Use extended temperature range (-49C to +206.875C).
- */
-#define SBTSI_CONFIG_EXT_RANGE_SHIFT 2
-/*
- * ReadOrder bit specifies the reading order of integer and decimal part of
- * CPU temperature for atomic reads. If bit == 0, reading integer part triggers
- * latching of the decimal part, so integer part should be read first.
- * If bit == 1, read order should be reversed.
- */
-#define SBTSI_CONFIG_READ_ORDER_SHIFT 5
-
#define SBTSI_TEMP_EXT_RANGE_ADJ 49000
#define SBTSI_TEMP_MIN 0
#define SBTSI_TEMP_MAX 255875
-/* Each client has this additional data */
-struct sbtsi_data {
- struct i2c_client *client;
- bool ext_range_mode;
- bool read_order;
-};
-
/*
* From SB-TSI spec: CPU temperature readings and limit registers encode the
* temperature in increments of 0.125 from 0 to 255.875. The "high byte"
@@ -195,55 +172,33 @@ static const struct hwmon_chip_info sbtsi_chip_info = {
.info = sbtsi_info,
};
-static int sbtsi_probe(struct i2c_client *client)
+static int sbtsi_probe(struct auxiliary_device *adev,
+ const struct auxiliary_device_id *id)
{
- struct device *dev = &client->dev;
+ struct sbtsi_data *data = dev_get_drvdata(adev->dev.parent);
+ struct device *dev = &adev->dev;
struct device *hwmon_dev;
- struct sbtsi_data *data;
- int err;
- data = devm_kzalloc(dev, sizeof(struct sbtsi_data), GFP_KERNEL);
- if (!data)
- return -ENOMEM;
-
- data->client = client;
-
- err = i2c_smbus_read_byte_data(data->client, SBTSI_REG_CONFIG);
- if (err < 0)
- return err;
- data->ext_range_mode = FIELD_GET(BIT(SBTSI_CONFIG_EXT_RANGE_SHIFT), err);
- data->read_order = FIELD_GET(BIT(SBTSI_CONFIG_READ_ORDER_SHIFT), err);
-
- hwmon_dev = devm_hwmon_device_register_with_info(dev, client->name, data,
+ hwmon_dev = devm_hwmon_device_register_with_info(dev, "sbtsi", data,
&sbtsi_chip_info, NULL);
return PTR_ERR_OR_ZERO(hwmon_dev);
}
-static const struct i2c_device_id sbtsi_id[] = {
- {"sbtsi"},
- {}
+static const struct auxiliary_device_id sbtsi_id[] = {
+ { .name = AMD_SBTSI_ADEV "." AMD_SBTSI_AUX_HWMON },
+ { }
};
-MODULE_DEVICE_TABLE(i2c, sbtsi_id);
+MODULE_DEVICE_TABLE(auxiliary, sbtsi_id);
-static const struct of_device_id __maybe_unused sbtsi_of_match[] = {
- {
- .compatible = "amd,sbtsi",
- },
- { },
-};
-MODULE_DEVICE_TABLE(of, sbtsi_of_match);
-
-static struct i2c_driver sbtsi_driver = {
+static struct auxiliary_driver sbtsi_driver = {
.driver = {
.name = "sbtsi",
- .of_match_table = of_match_ptr(sbtsi_of_match),
},
.probe = sbtsi_probe,
.id_table = sbtsi_id,
};
-
-module_i2c_driver(sbtsi_driver);
+module_auxiliary_driver(sbtsi_driver);
MODULE_AUTHOR("Kun Yi <kunyi@google.com>");
MODULE_DESCRIPTION("Hwmon driver for AMD SB-TSI emulated sensor");
diff --git a/drivers/misc/amd-sbi/Kconfig b/drivers/misc/amd-sbi/Kconfig
index 30e7fad7356c..512251690e0e 100644
--- a/drivers/misc/amd-sbi/Kconfig
+++ b/drivers/misc/amd-sbi/Kconfig
@@ -20,3 +20,16 @@ config AMD_SBRMI_HWMON
This provides support for RMI device hardware monitoring. If enabled,
a hardware monitoring device will be created for each socket in
the system.
+
+config AMD_SBTSI
+ tristate "AMD side band TSI support"
+ depends on I2C
+ depends on ARM || ARM64 || COMPILE_TEST
+ select AUXILIARY_BUS
+ help
+ Enables support for the AMD SB-TSI (Side Band Temperature Sensor
+ Interface) driver, which provides access to emulated CPU temperature
+ sensors on AMD SoCs via an I2C connected BMC device.
+
+ This driver can also be built as a module. If so, the module will
+ be called sbtsi.
diff --git a/drivers/misc/amd-sbi/Makefile b/drivers/misc/amd-sbi/Makefile
index 38eaaa651fd9..28f95b9e204f 100644
--- a/drivers/misc/amd-sbi/Makefile
+++ b/drivers/misc/amd-sbi/Makefile
@@ -2,3 +2,6 @@
sbrmi-i2c-objs += rmi-i2c.o rmi-core.o
sbrmi-i2c-$(CONFIG_AMD_SBRMI_HWMON) += rmi-hwmon.o
obj-$(CONFIG_AMD_SBRMI_I2C) += sbrmi-i2c.o
+# SBTSI Configuration
+sbtsi-objs += tsi.o
+obj-$(CONFIG_AMD_SBTSI) += sbtsi.o
diff --git a/drivers/misc/amd-sbi/tsi.c b/drivers/misc/amd-sbi/tsi.c
new file mode 100644
index 000000000000..ee2216785550
--- /dev/null
+++ b/drivers/misc/amd-sbi/tsi.c
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * tsi.c - AMD SBTSI I2C core driver. Probes the SBTSI device over I2C
+ * and publishes an auxiliary device on the auxiliary bus.
+ *
+ * Copyright (C) 2026 Advanced Micro Devices, Inc.
+ */
+
+#include <linux/auxiliary_bus.h>
+#include <linux/bitfield.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/misc/tsi.h>
+#include <linux/slab.h>
+
+#define SBTSI_REG_CONFIG 0x03 /* RO */
+
+/*
+ * Bit for reporting value with temperature measurement range.
+ * bit == 0: Use default temperature range (0C to 255.875C).
+ * bit == 1: Use extended temperature range (-49C to +206.875C).
+ */
+#define SBTSI_CONFIG_EXT_RANGE_SHIFT 2
+
+/*
+ * ReadOrder bit specifies the reading order of integer and decimal part of
+ * CPU temperature for atomic reads. If bit == 0, reading integer part triggers
+ * latching of the decimal part, so integer part should be read first.
+ */
+#define SBTSI_CONFIG_READ_ORDER_SHIFT 5
+
+static void sbtsi_adev_release(struct device *dev)
+{
+ kfree(to_auxiliary_dev(dev));
+}
+
+static void sbtsi_unregister_hwmon_adev(void *_adev)
+{
+ struct auxiliary_device *adev = _adev;
+
+ auxiliary_device_delete(adev);
+ auxiliary_device_uninit(adev);
+}
+
+/*
+ * Create and publish an auxiliary device. The hwmon driver in
+ * drivers/hwmon/sbtsi_temp.c binds to this device.
+ *
+ * @dev: I2C device (parent of the auxiliary device)
+ * @dev_addr: I2C address — used as the auxiliary device instance ID so that
+ * each socket gets a unique name.
+ */
+static int sbtsi_create_hwmon_adev(struct device *dev, u8 dev_addr)
+{
+ struct auxiliary_device *adev;
+ int ret;
+
+ adev = kzalloc_obj(*adev);
+ if (!adev)
+ return -ENOMEM;
+
+ adev->name = AMD_SBTSI_AUX_HWMON;
+ adev->id = dev_addr;
+ adev->dev.parent = dev;
+ adev->dev.release = sbtsi_adev_release;
+
+ ret = auxiliary_device_init(adev);
+ if (ret) {
+ kfree(adev);
+ return ret;
+ }
+
+ ret = __auxiliary_device_add(adev, AMD_SBTSI_ADEV);
+ if (ret) {
+ auxiliary_device_uninit(adev);
+ return ret;
+ }
+
+ return devm_add_action_or_reset(dev, sbtsi_unregister_hwmon_adev, adev);
+}
+
+static int sbtsi_i2c_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct sbtsi_data *data;
+ int err;
+
+ data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+
+ data->client = client;
+ err = i2c_smbus_read_byte_data(data->client, SBTSI_REG_CONFIG);
+ if (err < 0)
+ return err;
+ data->ext_range_mode = FIELD_GET(BIT(SBTSI_CONFIG_EXT_RANGE_SHIFT), err);
+ data->read_order = FIELD_GET(BIT(SBTSI_CONFIG_READ_ORDER_SHIFT), err);
+
+ dev_set_drvdata(dev, data);
+ return sbtsi_create_hwmon_adev(dev, client->addr);
+}
+
+static const struct i2c_device_id sbtsi_id[] = {
+ {"sbtsi"},
+ {}
+};
+MODULE_DEVICE_TABLE(i2c, sbtsi_id);
+
+static const struct of_device_id __maybe_unused sbtsi_of_match[] = {
+ {
+ .compatible = "amd,sbtsi",
+ },
+ { },
+};
+MODULE_DEVICE_TABLE(of, sbtsi_of_match);
+
+static struct i2c_driver sbtsi_driver = {
+ .driver = {
+ .name = "sbtsi-i2c",
+ .of_match_table = of_match_ptr(sbtsi_of_match),
+ },
+ .probe = sbtsi_i2c_probe,
+ .id_table = sbtsi_id,
+};
+
+module_i2c_driver(sbtsi_driver);
+
+MODULE_DESCRIPTION("AMD SB-TSI I2C core driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/misc/tsi.h b/include/linux/misc/tsi.h
new file mode 100644
index 000000000000..6f7177edbcf5
--- /dev/null
+++ b/include/linux/misc/tsi.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * AMD SBTSI shared data structure and auxiliary bus definitions.
+ *
+ * Copyright (C) 2026 Advanced Micro Devices, Inc.
+ */
+
+#ifndef _LINUX_TSI_H_
+#define _LINUX_TSI_H_
+
+#include <linux/i2c.h>
+#include <linux/types.h>
+
+/**
+ * struct sbtsi_data - driver private data for an AMD SB-TSI device
+ * @client: underlying I2C client
+ * @ext_range_mode: sensor uses extended temperature range
+ * @read_order: if set, decimal part must be read before integer part
+ */
+struct sbtsi_data {
+ struct i2c_client *client;
+ bool ext_range_mode;
+ bool read_order;
+};
+
+/*
+ * Name of the auxiliary device published on the auxiliary bus by the core
+ * driver. The full device name is "amd-sbtsi.temp-sensor.<id>". where
+ * <id> is the auxiliary device instance id.
+ */
+#define AMD_SBTSI_ADEV "amd-sbtsi"
+#define AMD_SBTSI_AUX_HWMON "temp-sensor"
+
+#endif /* _LINUX_TSI_H_ */
--
2.34.1
^ permalink raw reply related
* [PATCH v2 2/6] hwmon: sbtsi_temp: Refactor temperature register access into helpers
From: Akshay Gupta @ 2026-05-15 13:45 UTC (permalink / raw)
To: linux-doc, linux-kernel, linux-hwmon
Cc: corbet, skhan, linux, arnd, gregkh, akshay.gupta,
naveenkrishna.chatradhi, Prathima.Lk, Anand.Umarji, Kevin.Tung,
Akshay Gupta
In-Reply-To: <20260515134506.397649-1-Akshay.Gupta@amd.com>
From: Prathima <Prathima.Lk@amd.com>
Extract the paired integer/decimal register reads and writes from the
hwmon read/write callbacks into sbtsi_temp_read() and sbtsi_temp_write()
helpers. This consolidates error handling and respects the ReadOrder bit
for atomic temperature latching.
This keeps register access independent while preserving existing hwmon
functionality.
Reviewed-by: Akshay Gupta <Akshay.Gupta@amd.com>
Signed-off-by: Prathima <Prathima.Lk@amd.com>
---
Changes since v1:
- New patch
drivers/hwmon/sbtsi_temp.c | 84 +++++++++++++++++++++++++++-----------
1 file changed, 61 insertions(+), 23 deletions(-)
diff --git a/drivers/hwmon/sbtsi_temp.c b/drivers/hwmon/sbtsi_temp.c
index 28258bf49922..078f4ab25bde 100644
--- a/drivers/hwmon/sbtsi_temp.c
+++ b/drivers/hwmon/sbtsi_temp.c
@@ -61,40 +61,82 @@ static inline void sbtsi_mc_to_reg(s32 temp, u8 *integer, u8 *decimal)
*decimal = (temp & 0x7) << 5;
}
+/*
+ * Read integer and decimal parts of an SB-TSI temperature register pair
+ * The read order is determined by the ReadOrder bit to ensure atomic latching.
+ */
+static int sbtsi_temp_read(struct sbtsi_data *data, u8 reg1, u8 reg2,
+ u8 *val1, u8 *val2)
+{
+ int ret;
+
+ ret = i2c_smbus_read_byte_data(data->client, reg1);
+ if (ret < 0)
+ return ret;
+ *val1 = ret;
+ ret = i2c_smbus_read_byte_data(data->client, reg2);
+ if (ret < 0)
+ return ret;
+ *val2 = ret;
+ return 0;
+}
+
+/*
+ * Write integer and decimal parts of an SB-TSI temperature register pair.
+ */
+static int sbtsi_temp_write(struct sbtsi_data *data, u8 reg_int, u8 reg_dec,
+ u8 val_int, u8 val_dec)
+{
+ int ret;
+
+ ret = i2c_smbus_write_byte_data(data->client, reg_int, val_int);
+ if (!ret)
+ ret = i2c_smbus_write_byte_data(data->client, reg_dec, val_dec);
+ return ret;
+}
+
static int sbtsi_read(struct device *dev, enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
struct sbtsi_data *data = dev_get_drvdata(dev);
s32 temp_int, temp_dec;
+ int err;
+ u8 val_int, val_dec;
switch (attr) {
case hwmon_temp_input:
- if (data->read_order) {
- temp_dec = i2c_smbus_read_byte_data(data->client, SBTSI_REG_TEMP_DEC);
- temp_int = i2c_smbus_read_byte_data(data->client, SBTSI_REG_TEMP_INT);
- } else {
- temp_int = i2c_smbus_read_byte_data(data->client, SBTSI_REG_TEMP_INT);
- temp_dec = i2c_smbus_read_byte_data(data->client, SBTSI_REG_TEMP_DEC);
- }
+ if (data->read_order)
+ err = sbtsi_temp_read(data,
+ SBTSI_REG_TEMP_DEC, SBTSI_REG_TEMP_INT,
+ &val_dec, &val_int);
+ else
+ err = sbtsi_temp_read(data,
+ SBTSI_REG_TEMP_INT, SBTSI_REG_TEMP_DEC,
+ &val_int, &val_dec);
+ if (err < 0)
+ return err;
break;
case hwmon_temp_max:
- temp_int = i2c_smbus_read_byte_data(data->client, SBTSI_REG_TEMP_HIGH_INT);
- temp_dec = i2c_smbus_read_byte_data(data->client, SBTSI_REG_TEMP_HIGH_DEC);
+ err = sbtsi_temp_read(data,
+ SBTSI_REG_TEMP_HIGH_INT, SBTSI_REG_TEMP_HIGH_DEC,
+ &val_int, &val_dec);
+ if (err < 0)
+ return err;
break;
case hwmon_temp_min:
- temp_int = i2c_smbus_read_byte_data(data->client, SBTSI_REG_TEMP_LOW_INT);
- temp_dec = i2c_smbus_read_byte_data(data->client, SBTSI_REG_TEMP_LOW_DEC);
+ err = sbtsi_temp_read(data,
+ SBTSI_REG_TEMP_LOW_INT, SBTSI_REG_TEMP_LOW_DEC,
+ &val_int, &val_dec);
+
+ if (err < 0)
+ return err;
break;
default:
return -EINVAL;
}
-
- if (temp_int < 0)
- return temp_int;
- if (temp_dec < 0)
- return temp_dec;
-
+ temp_int = val_int;
+ temp_dec = val_dec;
*val = sbtsi_reg_to_mc(temp_int, temp_dec);
if (data->ext_range_mode)
*val -= SBTSI_TEMP_EXT_RANGE_ADJ;
@@ -106,7 +148,7 @@ static int sbtsi_write(struct device *dev, enum hwmon_sensor_types type,
u32 attr, int channel, long val)
{
struct sbtsi_data *data = dev_get_drvdata(dev);
- int reg_int, reg_dec, err;
+ int reg_int, reg_dec;
u8 temp_int, temp_dec;
switch (attr) {
@@ -127,11 +169,7 @@ static int sbtsi_write(struct device *dev, enum hwmon_sensor_types type,
val = clamp_val(val, SBTSI_TEMP_MIN, SBTSI_TEMP_MAX);
sbtsi_mc_to_reg(val, &temp_int, &temp_dec);
- err = i2c_smbus_write_byte_data(data->client, reg_int, temp_int);
- if (err)
- return err;
-
- return i2c_smbus_write_byte_data(data->client, reg_dec, temp_dec);
+ return sbtsi_temp_write(data, reg_int, reg_dec, temp_int, temp_dec);
}
static umode_t sbtsi_is_visible(const void *data,
--
2.34.1
^ permalink raw reply related
* [PATCH v2 3/6] hwmon/misc: amd-sbi: Move sbtsi register transfer to core abstraction
From: Akshay Gupta @ 2026-05-15 13:45 UTC (permalink / raw)
To: linux-doc, linux-kernel, linux-hwmon
Cc: corbet, skhan, linux, arnd, gregkh, akshay.gupta,
naveenkrishna.chatradhi, Prathima.Lk, Anand.Umarji, Kevin.Tung,
Akshay Gupta
In-Reply-To: <20260515134506.397649-1-Akshay.Gupta@amd.com>
From: Prathima <Prathima.Lk@amd.com>
Move the I2C read/write byte operations from the sbtsi hwmon driver into
a common sbtsi_xfer() function in tsi-core.c.
This decouples the hwmon sensor driver from the underlying bus transport,
preparing for I3C support in a subsequent patch.
This patch does not introduce any functional changes. The updates are limited
to code organization/cleanup and should not affect the runtime
behavior of the driver
Reviewed-by: Akshay Gupta <Akshay.Gupta@amd.com>
Signed-off-by: Prathima <Prathima.Lk@amd.com>
---
Changes since v1:
- New patch
drivers/hwmon/sbtsi_temp.c | 17 ++++++-----------
drivers/misc/amd-sbi/Makefile | 2 +-
drivers/misc/amd-sbi/tsi-core.c | 30 ++++++++++++++++++++++++++++++
include/linux/misc/tsi.h | 13 +++++++++++++
4 files changed, 50 insertions(+), 12 deletions(-)
create mode 100644 drivers/misc/amd-sbi/tsi-core.c
diff --git a/drivers/hwmon/sbtsi_temp.c b/drivers/hwmon/sbtsi_temp.c
index 078f4ab25bde..d7ae986d824c 100644
--- a/drivers/hwmon/sbtsi_temp.c
+++ b/drivers/hwmon/sbtsi_temp.c
@@ -70,15 +70,10 @@ static int sbtsi_temp_read(struct sbtsi_data *data, u8 reg1, u8 reg2,
{
int ret;
- ret = i2c_smbus_read_byte_data(data->client, reg1);
- if (ret < 0)
- return ret;
- *val1 = ret;
- ret = i2c_smbus_read_byte_data(data->client, reg2);
- if (ret < 0)
- return ret;
- *val2 = ret;
- return 0;
+ ret = sbtsi_xfer(data, reg1, val1, true);
+ if (!ret)
+ ret = sbtsi_xfer(data, reg2, val2, true);
+ return ret;
}
/*
@@ -89,9 +84,9 @@ static int sbtsi_temp_write(struct sbtsi_data *data, u8 reg_int, u8 reg_dec,
{
int ret;
- ret = i2c_smbus_write_byte_data(data->client, reg_int, val_int);
+ ret = sbtsi_xfer(data, reg_int, &val_int, false);
if (!ret)
- ret = i2c_smbus_write_byte_data(data->client, reg_dec, val_dec);
+ ret = sbtsi_xfer(data, reg_dec, &val_dec, false);
return ret;
}
diff --git a/drivers/misc/amd-sbi/Makefile b/drivers/misc/amd-sbi/Makefile
index 28f95b9e204f..ce9321f5c601 100644
--- a/drivers/misc/amd-sbi/Makefile
+++ b/drivers/misc/amd-sbi/Makefile
@@ -3,5 +3,5 @@ sbrmi-i2c-objs += rmi-i2c.o rmi-core.o
sbrmi-i2c-$(CONFIG_AMD_SBRMI_HWMON) += rmi-hwmon.o
obj-$(CONFIG_AMD_SBRMI_I2C) += sbrmi-i2c.o
# SBTSI Configuration
-sbtsi-objs += tsi.o
+sbtsi-objs += tsi.o tsi-core.o
obj-$(CONFIG_AMD_SBTSI) += sbtsi.o
diff --git a/drivers/misc/amd-sbi/tsi-core.c b/drivers/misc/amd-sbi/tsi-core.c
new file mode 100644
index 000000000000..6ef1831515bb
--- /dev/null
+++ b/drivers/misc/amd-sbi/tsi-core.c
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * tsi-core.c - file defining SB-TSI protocols compliant
+ * AMD SoC device.
+ *
+ * Copyright (C) 2026 Advanced Micro Devices, Inc.
+ */
+
+#include <linux/module.h>
+#include <linux/misc/tsi.h>
+
+/* I2C transfer function */
+static int sbtsi_i2c_xfer(struct sbtsi_data *data, u8 reg, u8 *val, bool is_read)
+{
+ if (is_read) {
+ int ret = i2c_smbus_read_byte_data(data->client, reg);
+
+ if (ret < 0)
+ return ret;
+ *val = ret;
+ return 0;
+ }
+ return i2c_smbus_write_byte_data(data->client, reg, *val);
+}
+
+int sbtsi_xfer(struct sbtsi_data *data, u8 reg, u8 *val, bool is_read)
+{
+ return sbtsi_i2c_xfer(data, reg, val, is_read);
+}
+EXPORT_SYMBOL_GPL(sbtsi_xfer);
diff --git a/include/linux/misc/tsi.h b/include/linux/misc/tsi.h
index 6f7177edbcf5..8f8cb90c2023 100644
--- a/include/linux/misc/tsi.h
+++ b/include/linux/misc/tsi.h
@@ -31,4 +31,17 @@ struct sbtsi_data {
#define AMD_SBTSI_ADEV "amd-sbtsi"
#define AMD_SBTSI_AUX_HWMON "temp-sensor"
+/**
+ * sbtsi_xfer - Perform a register read or write transfer on an AMD SB-TSI device.
+ *
+ * @data: Pointer to the sbtsi_data structure containing the device context
+ * @reg: Register address to access.
+ * @val: Pointer to the value to read into or write from.
+ * @is_read: If true, performs a read transfer and stores the result in @val.
+ * If false, performs a write transfer using the value in @val.
+ *
+ * Returns 0 on success, or a negative error code on failure.
+ */
+int sbtsi_xfer(struct sbtsi_data *data, u8 reg, u8 *val, bool is_read);
+
#endif /* _LINUX_TSI_H_ */
--
2.34.1
^ permalink raw reply related
* [PATCH v2 5/6] misc: amd-sbi: Add SBTSI ioctl register transfer interface
From: Akshay Gupta @ 2026-05-15 13:45 UTC (permalink / raw)
To: linux-doc, linux-kernel, linux-hwmon
Cc: corbet, skhan, linux, arnd, gregkh, akshay.gupta,
naveenkrishna.chatradhi, Prathima.Lk, Anand.Umarji, Kevin.Tung,
Akshay Gupta
In-Reply-To: <20260515134506.397649-1-Akshay.Gupta@amd.com>
From: Prathima <Prathima.Lk@amd.com>
Implement IOCTL interface for SB-TSI driver to enable userspace access
to TSI register read/write operations through the AMD Advanced Platform
Management Link (APML) protocol.
Add an ioctl command (SBTSI_IOCTL_REG_XFER_CMD) that accepts a register
address, data byte, and direction flag. Serialize access with a mutex
shared between the hwmon and ioctl paths to prevent concurrent bus
transactions from corrupting register state.
Reviewed-by: Akshay Gupta <Akshay.Gupta@amd.com>
Signed-off-by: Prathima <Prathima.Lk@amd.com>
---
Changes since v1:
- Use of devm_mutex_init in place of mutex_init
- Use of guard_mutex in place of mutex_lock()/mutex_unlock()
- Use of devm_add_action_or_reset() for clean removal
drivers/hwmon/sbtsi_temp.c | 6 +++
drivers/misc/amd-sbi/tsi-core.c | 84 ++++++++++++++++++++++++++++++++-
drivers/misc/amd-sbi/tsi-core.h | 15 ++++++
drivers/misc/amd-sbi/tsi.c | 20 ++++++--
include/linux/misc/tsi.h | 8 ++++
include/uapi/misc/amd-apml.h | 23 +++++++++
6 files changed, 151 insertions(+), 5 deletions(-)
create mode 100644 drivers/misc/amd-sbi/tsi-core.h
diff --git a/drivers/hwmon/sbtsi_temp.c b/drivers/hwmon/sbtsi_temp.c
index d7ae986d824c..00e982f4c716 100644
--- a/drivers/hwmon/sbtsi_temp.c
+++ b/drivers/hwmon/sbtsi_temp.c
@@ -64,12 +64,15 @@ static inline void sbtsi_mc_to_reg(s32 temp, u8 *integer, u8 *decimal)
/*
* Read integer and decimal parts of an SB-TSI temperature register pair
* The read order is determined by the ReadOrder bit to ensure atomic latching.
+ * The mutex protects against concurrent access to the shared I2C/I3C bus by
+ * the hwmon sysfs and a userspace ioctl
*/
static int sbtsi_temp_read(struct sbtsi_data *data, u8 reg1, u8 reg2,
u8 *val1, u8 *val2)
{
int ret;
+ guard(mutex)(&data->lock);
ret = sbtsi_xfer(data, reg1, val1, true);
if (!ret)
ret = sbtsi_xfer(data, reg2, val2, true);
@@ -78,12 +81,15 @@ static int sbtsi_temp_read(struct sbtsi_data *data, u8 reg1, u8 reg2,
/*
* Write integer and decimal parts of an SB-TSI temperature register pair.
+ * The mutex protects against concurrent access to the shared I2C/I3C bus by
+ * the hwmon sysfs and a userspace ioctl
*/
static int sbtsi_temp_write(struct sbtsi_data *data, u8 reg_int, u8 reg_dec,
u8 val_int, u8 val_dec)
{
int ret;
+ guard(mutex)(&data->lock);
ret = sbtsi_xfer(data, reg_int, &val_int, false);
if (!ret)
ret = sbtsi_xfer(data, reg_dec, &val_dec, false);
diff --git a/drivers/misc/amd-sbi/tsi-core.c b/drivers/misc/amd-sbi/tsi-core.c
index 19388737b225..c5bd60409d5b 100644
--- a/drivers/misc/amd-sbi/tsi-core.c
+++ b/drivers/misc/amd-sbi/tsi-core.c
@@ -6,8 +6,12 @@
* Copyright (C) 2026 Advanced Micro Devices, Inc.
*/
+#include <linux/fs.h>
+#include <linux/ioctl.h>
#include <linux/module.h>
-#include <linux/misc/tsi.h>
+#include <linux/uaccess.h>
+#include <uapi/misc/amd-apml.h>
+#include "tsi-core.h"
/* I2C transfer function */
static int sbtsi_i2c_xfer(struct sbtsi_data *data, u8 reg, u8 *val, bool is_read)
@@ -62,7 +66,83 @@ int sbtsi_xfer(struct sbtsi_data *data, u8 reg, u8 *val, bool is_read)
if (data->is_i3c)
return is_read ? sbtsi_i3c_read(data, reg, val)
: sbtsi_i3c_write(data, reg, *val);
-
return sbtsi_i2c_xfer(data, reg, val, is_read);
}
EXPORT_SYMBOL_GPL(sbtsi_xfer);
+
+/*
+ * The mutex protects against concurrent access to the shared I2C/I3C bus by
+ * the hwmon sysfs and a userspace ioctl.
+ */
+static int sbtsi_xfer_ioctl(struct sbtsi_data *data, u8 reg, u8 *val, bool is_read)
+{
+ guard(mutex)(&data->lock);
+ return sbtsi_xfer(data, reg, val, is_read);
+}
+
+static int apml_tsi_reg_xfer(struct sbtsi_data *data,
+ struct apml_tsi_xfer_msg __user *arg)
+{
+ struct apml_tsi_xfer_msg msg = { 0 };
+ int ret;
+
+ if (copy_from_user(&msg, arg, sizeof(struct apml_tsi_xfer_msg)))
+ return -EFAULT;
+
+ ret = sbtsi_xfer_ioctl(data, msg.reg_addr, &msg.data_in_out, msg.rflag);
+
+ if (msg.rflag && !ret) {
+ if (copy_to_user(arg, &msg, sizeof(struct apml_tsi_xfer_msg)))
+ return -EFAULT;
+ }
+ return ret;
+}
+
+static long sbtsi_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
+{
+ void __user *argp = (void __user *)arg;
+ struct sbtsi_data *data;
+
+ data = container_of(fp->private_data, struct sbtsi_data, sbtsi_misc_dev);
+ switch (cmd) {
+ case SBTSI_IOCTL_REG_XFER_CMD:
+ return apml_tsi_reg_xfer(data, argp);
+ default:
+ return -ENOTTY;
+ }
+}
+
+static const struct file_operations sbtsi_fops = {
+ .owner = THIS_MODULE,
+ .unlocked_ioctl = sbtsi_ioctl,
+ .compat_ioctl = compat_ptr_ioctl,
+};
+
+static void sbtsi_misc_deregister(void *data)
+{
+ misc_deregister((struct miscdevice *)data);
+}
+
+int create_misc_tsi_device(struct sbtsi_data *data, struct device *dev)
+{
+ int ret;
+
+ data->sbtsi_misc_dev.name = devm_kasprintf(dev, GFP_KERNEL,
+ "sbtsi-%x", data->dev_addr);
+ if (!data->sbtsi_misc_dev.name)
+ return -ENOMEM;
+ data->sbtsi_misc_dev.minor = MISC_DYNAMIC_MINOR;
+ data->sbtsi_misc_dev.fops = &sbtsi_fops;
+ data->sbtsi_misc_dev.parent = dev;
+ data->sbtsi_misc_dev.nodename = devm_kasprintf(dev, GFP_KERNEL,
+ "sbtsi-%x", data->dev_addr);
+ if (!data->sbtsi_misc_dev.nodename)
+ return -ENOMEM;
+ data->sbtsi_misc_dev.mode = 0600;
+
+ ret = misc_register(&data->sbtsi_misc_dev);
+ if (ret)
+ return ret;
+ return devm_add_action_or_reset(dev, sbtsi_misc_deregister,
+ &data->sbtsi_misc_dev);
+}
diff --git a/drivers/misc/amd-sbi/tsi-core.h b/drivers/misc/amd-sbi/tsi-core.h
new file mode 100644
index 000000000000..7bf967a09837
--- /dev/null
+++ b/drivers/misc/amd-sbi/tsi-core.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * AMD SBTSI misc tsi device .
+ *
+ * Copyright (C) 2026 Advanced Micro Devices, Inc.
+ */
+
+#ifndef _LINUX_TSI_CORE_H_
+#define _LINUX_TSI_CORE_H_
+
+#include <linux/misc/tsi.h>
+
+int create_misc_tsi_device(struct sbtsi_data *data, struct device *dev);
+
+#endif /* _LINUX_TSI_CORE_H_ */
diff --git a/drivers/misc/amd-sbi/tsi.c b/drivers/misc/amd-sbi/tsi.c
index 43bbac7faf08..6a9356740f4e 100644
--- a/drivers/misc/amd-sbi/tsi.c
+++ b/drivers/misc/amd-sbi/tsi.c
@@ -10,8 +10,8 @@
#include <linux/bitfield.h>
#include <linux/module.h>
#include <linux/of.h>
-#include <linux/misc/tsi.h>
#include <linux/slab.h>
+#include "tsi-core.h"
#define SBTSI_REG_CONFIG 0x03 /* RO */
@@ -89,6 +89,9 @@ static int sbtsi_i2c_probe(struct i2c_client *client)
if (!data)
return -ENOMEM;
+ err = devm_mutex_init(dev, &data->lock);
+ if (err)
+ return err;
data->is_i3c = false;
data->client = client;
err = i2c_smbus_read_byte_data(data->client, SBTSI_REG_CONFIG);
@@ -98,7 +101,11 @@ static int sbtsi_i2c_probe(struct i2c_client *client)
data->read_order = FIELD_GET(BIT(SBTSI_CONFIG_READ_ORDER_SHIFT), err);
dev_set_drvdata(dev, data);
- return sbtsi_create_hwmon_adev(dev, client->addr);
+ err = sbtsi_create_hwmon_adev(dev, client->addr);
+ if (err < 0)
+ return err;
+ data->dev_addr = client->addr;
+ return create_misc_tsi_device(data, dev);
}
static const struct i2c_device_id sbtsi_id[] = {
@@ -145,6 +152,9 @@ static int sbtsi_i3c_probe(struct i3c_device *i3cdev)
if (!data)
return -ENOMEM;
+ err = devm_mutex_init(dev, &data->lock);
+ if (err)
+ return err;
data->i3cdev = i3cdev;
data->is_i3c = true;
@@ -156,7 +166,11 @@ static int sbtsi_i3c_probe(struct i3c_device *i3cdev)
data->read_order = FIELD_GET(BIT(SBTSI_CONFIG_READ_ORDER_SHIFT), val);
dev_set_drvdata(dev, data);
- return sbtsi_create_hwmon_adev(dev, i3cdev->desc->info.dyn_addr);
+ err = sbtsi_create_hwmon_adev(dev, i3cdev->desc->info.dyn_addr);
+ if (err < 0)
+ return err;
+ data->dev_addr = i3cdev->desc->info.dyn_addr;
+ return create_misc_tsi_device(data, dev);
}
static const struct i3c_device_id sbtsi_i3c_id[] = {
diff --git a/include/linux/misc/tsi.h b/include/linux/misc/tsi.h
index 7ce689081427..184b1aa14f0a 100644
--- a/include/linux/misc/tsi.h
+++ b/include/linux/misc/tsi.h
@@ -11,12 +11,17 @@
#include <linux/i2c.h>
#include <linux/i3c/device.h>
#include <linux/i3c/master.h>
+#include <linux/miscdevice.h>
+#include <linux/mutex.h>
#include <linux/types.h>
/**
* struct sbtsi_data - driver private data for an AMD SB-TSI device
* @client: underlying I2C client
* @i3cdev: underlying I3C device (when using I3C bus)
+ * @sbtsi_misc_dev: miscdevice exposing ioctl interface at /dev/sbtsi-<addr>
+ * @lock: mutex protecting concurrent access to the device
+ * @dev_addr: I2C/I3C device address, used to name the misc device node
* @ext_range_mode: sensor uses extended temperature range
* @read_order: if set, decimal part must be read before integer part
* @is_i3c: true when the device is accessed over I3C
@@ -26,6 +31,9 @@ struct sbtsi_data {
struct i2c_client *client;
struct i3c_device *i3cdev;
};
+ struct miscdevice sbtsi_misc_dev;
+ struct mutex lock; /* protects concurrent access to the device */
+ u8 dev_addr;
bool ext_range_mode;
bool read_order;
bool is_i3c;
diff --git a/include/uapi/misc/amd-apml.h b/include/uapi/misc/amd-apml.h
index 745b3338fc06..8a85f79b0938 100644
--- a/include/uapi/misc/amd-apml.h
+++ b/include/uapi/misc/amd-apml.h
@@ -73,6 +73,13 @@ struct apml_reg_xfer_msg {
__u8 rflag;
};
+struct apml_tsi_xfer_msg {
+ __u8 reg_addr; /* TSI register address offset */
+ __u8 data_in_out; /* Register data for read/write */
+ __u8 rflag; /* Register read or write */
+ __u8 pad; /* Explicit padding */
+};
+
/*
* AMD sideband interface base IOCTL
*/
@@ -149,4 +156,20 @@ struct apml_reg_xfer_msg {
*/
#define SBRMI_IOCTL_REG_XFER_CMD _IOWR(SB_BASE_IOCTL_NR, 3, struct apml_reg_xfer_msg)
+/**
+ * DOC: SBTSI_IOCTL_REG_XFER_CMD
+ *
+ * @Parameters
+ *
+ * @struct apml_tsi_xfer_msg
+ * Pointer to the &struct apml_tsi_xfer_msg that will contain the protocol
+ * information
+ *
+ * @Description
+ * IOCTL command for APML TSI messages using generic _IOWR
+ * The IOCTL provides userspace access to AMD sideband TSI register xfer protocol
+ * - TSI protocol to read/write temperature sensor registers
+ */
+#define SBTSI_IOCTL_REG_XFER_CMD _IOWR(SB_BASE_IOCTL_NR, 4, struct apml_tsi_xfer_msg)
+
#endif /*_AMD_APML_H_*/
--
2.34.1
^ permalink raw reply related
* [PATCH v2 4/6] misc: amd-sbi: Add support for SB-TSI over I3C
From: Akshay Gupta @ 2026-05-15 13:45 UTC (permalink / raw)
To: linux-doc, linux-kernel, linux-hwmon
Cc: corbet, skhan, linux, arnd, gregkh, akshay.gupta,
naveenkrishna.chatradhi, Prathima.Lk, Anand.Umarji, Kevin.Tung,
Akshay Gupta
In-Reply-To: <20260515134506.397649-1-Akshay.Gupta@amd.com>
From: Prathima <Prathima.Lk@amd.com>
AMD SB-TSI temperature sensors can be accessed over both
I2C and I3C buses depending on the platform configuration.
Extend the SB-TSI driver to support both I2C and I3C bus interfaces
by selecting the appropriate transport based on the probed bus type.
The driver maintains backward compatibility with existing I2C
deployments while enabling support for systems using the I3C bus.
Register both I2C and I3C drivers using module_i3c_i2c_driver() and
update the Kconfig dependency from I2C to I3C_OR_I2C.
Reviewed-by: Akshay Gupta <Akshay.Gupta@amd.com>
Signed-off-by: Prathima <Prathima.Lk@amd.com>
---
Changes since v1:
- Changes in accordance with usage of auxiliary device
drivers/misc/amd-sbi/Kconfig | 4 +--
drivers/misc/amd-sbi/tsi-core.c | 38 ++++++++++++++++++++
drivers/misc/amd-sbi/tsi.c | 61 +++++++++++++++++++++++++++++++--
include/linux/misc/tsi.h | 10 +++++-
4 files changed, 107 insertions(+), 6 deletions(-)
diff --git a/drivers/misc/amd-sbi/Kconfig b/drivers/misc/amd-sbi/Kconfig
index 512251690e0e..1a96b71f8506 100644
--- a/drivers/misc/amd-sbi/Kconfig
+++ b/drivers/misc/amd-sbi/Kconfig
@@ -23,13 +23,13 @@ config AMD_SBRMI_HWMON
config AMD_SBTSI
tristate "AMD side band TSI support"
- depends on I2C
+ depends on I3C_OR_I2C
depends on ARM || ARM64 || COMPILE_TEST
select AUXILIARY_BUS
help
Enables support for the AMD SB-TSI (Side Band Temperature Sensor
Interface) driver, which provides access to emulated CPU temperature
- sensors on AMD SoCs via an I2C connected BMC device.
+ sensors on AMD SoCs via an I2C/I3C connected BMC device.
This driver can also be built as a module. If so, the module will
be called sbtsi.
diff --git a/drivers/misc/amd-sbi/tsi-core.c b/drivers/misc/amd-sbi/tsi-core.c
index 6ef1831515bb..19388737b225 100644
--- a/drivers/misc/amd-sbi/tsi-core.c
+++ b/drivers/misc/amd-sbi/tsi-core.c
@@ -23,8 +23,46 @@ static int sbtsi_i2c_xfer(struct sbtsi_data *data, u8 reg, u8 *val, bool is_read
return i2c_smbus_write_byte_data(data->client, reg, *val);
}
+/* I3C read transfer function */
+static int sbtsi_i3c_read(struct sbtsi_data *data, u8 reg, u8 *val)
+{
+ struct i3c_xfer xfers[2] = { };
+
+ /* Add Register data to read/write */
+ xfers[0].rnw = false;
+ xfers[0].len = 1;
+ xfers[0].data.out = ®
+
+ xfers[1].rnw = true;
+ xfers[1].len = 1;
+ xfers[1].data.in = val;
+
+ return i3c_device_do_xfers(data->i3cdev, xfers, 2, I3C_SDR);
+}
+
+/* I3C write transfer function */
+static int sbtsi_i3c_write(struct sbtsi_data *data, u8 reg, u8 val)
+{
+ u8 buf[2];
+ struct i3c_xfer xfers = {
+ .rnw = false,
+ .len = 2,
+ .data.out = buf,
+ };
+
+ buf[0] = reg;
+ buf[1] = val;
+
+ return i3c_device_do_xfers(data->i3cdev, &xfers, 1, I3C_SDR);
+}
+
+/* Unified transfer function for I2C and I3C access */
int sbtsi_xfer(struct sbtsi_data *data, u8 reg, u8 *val, bool is_read)
{
+ if (data->is_i3c)
+ return is_read ? sbtsi_i3c_read(data, reg, val)
+ : sbtsi_i3c_write(data, reg, *val);
+
return sbtsi_i2c_xfer(data, reg, val, is_read);
}
EXPORT_SYMBOL_GPL(sbtsi_xfer);
diff --git a/drivers/misc/amd-sbi/tsi.c b/drivers/misc/amd-sbi/tsi.c
index ee2216785550..43bbac7faf08 100644
--- a/drivers/misc/amd-sbi/tsi.c
+++ b/drivers/misc/amd-sbi/tsi.c
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
- * tsi.c - AMD SBTSI I2C core driver. Probes the SBTSI device over I2C
+ * tsi.c - AMD SBTSI I2C/I3C core driver. Probes the SBTSI device over I2C/I3C
* and publishes an auxiliary device on the auxiliary bus.
*
* Copyright (C) 2026 Advanced Micro Devices, Inc.
@@ -89,6 +89,7 @@ static int sbtsi_i2c_probe(struct i2c_client *client)
if (!data)
return -ENOMEM;
+ data->is_i3c = false;
data->client = client;
err = i2c_smbus_read_byte_data(data->client, SBTSI_REG_CONFIG);
if (err < 0)
@@ -123,7 +124,61 @@ static struct i2c_driver sbtsi_driver = {
.id_table = sbtsi_id,
};
-module_i2c_driver(sbtsi_driver);
+static int sbtsi_i3c_probe(struct i3c_device *i3cdev)
+{
+ struct device *dev = i3cdev_to_dev(i3cdev);
+ struct sbtsi_data *data;
+ int err;
+ u8 val;
+
+ /*
+ * AMD OOB devices differ on basis of Instance ID,
+ * for SBTSI, instance ID is 0.
+ * As the device Id match is not on basis of Instance ID,
+ * add the below check to probe the SBTSI device only and
+ * not other OOB devices.
+ */
+ if (I3C_PID_INSTANCE_ID(i3cdev->desc->info.pid) != 0)
+ return -ENXIO;
+
+ data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+
+ data->i3cdev = i3cdev;
+ data->is_i3c = true;
+
+ err = sbtsi_xfer(data, SBTSI_REG_CONFIG, &val, true);
+ if (err)
+ return err;
+
+ data->ext_range_mode = FIELD_GET(BIT(SBTSI_CONFIG_EXT_RANGE_SHIFT), val);
+ data->read_order = FIELD_GET(BIT(SBTSI_CONFIG_READ_ORDER_SHIFT), val);
+
+ dev_set_drvdata(dev, data);
+ return sbtsi_create_hwmon_adev(dev, i3cdev->desc->info.dyn_addr);
+}
+
+static const struct i3c_device_id sbtsi_i3c_id[] = {
+ /* PID for AMD SBTSI device */
+ I3C_DEVICE_EXTRA_INFO(0x112, 0x0, 0x1, NULL),
+ I3C_DEVICE_EXTRA_INFO(0x0, 0x0, 0x118, NULL), /* Socket:0, Venice */
+ I3C_DEVICE_EXTRA_INFO(0x0, 0x100, 0x118, NULL), /* Socket:1, Venice */
+ I3C_DEVICE_EXTRA_INFO(0x112, 0x0, 0x119, NULL), /* Socket:0, Venice */
+ I3C_DEVICE_EXTRA_INFO(0x112, 0x100, 0x119, NULL), /* Socket:1, Venice */
+ {}
+};
+MODULE_DEVICE_TABLE(i3c, sbtsi_i3c_id);
+
+static struct i3c_driver sbtsi_i3c_driver = {
+ .driver = {
+ .name = "sbtsi-i3c",
+ },
+ .probe = sbtsi_i3c_probe,
+ .id_table = sbtsi_i3c_id,
+};
+
+module_i3c_i2c_driver(sbtsi_i3c_driver, &sbtsi_driver);
-MODULE_DESCRIPTION("AMD SB-TSI I2C core driver");
+MODULE_DESCRIPTION("AMD SB-TSI I2C/I3C core driver");
MODULE_LICENSE("GPL");
diff --git a/include/linux/misc/tsi.h b/include/linux/misc/tsi.h
index 8f8cb90c2023..7ce689081427 100644
--- a/include/linux/misc/tsi.h
+++ b/include/linux/misc/tsi.h
@@ -9,18 +9,26 @@
#define _LINUX_TSI_H_
#include <linux/i2c.h>
+#include <linux/i3c/device.h>
+#include <linux/i3c/master.h>
#include <linux/types.h>
/**
* struct sbtsi_data - driver private data for an AMD SB-TSI device
* @client: underlying I2C client
+ * @i3cdev: underlying I3C device (when using I3C bus)
* @ext_range_mode: sensor uses extended temperature range
* @read_order: if set, decimal part must be read before integer part
+ * @is_i3c: true when the device is accessed over I3C
*/
struct sbtsi_data {
- struct i2c_client *client;
+ union {
+ struct i2c_client *client;
+ struct i3c_device *i3cdev;
+ };
bool ext_range_mode;
bool read_order;
+ bool is_i3c;
};
/*
--
2.34.1
^ permalink raw reply related
* [PATCH v2 6/6] docs: misc: amd-sbi: Document SBTSI userspace interface
From: Akshay Gupta @ 2026-05-15 13:45 UTC (permalink / raw)
To: linux-doc, linux-kernel, linux-hwmon
Cc: corbet, skhan, linux, arnd, gregkh, akshay.gupta,
naveenkrishna.chatradhi, Prathima.Lk, Anand.Umarji, Kevin.Tung,
Akshay Gupta
In-Reply-To: <20260515134506.397649-1-Akshay.Gupta@amd.com>
From: Prathima <Prathima.Lk@amd.com>
- Document AMD sideband IOCTL description defined
for SBTSI and its usage.
User space C-APIs are made available by esmi_oob_library [1],
which is provided by the E-SMS project [2].
Link: https://github.com/amd/esmi_oob_library [1]
Link: https://www.amd.com/en/developer/e-sms.html [2]
Include a user-space open example for /dev/sbtsi-* and list auxiliary
bus sysfs paths.
Reviewed-by: Akshay Gupta <Akshay.Gupta@amd.com>
Signed-off-by: Prathima <Prathima.Lk@amd.com>
---
Changes since v1:
- Elaborate the document
Documentation/misc-devices/amd-sbi.rst | 64 ++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
diff --git a/Documentation/misc-devices/amd-sbi.rst b/Documentation/misc-devices/amd-sbi.rst
index f91ddadefe48..6a6344439ef5 100644
--- a/Documentation/misc-devices/amd-sbi.rst
+++ b/Documentation/misc-devices/amd-sbi.rst
@@ -48,6 +48,56 @@ Access restrictions:
* APML Mailbox messages and Register xfer access are read-write,
* CPUID and MCA_MSR access is read-only.
+SBTSI device
+============
+
+sbtsi driver under the drivers/misc/amd-sbi creates miscdevice
+/dev/sbtsi-* to let user space programs run APML TSI register xfer
+commands.
+
+The driver supports both I2C and I3C transports for SB-TSI targets.
+The transport is selected by the bus where the device is enumerated.
+
+.. code-block:: bash
+
+ $ ls -al /dev/sbtsi-4c
+ crw------- 1 root root 10, 116 Apr 2 05:22 /dev/sbtsi-4c
+
+
+Access restrictions:
+ * Only root user is allowed to open the file.
+ * APML TSI Register xfer access is read-write.
+
+SBTSI hwmon interface
+=====================
+
+The sbtsi_temp auxiliary driver binds to the auxiliary device published
+by the core sbtsi driver on the auxiliary bus. The auxiliary device is
+named amd-sbtsi.temp-sensor.<addr> where <addr> is the device's dynamic
+address.
+
+It registers a hwmon device, providing a standard Linux hwmon interface
+for reading CPU temperature and managing temperature limits.
+
+The hwmon device appears under ``/sys/class/hwmon/`` when both ``sbtsi.ko``
+and ``sbtsi_temp.ko`` are loaded.
+
+Verify auxiliary bus device::
+
+ ls /sys/bus/auxiliary/devices/
+ # e.g. amd-sbtsi.temp-sensor.X
+
+Example usage::
+
+ # Read current temperature
+ cat /sys/class/hwmon/hwmon<N>/temp1_input
+
+ # Set high temperature limit to 70 °C
+ echo 70000 > /sys/class/hwmon/hwmon<N>/temp1_max
+
+ # Verify
+ cat /sys/class/hwmon/hwmon<N>/temp1_max
+
Driver IOCTLs
=============
@@ -63,6 +113,9 @@ Driver IOCTLs
.. c:macro:: SBRMI_IOCTL_REG_XFER_CMD
.. kernel-doc:: include/uapi/misc/amd-apml.h
:doc: SBRMI_IOCTL_REG_XFER_CMD
+.. c:macro:: SBTSI_IOCTL_REG_XFER_CMD
+.. kernel-doc:: include/uapi/misc/amd-apml.h
+ :doc: SBTSI_IOCTL_REG_XFER_CMD
User-space usage
================
@@ -85,6 +138,16 @@ Next thing, open the device file, as follows::
exit(1);
}
+To open SB-TSI device::
+
+ int file;
+
+ file = open("/dev/sbtsi-*", O_RDWR);
+ if (file < 0) {
+ /* ERROR HANDLING */
+ exit(1);
+ }
+
The following IOCTLs are defined:
``#define SB_BASE_IOCTL_NR 0xF9``
@@ -92,6 +155,7 @@ The following IOCTLs are defined:
``#define SBRMI_IOCTL_CPUID_CMD _IOWR(SB_BASE_IOCTL_NR, 1, struct apml_cpuid_msg)``
``#define SBRMI_IOCTL_MCAMSR_CMD _IOWR(SB_BASE_IOCTL_NR, 2, struct apml_mcamsr_msg)``
``#define SBRMI_IOCTL_REG_XFER_CMD _IOWR(SB_BASE_IOCTL_NR, 3, struct apml_reg_xfer_msg)``
+``#define SBTSI_IOCTL_REG_XFER_CMD _IOWR(SB_BASE_IOCTL_NR, 4, struct apml_tsi_xfer_msg)``
User space C-APIs are made available by esmi_oob_library, hosted at
--
2.34.1
^ permalink raw reply related
* Re: [PATCH v13 0/4] kunit: Add support for suppressing warning backtraces
From: Guenter Roeck @ 2026-05-15 13:51 UTC (permalink / raw)
To: Albert Esteve, Arnd Bergmann, Brendan Higgins, David Gow,
Rae Moar, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Jonathan Corbet, Shuah Khan,
Andrew Morton, Paul Walmsley, Palmer Dabbelt, Albert Ou,
Alexandre Ghiti
Cc: linux-kernel, linux-arch, linux-kselftest, kunit-dev, dri-devel,
workflows, linux-riscv, linux-doc, peterz, Alessandro Carminati,
Kees Cook, Linux Kernel Functional Testing, Maíra Canal,
Dan Carpenter, Simona Vetter
In-Reply-To: <20260515-kunit_add_support-v13-0-18ee42f96e7b@redhat.com>
Hi Albert,
On 5/15/26 05:29, Albert Esteve wrote:
...
> Guenter Roeck (3):
> kunit: Add backtrace suppression self-tests
> drm: Suppress intentional warning backtraces in scaling unit tests
> kunit: Add documentation for warning backtrace suppression API
>
How much of that is from me at this point ? Wouldn't it make sense to drop me
as "author" of those patches ?
I would not mind. I had the idea, but others like you are doing the hard work
of pushing it through.
Thanks,
Guenter
^ permalink raw reply
* Re: [PATCH] docs: sphinx-static: fix typo "wich" -> "which"
From: Jonathan Corbet @ 2026-05-15 13:52 UTC (permalink / raw)
To: Clinton Phillips; +Cc: Clinton Phillips, linux-doc, linux-kernel
In-Reply-To: <20260513195956.25307-1-clintdotphillips@gmail.com>
Clinton Phillips <clintdotphillips@gmail.com> writes:
> Trivial typo fix in a CSS comment for the documentation theme.
>
> Signed-off-by: Clinton Phillips <clintdotphillips@gmail.com>
> ---
> Documentation/sphinx-static/custom.css | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/Documentation/sphinx-static/custom.css b/Documentation/sphinx-static/custom.css
> index f91393426..5aa0a1ed9 100644
> --- a/Documentation/sphinx-static/custom.css
> +++ b/Documentation/sphinx-static/custom.css
> @@ -30,7 +30,7 @@ img.logo {
> margin-bottom: 20px;
> }
>
> -/* The default is to use -1em, wich makes it override text */
> +/* The default is to use -1em, which makes it override text */
> li { text-indent: 0em; }
Applied, thanks.
jon
^ permalink raw reply
* Re: [PATCH RFC 2/5] dma-heap: charge dma-buf memory via explicit memcg
From: Christian Brauner @ 2026-05-15 13:53 UTC (permalink / raw)
To: Albert Esteve
Cc: Tejun Heo, Johannes Weiner, Michal Koutný, Jonathan Corbet,
Shuah Khan, Sumit Semwal, Christian König, Michal Hocko,
Roman Gushchin, Shakeel Butt, Muchun Song, Andrew Morton,
Benjamin Gaignard, Brian Starkey, John Stultz, T.J. Mercier,
Paul Moore, James Morris, Serge E. Hallyn, Stephen Smalley,
Ondrej Mosnacek, Shuah Khan, cgroups, linux-doc, linux-kernel,
linux-media, dri-devel, linaro-mm-sig, linux-mm,
linux-security-module, selinux, linux-kselftest, mripard,
echanude
In-Reply-To: <20260512-v2_20230123_tjmercier_google_com-v1-2-6326701c3691@redhat.com>
On Tue, May 12, 2026 at 11:10:44AM +0200, Albert Esteve wrote:
> On embedded platforms a central process often allocates dma-buf
> memory on behalf of client applications. Without a way to
> attribute the charge to the requesting client's cgroup, the
> cost lands on the allocator, making per-cgroup memory limits
> ineffective for the actual consumers.
>
> Add charge_pid_fd to struct dma_heap_allocation_data. When set to
Please be aware that pidfds come in two flavors:
thread-group pidfds and thread-specific pidfds. Make sure that your API
doesn't implicitly depend on this distinction not existing.
> a valid pidfd, DMA_HEAP_IOCTL_ALLOC resolves the target task's
> memcg and charges the buffer there via mem_cgroup_charge_dmabuf()
> inside dma_heap_buffer_alloc(). Without charge_pid_fd, and with
> the mem_accounting module parameter enabled, the buffer is charged
> to the allocator's own cgroup.
>
> Additionally, commit 3c227be90659 ("dma-buf: system_heap: account for
> system heap allocation in memcg") adds __GFP_ACCOUNT to system-heap
> page allocations. Keeping __GFP_ACCOUNT would charge the same pages
> twice (once to kmem, once to MEMCG_DMABUF), thus remove it and route
> all accounting through a single MEMCG_DMABUF path.
>
> Usage examples:
>
> 1. Central allocator charging to a client at allocation time.
> The allocator knows the client's PID (e.g., from binder's
> sender_pid) and uses pidfd to attribute the charge:
>
> pid_t client_pid = txn->sender_pid;
> int pidfd = pidfd_open(client_pid, 0);
>
> struct dma_heap_allocation_data alloc = {
> .len = buffer_size,
> .fd_flags = O_RDWR | O_CLOEXEC,
> .charge_pid_fd = pidfd,
> };
> ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &alloc);
> close(pidfd);
> /* alloc.fd is now charged to client's cgroup */
>
> 2. Default allocation (no pidfd, mem_accounting=1).
> When charge_pid_fd is not set and the mem_accounting module
> parameter is enabled, the buffer is charged to the allocator's
> own cgroup:
>
> struct dma_heap_allocation_data alloc = {
> .len = buffer_size,
> .fd_flags = O_RDWR | O_CLOEXEC,
> };
> ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &alloc);
> /* charged to current process's cgroup */
>
> Current limitations:
>
> - Single-owner model: a dma-buf carries one memcg charge regardless of
> how many processes share it. Means only the first owner (and exporter)
> of the shared buffer bears the charge.
> - Only memcg accounting supported. While this makes sense for system
> heap buffers, other heaps (e.g., CMA heaps) will require selectively
> charging also for the dmem controller.
>
> Signed-off-by: Albert Esteve <aesteve@redhat.com>
> ---
> Documentation/admin-guide/cgroup-v2.rst | 5 ++--
> drivers/dma-buf/dma-buf.c | 16 ++++---------
> drivers/dma-buf/dma-heap.c | 42 ++++++++++++++++++++++++++++++---
> drivers/dma-buf/heaps/system_heap.c | 2 --
> include/uapi/linux/dma-heap.h | 6 +++++
> 5 files changed, 53 insertions(+), 18 deletions(-)
>
> diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
> index 8bdbc2e866430..824d269531eb1 100644
> --- a/Documentation/admin-guide/cgroup-v2.rst
> +++ b/Documentation/admin-guide/cgroup-v2.rst
> @@ -1636,8 +1636,9 @@ The following nested keys are defined.
> structures.
>
> dmabuf (npn)
> - Amount of memory used for exported DMA buffers allocated by the cgroup.
> - Stays with the allocating cgroup regardless of how the buffer is shared.
> + Amount of memory used for exported DMA buffers allocated by or on
> + behalf of the cgroup. Stays with the allocating cgroup regardless
> + of how the buffer is shared.
>
> workingset_refault_anon
> Number of refaults of previously evicted anonymous pages.
> diff --git a/drivers/dma-buf/dma-buf.c b/drivers/dma-buf/dma-buf.c
> index ce02377f48908..23fb758b78297 100644
> --- a/drivers/dma-buf/dma-buf.c
> +++ b/drivers/dma-buf/dma-buf.c
> @@ -181,8 +181,11 @@ static void dma_buf_release(struct dentry *dentry)
> */
> BUG_ON(dmabuf->cb_in.active || dmabuf->cb_out.active);
>
> - mem_cgroup_uncharge_dmabuf(dmabuf->memcg, PAGE_ALIGN(dmabuf->size) / PAGE_SIZE);
> - mem_cgroup_put(dmabuf->memcg);
> + if (dmabuf->memcg) {
> + mem_cgroup_uncharge_dmabuf(dmabuf->memcg,
> + PAGE_ALIGN(dmabuf->size) / PAGE_SIZE);
> + mem_cgroup_put(dmabuf->memcg);
> + }
>
> dmabuf->ops->release(dmabuf);
>
> @@ -764,13 +767,6 @@ struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info)
> dmabuf->resv = resv;
> }
>
> - dmabuf->memcg = get_mem_cgroup_from_mm(current->mm);
> - if (!mem_cgroup_charge_dmabuf(dmabuf->memcg, PAGE_ALIGN(dmabuf->size) / PAGE_SIZE,
> - GFP_KERNEL)) {
> - ret = -ENOMEM;
> - goto err_memcg;
> - }
> -
> file->private_data = dmabuf;
> file->f_path.dentry->d_fsdata = dmabuf;
> dmabuf->file = file;
> @@ -781,8 +777,6 @@ struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info)
>
> return dmabuf;
>
> -err_memcg:
> - mem_cgroup_put(dmabuf->memcg);
> err_file:
> fput(file);
> err_module:
> diff --git a/drivers/dma-buf/dma-heap.c b/drivers/dma-buf/dma-heap.c
> index ac5f8685a6494..ff6e259afcdc0 100644
> --- a/drivers/dma-buf/dma-heap.c
> +++ b/drivers/dma-buf/dma-heap.c
> @@ -7,13 +7,17 @@
> */
>
> #include <linux/cdev.h>
> +#include <linux/cgroup.h>
> #include <linux/device.h>
> #include <linux/dma-buf.h>
> #include <linux/dma-heap.h>
> +#include <linux/memcontrol.h>
> +#include <linux/sched/mm.h>
> #include <linux/err.h>
> #include <linux/export.h>
> #include <linux/list.h>
> #include <linux/nospec.h>
> +#include <linux/pidfd.h>
> #include <linux/syscalls.h>
> #include <linux/uaccess.h>
> #include <linux/xarray.h>
> @@ -55,10 +59,12 @@ MODULE_PARM_DESC(mem_accounting,
> "Enable cgroup-based memory accounting for dma-buf heap allocations (default=false).");
>
> static int dma_heap_buffer_alloc(struct dma_heap *heap, size_t len,
> - u32 fd_flags,
> - u64 heap_flags)
> + u32 fd_flags, u64 heap_flags,
> + struct mem_cgroup *charge_to)
> {
> struct dma_buf *dmabuf;
> + unsigned int nr_pages;
> + struct mem_cgroup *memcg = charge_to;
> int fd;
>
> /*
> @@ -73,6 +79,22 @@ static int dma_heap_buffer_alloc(struct dma_heap *heap, size_t len,
> if (IS_ERR(dmabuf))
> return PTR_ERR(dmabuf);
>
> + nr_pages = len / PAGE_SIZE;
> +
> + if (memcg)
> + css_get(&memcg->css);
> + else if (mem_accounting)
> + memcg = get_mem_cgroup_from_mm(current->mm);
> +
> + if (memcg) {
> + if (!mem_cgroup_charge_dmabuf(memcg, nr_pages, GFP_KERNEL)) {
> + mem_cgroup_put(memcg);
> + dma_buf_put(dmabuf);
> + return -ENOMEM;
> + }
> + dmabuf->memcg = memcg;
> + }
> +
> fd = dma_buf_fd(dmabuf, fd_flags);
> if (fd < 0) {
> dma_buf_put(dmabuf);
> @@ -102,6 +124,9 @@ static long dma_heap_ioctl_allocate(struct file *file, void *data)
> {
> struct dma_heap_allocation_data *heap_allocation = data;
> struct dma_heap *heap = file->private_data;
> + struct mem_cgroup *memcg = NULL;
> + struct task_struct *task;
> + unsigned int pidfd_flags;
> int fd;
>
> if (heap_allocation->fd)
> @@ -113,9 +138,20 @@ static long dma_heap_ioctl_allocate(struct file *file, void *data)
> if (heap_allocation->heap_flags & ~DMA_HEAP_VALID_HEAP_FLAGS)
> return -EINVAL;
>
> + if (heap_allocation->charge_pid_fd) {
> + task = pidfd_get_task(heap_allocation->charge_pid_fd, &pidfd_flags);
Will always get a thread-group leader pidfd and will fail if this is a
thread-specific pidfd. pidfd_open(1234, PIDFD_THREAD) can be used to
open a thread-specific pidfd.
> + if (IS_ERR(task))
> + return PTR_ERR(task);
> +
> + memcg = get_mem_cgroup_from_mm(task->mm);
> + put_task_struct(task);
> + }
> +
> fd = dma_heap_buffer_alloc(heap, heap_allocation->len,
> heap_allocation->fd_flags,
> - heap_allocation->heap_flags);
> + heap_allocation->heap_flags,
> + memcg);
> + mem_cgroup_put(memcg);
> if (fd < 0)
> return fd;
>
> diff --git a/drivers/dma-buf/heaps/system_heap.c b/drivers/dma-buf/heaps/system_heap.c
> index 03c2b87cb1112..95d7688167b93 100644
> --- a/drivers/dma-buf/heaps/system_heap.c
> +++ b/drivers/dma-buf/heaps/system_heap.c
> @@ -385,8 +385,6 @@ static struct page *alloc_largest_available(unsigned long size,
> if (max_order < orders[i])
> continue;
> flags = order_flags[i];
> - if (mem_accounting)
> - flags |= __GFP_ACCOUNT;
> page = alloc_pages(flags, orders[i]);
> if (!page)
> continue;
> diff --git a/include/uapi/linux/dma-heap.h b/include/uapi/linux/dma-heap.h
> index a4cf716a49fa6..e02b0f8cbc6a1 100644
> --- a/include/uapi/linux/dma-heap.h
> +++ b/include/uapi/linux/dma-heap.h
> @@ -29,6 +29,10 @@
> * handle to the allocated dma-buf
> * @fd_flags: file descriptor flags used when allocating
> * @heap_flags: flags passed to heap
> + * @charge_pid_fd: optional pidfd of the process whose cgroup should be
> + * charged for this allocation; 0 means charge the calling
> + * process's cgroup
> + * @__padding: reserved, must be zero
> *
> * Provided by userspace as an argument to the ioctl
> */
> @@ -37,6 +41,8 @@ struct dma_heap_allocation_data {
> __u32 fd;
> __u32 fd_flags;
> __u64 heap_flags;
> + __u32 charge_pid_fd;
> + __u32 __padding;
> };
>
> #define DMA_HEAP_IOC_MAGIC 'H'
>
> --
> 2.53.0
>
^ permalink raw reply
* Re: [PATCH] dm: fix dm-inlinecrypt docs warnings
From: Jonathan Corbet @ 2026-05-15 13:55 UTC (permalink / raw)
To: Randy Dunlap, linux-kernel
Cc: Randy Dunlap, Linlin Zhang, Alasdair Kergon, Mike Snitzer,
Mikulas Patocka, Benjamin Marzinski, dm-devel, Shuah Khan,
linux-doc
In-Reply-To: <20260512180409.1193504-1-rdunlap@infradead.org>
Randy Dunlap <rdunlap@infradead.org> writes:
> Add this file to the index and use a longer heading overline string
> to eliminate warnings:
>
> Documentation/admin-guide/device-mapper/dm-inlinecrypt.rst:1: WARNING: Title overline too short.
> ========
> dm-inlinecrypt
> ========
> Documentation/admin-guide/device-mapper/dm-inlinecrypt.rst: WARNING: document isn't included in any toctree [toc.not_included]
>
> Fixes: b4a0774bd7fd ("dm: add documentation for dm-inlinecrypt target")
> Signed-off-by: Randy Dunlap <rdunlap@infradead.org>
> ---
> Cc: Linlin Zhang <linlin.zhang@oss.qualcomm.com>
> Cc: Alasdair Kergon <agk@redhat.com>
> Cc: Mike Snitzer <snitzer@kernel.org>
> Cc: Mikulas Patocka <mpatocka@redhat.com>
> Cc: Benjamin Marzinski <bmarzins@redhat.com>
> Cc: dm-devel@lists.linux.dev
> Cc: Jonathan Corbet <corbet@lwn.net>
> Cc: Shuah Khan <skhan@linuxfoundation.org>
> Cc: linux-doc@vger.kernel.org
>
> Documentation/admin-guide/device-mapper/dm-inlinecrypt.rst | 4 ++--
> Documentation/admin-guide/device-mapper/index.rst | 1 +
> 2 files changed, 3 insertions(+), 2 deletions(-)
This doesn't apply to docs-next, so I'm guessing it's intended for some
other tree?
Thanks,
jon
^ permalink raw reply
* Re: [PATCH v4] docs: reporting-issues: replace "these advices" with "all of this advice"
From: Jonathan Corbet @ 2026-05-15 13:56 UTC (permalink / raw)
To: Chen-Shi-Hong, linux; +Cc: skhan, linux-doc, linux-kernel, Chen-Shi-Hong
In-Reply-To: <20260514082808.655-1-eric039eric@gmail.com>
Chen-Shi-Hong <eric039eric@gmail.com> writes:
> "Advice" is an uncountable noun, so "these advices" is grammatically
> incorrect.
>
> Replace it with "all of this advice" instead, which keeps the sentence
> grammatical while also making it clear that it refers to the full set of
> recommendations in the paragraph.
>
> Signed-off-by: Chen-Shi-Hong <eric039eric@gmail.com>
> ---
> v4:
> - move version changelog below the "---"
> - send as a separate thread
>
> v3:
> - resend against the original base as requested
> - replace "these advices" directly with "all of this advice"
>
> v2:
> - use "all of this advice" based on review feedback
> Documentation/admin-guide/reporting-issues.rst | 4 ++--
> 1 file changed, 2 insertions(+), 2 deletions(-)
Applied, thanks.
jon
^ permalink raw reply
* [PATCH v3 0/3] x86,fs/resctrl,arm_mpam: Factor MBA parse-time conversion to be per-arch
From: Ben Horgan @ 2026-05-15 14:06 UTC (permalink / raw)
To: ben.horgan
Cc: james.morse, reinette.chatre, fenghuay, linux-kernel,
linux-arm-kernel, tglx, mingo, bp, dave.hansen, hpa, corbet, x86,
linux-doc, dave.martin
This is a new version of Dave Martin's patch [1] to delegate rounding of
bandwidth control user values to the arch code. As there is now more than one
architecture using resctrl, I split the original patch into two, a core resctrl
patch and an x86 patch, and added an MPAM patch. Please let me know if the patch
break down and ordering is sensible and whether the pattern should be followed
for any future similar changes.
This does have a user visible effect on MB schema when using MPAM hardware
with 'bandwidth_gran' greater than 1. I'm not sure if MPAM hardware with such
coarse controls exists in the wild but it is spec compliant and I've tested it
on a model.
[1] https://lore.kernel.org/lkml/20251031154225.14799-1-Dave.Martin@arm.com/
Ben Horgan (2):
x86/resctrl: Add resctrl_arch_preconvert_bw()
arm_mpam: resctrl: Add pass-through resctrl_arch_preconvert_bw()
Dave Martin (1):
fs/resctrl: Factor MBA parse-time conversion to be per-arch
Documentation/filesystems/resctrl.rst | 17 +++++++++--------
arch/x86/kernel/cpu/resctrl/ctrlmondata.c | 6 ++++++
drivers/resctrl/mpam_resctrl.c | 5 +++++
fs/resctrl/ctrlmondata.c | 6 +++---
include/linux/resctrl.h | 19 +++++++++++++++++++
5 files changed, 42 insertions(+), 11 deletions(-)
--
2.43.0
^ permalink raw reply
* [PATCH v3 1/3] x86/resctrl: Add resctrl_arch_preconvert_bw()
From: Ben Horgan @ 2026-05-15 14:06 UTC (permalink / raw)
To: ben.horgan
Cc: james.morse, reinette.chatre, fenghuay, linux-kernel,
linux-arm-kernel, tglx, mingo, bp, dave.hansen, hpa, corbet, x86,
linux-doc, dave.martin
In-Reply-To: <20260515140612.1205251-1-ben.horgan@arm.com>
On MPAM systems the rounding behaviour of the MBA control would be improved
if the rounding in the fs/resctrl code is removed but this is not the
case for x86. To allow any rounding or conversion of the bandwidth value
provided by the user to be specified by the arch code a new arch hook is
required.
Introduce resctrl_arch_preconvert_bw(), and add its x86 implementation.
This is currently unused in resctrl but when plumbed in it will replace the
call to roundup() in bw_validate().
Signed-off-by: Dave Martin <dave.martin@arm.com>
Signed-off-by: Ben Horgan <ben.horgan@arm.com>
---
Changes since Dave's v2:
Split from larger patch and add commit message
Update kernel-doc (Reinette)
---
arch/x86/kernel/cpu/resctrl/ctrlmondata.c | 6 ++++++
include/linux/resctrl.h | 19 +++++++++++++++++++
2 files changed, 25 insertions(+)
diff --git a/arch/x86/kernel/cpu/resctrl/ctrlmondata.c b/arch/x86/kernel/cpu/resctrl/ctrlmondata.c
index b20e705606b8..19ae596f6b30 100644
--- a/arch/x86/kernel/cpu/resctrl/ctrlmondata.c
+++ b/arch/x86/kernel/cpu/resctrl/ctrlmondata.c
@@ -16,9 +16,15 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/cpu.h>
+#include <linux/math.h>
#include "internal.h"
+u32 resctrl_arch_preconvert_bw(u32 val, const struct rdt_resource *r)
+{
+ return roundup(val, (unsigned long)r->membw.bw_gran);
+}
+
int resctrl_arch_update_one(struct rdt_resource *r, struct rdt_ctrl_domain *d,
u32 closid, enum resctrl_conf_type t, u32 cfg_val)
{
diff --git a/include/linux/resctrl.h b/include/linux/resctrl.h
index 006e57fd7ca5..33a6742da4f9 100644
--- a/include/linux/resctrl.h
+++ b/include/linux/resctrl.h
@@ -500,6 +500,25 @@ bool resctrl_arch_mbm_cntr_assign_enabled(struct rdt_resource *r);
*/
int resctrl_arch_mbm_cntr_assign_set(struct rdt_resource *r, bool enable);
+/**
+ * resctrl_arch_preconvert_bw() - Prepare bandwidth control value for arch use.
+ * @val: Bandwidth control value written to the schemata file by userspace.
+ * @r: Resource whose schema was written.
+ *
+ * Convert the user provided bandwidth control value to an appropriate form for
+ * consumption by the hardware driver for resource @r. Converted value is stored
+ * in rdt_ctrl_domain::staged_config[] for later consumption by
+ * resctrl_arch_update_domains(). Is not called when MBA software controller is
+ * enabled.
+ *
+ * Architectures for which this pre-conversion hook is not useful should supply
+ * an implementation of this function that just returns val unmodified.
+ *
+ * Return:
+ * The converted value.
+ */
+u32 resctrl_arch_preconvert_bw(u32 val, const struct rdt_resource *r);
+
/*
* Update the ctrl_val and apply this config right now.
* Must be called on one of the domain's CPUs.
--
2.43.0
^ permalink raw reply related
* [PATCH v3 2/3] arm_mpam: resctrl: Add pass-through resctrl_arch_preconvert_bw()
From: Ben Horgan @ 2026-05-15 14:06 UTC (permalink / raw)
To: ben.horgan
Cc: james.morse, reinette.chatre, fenghuay, linux-kernel,
linux-arm-kernel, tglx, mingo, bp, dave.hansen, hpa, corbet, x86,
linux-doc, dave.martin
In-Reply-To: <20260515140612.1205251-1-ben.horgan@arm.com>
resctrl rounds up the percentage value of the MBA based on the bw_gran. As
MPAM uses a binary fixed point fraction format for MBA rather than a
decimal percentage, this introduces rounding errors.
Without this additional rounding, if the user reads the value in an MB
schema and then writes it back to the schema, the value in hardware won't
change. However, with this additional rounding, this guarantee is broken
for systems with mbw_wd < 7.
resctrl is introducing resctrl_arch_preconvert_bw() to allow the arch code
to specify the conversion resctrl does to the user-provided bandwidth
value. Add the MPAM version of resctrl_arch_preconvert_bw(). This does no
conversion.
Signed-off-by: Ben Horgan <ben.horgan@arm.com>
---
drivers/resctrl/mpam_resctrl.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/drivers/resctrl/mpam_resctrl.c b/drivers/resctrl/mpam_resctrl.c
index 226ff6f532fa..5a2104af22cc 100644
--- a/drivers/resctrl/mpam_resctrl.c
+++ b/drivers/resctrl/mpam_resctrl.c
@@ -167,6 +167,11 @@ bool resctrl_arch_get_cdp_enabled(enum resctrl_res_level rid)
return mpam_resctrl_controls[rid].cdp_enabled;
}
+u32 resctrl_arch_preconvert_bw(u32 val, const struct rdt_resource *r)
+{
+ return val;
+}
+
/**
* resctrl_reset_task_closids() - Reset the PARTID/PMG values for all tasks.
*
--
2.43.0
^ permalink raw reply related
* [PATCH v3 3/3] fs/resctrl: Factor MBA parse-time conversion to be per-arch
From: Ben Horgan @ 2026-05-15 14:06 UTC (permalink / raw)
To: ben.horgan
Cc: james.morse, reinette.chatre, fenghuay, linux-kernel,
linux-arm-kernel, tglx, mingo, bp, dave.hansen, hpa, corbet, x86,
linux-doc, dave.martin, Dave Martin, Ben Horgan
In-Reply-To: <20260515140612.1205251-1-ben.horgan@arm.com>
From: Dave Martin <Dave.Martin@arm.com>
The control value parser for the MB resource currently coerces the
memory bandwidth percentage value from userspace to be an exact
multiple of the rdt_resource::resctrl_membw::bw_gran parameter.
On MPAM systems, this results in somewhat worse-than-worst-case
rounding, since the bandwidth granularity advertised to resctrl by the
MPAM driver is in general only an approximation to the actual hardware
granularity on these systems, and the hardware bandwidth allocation
control value is not natively a percentage -- necessitating a further
conversion in the resctrl_arch_update_domains() path, regardless of the
conversion done at parse time.
For MPAM and x86 use their custom pre-prepared parse-time conversion,
resctrl_arch_preconvert_bw(). This will avoid accumulated error
from rounding the value twice on MPAM systems. For x86 systems there
is no functional change.
Clarify the documentation, but avoid overly exact promises.
Clamping to bw_min and bw_max still feels generic: leave it in the core
code, for now.
[ BH: Split out x86 specific changes ]
Signed-off-by: Dave Martin <Dave.Martin@arm.com>
Signed-off-by: Ben Horgan <Ben.Horgan@arm.com>
Reviewed-by: Ben Horgan <ben.horgan@arm.com>
---
Documentation/filesystems/resctrl.rst | 17 +++++++++--------
fs/resctrl/ctrlmondata.c | 6 +++---
2 files changed, 12 insertions(+), 11 deletions(-)
diff --git a/Documentation/filesystems/resctrl.rst b/Documentation/filesystems/resctrl.rst
index b003bed339fd..4322d8025453 100644
--- a/Documentation/filesystems/resctrl.rst
+++ b/Documentation/filesystems/resctrl.rst
@@ -236,12 +236,11 @@ with respect to allocation:
user can request.
"bandwidth_gran":
- The granularity in which the memory bandwidth
- percentage is allocated. The allocated
- b/w percentage is rounded off to the next
- control step available on the hardware. The
- available bandwidth control steps are:
- min_bandwidth + N * bandwidth_gran.
+ The approximate granularity in which the memory bandwidth
+ percentage is allocated. The allocated bandwidth percentage
+ is rounded up to the next control step available on the
+ hardware. The available hardware steps are no larger than
+ this value.
"delay_linear":
Indicates if the delay scale is linear or
@@ -871,8 +870,10 @@ The minimum bandwidth percentage value for each cpu model is predefined
and can be looked up through "info/MB/min_bandwidth". The bandwidth
granularity that is allocated is also dependent on the cpu model and can
be looked up at "info/MB/bandwidth_gran". The available bandwidth
-control steps are: min_bw + N * bw_gran. Intermediate values are rounded
-to the next control step available on the hardware.
+control steps are, approximately, min_bw + N * bw_gran. The steps may
+appear irregular due to rounding to an exact percentage: bw_gran is the
+maximum interval between the percentage values corresponding to any two
+adjacent steps in the hardware.
The bandwidth throttling is a core specific mechanism on some of Intel
SKUs. Using a high bandwidth and a low bandwidth setting on two threads
diff --git a/fs/resctrl/ctrlmondata.c b/fs/resctrl/ctrlmondata.c
index 9a7dfc48cb2e..934e12f5d145 100644
--- a/fs/resctrl/ctrlmondata.c
+++ b/fs/resctrl/ctrlmondata.c
@@ -37,8 +37,8 @@ typedef int (ctrlval_parser_t)(struct rdt_parse_data *data,
/*
* Check whether MBA bandwidth percentage value is correct. The value is
* checked against the minimum and max bandwidth values specified by the
- * hardware. The allocated bandwidth percentage is rounded to the next
- * control step available on the hardware.
+ * hardware. The allocated bandwidth percentage is converted as
+ * appropriate for consumption by the specific hardware driver.
*/
static bool bw_validate(char *buf, u32 *data, struct rdt_resource *r)
{
@@ -71,7 +71,7 @@ static bool bw_validate(char *buf, u32 *data, struct rdt_resource *r)
return false;
}
- *data = roundup(bw, (unsigned long)r->membw.bw_gran);
+ *data = resctrl_arch_preconvert_bw(bw, r);
return true;
}
--
2.43.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox