Devicetree
 help / color / mirror / Atom feed
* [PATCH v6 00/16] AD9910 Direct Digital Synthesizer
@ 2026-06-18 13:27 Rodrigo Alencar via B4 Relay
  2026-06-18 13:27 ` [PATCH v6 01/16] iio: ABI: add attributes for altcurrent channels Rodrigo Alencar via B4 Relay
                   ` (15 more replies)
  0 siblings, 16 replies; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

This patch series adds support for the Analog Devices AD9910 DDS.

This is a follow-up of the V3/V4/V5 discussion. For V1, we reached into
this channel composition agreement where physical channels may have
sub-channels. That adds the flexibility necessary for this design.
During V2, some feedback indicated that the ABI is too device-specific,
so DRG/RAM destination and operating modes are configured through
alternate paths and profile channels are created. In V3/V4/V5, there was
further discussion on the ABI and on mode priority debug.

The AD9910 DDS core can be driven through several independent mechanisms:
single tone profiles, a digital ramp generator, an internal RAM playback
engine, a parallel data port, and output shift keying. Each of these
represents a distinct signal path into the DDS accumulator, so the driver
models them as separate IIO output channels (all IIO_ALTVOLTAGE type).
This per-channel separation allows userspace to configure each mode
independently through its own set of sysfs attributes, and to
enable/disable modes individually via IIO_CHAN_INFO_ENABLE, relying on
the hardware's own mode selection architecture.

The AD9910 register map is not suited for the regmap framework: register
widths vary across the map (16, 32, and 64 bits). The driver instead
implements direct SPI access helpers with a software register cache, using
type-specific read/write/update functions (ad9910_reg{16,32,64}_{read,
write,update}) that handle endianness conversion and cache coherency.

Registers are cached for several reasons. The control/function registers
(CFR1, CFR2) are frequently queried to determine the current operating
mode (e.g., checking RAM_ENABLE before every profile register access),
and caching avoids repeated SPI read transactions for what are
essentially state checks. The cache also enables efficient
read-modify-write updates on multi-byte registers: the update functions
merge new field values with the cached register content without issuing
a SPI read, and skip the write entirely when the value is unchanged.
Finally, the profile registers serve dual purposes depending on whether
RAM mode is active -- they hold single tone parameters (FTW, POW, ASF)
in normal operation but are repurposed for RAM playback configuration
(start/end address, step rate, operating mode) when RAM is enabled. A
shadow register array (reg_profile[]) preserves the inactive mode's
settings across transitions, so no state is lost when switching between
single tone and RAM operation.

RAM data is loaded through firmware upload infrastructure. Userspace
writes the waveform data as a raw binary buffer (up to 4096 bytes for
the full 1024x32-bit RAM), and the driver reverses the byte array and
transfers it to the device in a single SPI transaction. Per-profile
start/end addresses and playback parameters (operating mode, step rate,
no-dwell control) are also configured through firmware update, using
metadata in the header.

Streaming data to the DDS core through the parallel data port at the
PD_CLK rate is not covered by this series. That functionality would
be added in a separate patch series, building on top of the IIO backend
infrastructure to provide a proper buffered data path.

Kind regards,

Rodrigo Alencar

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
Changes in v6:
- Introduce IIO_FREQUENCY channel type.
- Replace altvoltage channels for altcurrent.
- DRG and Parallel Port destination handled with different channel types.
- Parent attribute shows channel prefix rather than label.
- Link to v5: https://lore.kernel.org/r/20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com

Changes in v5:
- Drop RFC tag to the patch series.
- Address sashiko's comments.
- Add parent-child relationship between iio channels.
- List vs Table changes in documentation.
- Add crc and version check to RAM mode firmware update.
- Link to v4: https://lore.kernel.org/r/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com

Changes in v4:
- Digital Ramp step exposed as a rate of change.
- Dwell modes of Digital Ramp are controlled with dwell_en attribute. 
- Disable of active profile behaves as a software powerdown.
- Expose debugfs attributes to show mode priority.
- Add 64-bit debugfs reg access support into iio core.
- Link to v3: https://lore.kernel.org/r/20260417-ad9910-iio-driver-v3-0-29b93712a228@analog.com

Changes in v3:
- RAM custom configs (address range, destination, modes) loaded during firmware write.
- DRG destination defined when attrs are written.
- DRG modes broken down into enable attrs for ramp up/down channels.
- Add separate profile channels, switching done through enable attr
- Link to v2: https://lore.kernel.org/r/20260318-ad9910-iio-driver-v2-0-e79f93becf11@analog.com

Changes in v2:
- Device-tree bindings changes.
- RAM loading to use firmware update interface.
- Rearrange of channels into a hierarchy.
- Link to v1: https://lore.kernel.org/r/20260220-ad9910-iio-driver-v1-0-3b264aa48a10@analog.com

---
Rodrigo Alencar (16):
      iio: ABI: add attributes for altcurrent channels
      iio: ABI: scale and offset for frequency/phase channels
      iio: ABI: add parent entry for iio channels
      iio: add IIO_FREQUENCY channel type
      iio: core: support 64-bit register through debugfs
      iio: core: create local __iio_chan_prefix_emit() for reuse
      iio: core: add hierarchical channel relationships
      dt-bindings: iio: frequency: add ad9910
      iio: frequency: ad9910: initial driver implementation
      iio: frequency: ad9910: add basic parallel port support
      iio: frequency: ad9910: add digital ramp generator support
      iio: frequency: ad9910: add RAM mode support
      iio: frequency: ad9910: add output shift keying support
      iio: frequency: ad9910: show channel priority in debugfs
      iio: ABI: add docs for ad9910 sysfs entries
      docs: iio: add documentation for ad9910 driver

 Documentation/ABI/testing/sysfs-bus-iio            |   36 +
 .../ABI/testing/sysfs-bus-iio-frequency-ad9910     |   27 +
 .../bindings/iio/frequency/adi,ad9910.yaml         |  189 ++
 Documentation/iio/ad9910.rst                       |  759 +++++++
 Documentation/iio/index.rst                        |    1 +
 MAINTAINERS                                        |   10 +
 drivers/iio/frequency/Kconfig                      |   21 +
 drivers/iio/frequency/Makefile                     |    1 +
 drivers/iio/frequency/ad9910.c                     | 2288 ++++++++++++++++++++
 drivers/iio/industrialio-core.c                    |  245 ++-
 include/linux/iio/iio-opaque.h                     |    2 +-
 include/linux/iio/iio.h                            |    9 +
 include/uapi/linux/iio/types.h                     |    1 +
 tools/iio/iio_event_monitor.c                      |    2 +
 14 files changed, 3482 insertions(+), 109 deletions(-)
---
base-commit: a50909aa46dec46de3c73235fc15a7d6f763d996
change-id: 20260218-ad9910-iio-driver-9b3d214c251f

Best regards,
-- 
Rodrigo Alencar <rodrigo.alencar@analog.com>



^ permalink raw reply	[flat|nested] 28+ messages in thread

* [PATCH v6 01/16] iio: ABI: add attributes for altcurrent channels
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:27 ` [PATCH v6 02/16] iio: ABI: scale and offset for frequency/phase channels Rodrigo Alencar via B4 Relay
                   ` (14 subsequent siblings)
  15 siblings, 0 replies; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add missing attributes applicable to altcurrent channels, e.g., frequency,
phase, scale, raw, label and powerdown.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 Documentation/ABI/testing/sysfs-bus-iio | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/Documentation/ABI/testing/sysfs-bus-iio b/Documentation/ABI/testing/sysfs-bus-iio
index d8d6d85235b0..f52db6abcf31 100644
--- a/Documentation/ABI/testing/sysfs-bus-iio
+++ b/Documentation/ABI/testing/sysfs-bus-iio
@@ -478,6 +478,8 @@ What:		/sys/bus/iio/devices/iio:deviceX/in_currentY_scale
 What:		/sys/bus/iio/devices/iio:deviceX/in_currentY_supply_scale
 What:		/sys/bus/iio/devices/iio:deviceX/in_current_scale
 What:		/sys/bus/iio/devices/iio:deviceX/in_current_q_scale
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_scale
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrent_scale
 What:		/sys/bus/iio/devices/iio:deviceX/in_accel_scale
 What:		/sys/bus/iio/devices/iio:deviceX/in_accel_peak_scale
 What:		/sys/bus/iio/devices/iio:deviceX/in_anglvel_scale
@@ -798,6 +800,8 @@ What:		/sys/bus/iio/devices/iio:deviceX/out_voltageY_powerdown
 What:		/sys/bus/iio/devices/iio:deviceX/out_voltage_powerdown
 What:		/sys/bus/iio/devices/iio:deviceX/out_altvoltageY_powerdown
 What:		/sys/bus/iio/devices/iio:deviceX/out_altvoltage_powerdown
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_powerdown
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrent_powerdown
 KernelVersion:	2.6.38
 Contact:	linux-iio@vger.kernel.org
 Description:
@@ -808,6 +812,7 @@ Description:
 		together.
 
 What:		/sys/bus/iio/devices/iio:deviceX/out_altvoltageY_frequency
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_frequency
 KernelVersion:	3.4.0
 Contact:	linux-iio@vger.kernel.org
 Description:
@@ -834,8 +839,10 @@ Description:
 		all the other channels, since it involves changing the VCO
 		fundamental output frequency.
 
+What:		/sys/bus/iio/devices/iio:deviceX/in_altcurrentY_phase
 What:		/sys/bus/iio/devices/iio:deviceX/in_altvoltageY_i_phase
 What:		/sys/bus/iio/devices/iio:deviceX/in_altvoltageY_q_phase
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_phase
 What:		/sys/bus/iio/devices/iio:deviceX/out_altvoltageY_phase
 What:		/sys/bus/iio/devices/iio:deviceX/out_altvoltageY_i_phase
 What:		/sys/bus/iio/devices/iio:deviceX/out_altvoltageY_q_phase
@@ -1735,6 +1742,8 @@ Description:
 
 What:		/sys/bus/iio/devices/iio:deviceX/in_currentY_raw
 What:		/sys/bus/iio/devices/iio:deviceX/in_currentY_supply_raw
+What:		/sys/bus/iio/devices/iio:deviceX/in_altcurrentY_raw
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_raw
 KernelVersion:	3.17
 Contact:	linux-iio@vger.kernel.org
 Description:
@@ -2125,7 +2134,9 @@ Description:
 
 		Raw counter device counters direction for channel Y.
 
+What:		/sys/bus/iio/devices/iio:deviceX/in_altcurrentY_label
 What:		/sys/bus/iio/devices/iio:deviceX/in_voltageY_label
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_label
 What:		/sys/bus/iio/devices/iio:deviceX/out_voltageY_label
 KernelVersion:	5.8
 Contact:	linux-iio@vger.kernel.org

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 02/16] iio: ABI: scale and offset for frequency/phase channels
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
  2026-06-18 13:27 ` [PATCH v6 01/16] iio: ABI: add attributes for altcurrent channels Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:27 ` [PATCH v6 03/16] iio: ABI: add parent entry for iio channels Rodrigo Alencar via B4 Relay
                   ` (13 subsequent siblings)
  15 siblings, 0 replies; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Consider scale and offset attributes for frequency and phase channels.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 Documentation/ABI/testing/sysfs-bus-iio | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/Documentation/ABI/testing/sysfs-bus-iio b/Documentation/ABI/testing/sysfs-bus-iio
index f52db6abcf31..d9c4e3b53f29 100644
--- a/Documentation/ABI/testing/sysfs-bus-iio
+++ b/Documentation/ABI/testing/sysfs-bus-iio
@@ -441,6 +441,10 @@ What:		/sys/bus/iio/devices/iio:deviceX/in_voltageY_i_offset
 What:		/sys/bus/iio/devices/iio:deviceX/in_voltageY_q_offset
 What:		/sys/bus/iio/devices/iio:deviceX/in_currentY_offset
 What:		/sys/bus/iio/devices/iio:deviceX/in_current_offset
+What:		/sys/bus/iio/devices/iio:deviceX/out_phaseY_offset
+What:		/sys/bus/iio/devices/iio:deviceX/out_phase_offset
+What:		/sys/bus/iio/devices/iio:deviceX/out_frequencyY_offset
+What:		/sys/bus/iio/devices/iio:deviceX/out_frequency_offset
 What:		/sys/bus/iio/devices/iio:deviceX/in_tempY_offset
 What:		/sys/bus/iio/devices/iio:deviceX/in_temp_offset
 What:		/sys/bus/iio/devices/iio:deviceX/in_pressureY_offset
@@ -474,6 +478,10 @@ What:		/sys/bus/iio/devices/iio:deviceX/in_voltage_scale
 What:		/sys/bus/iio/devices/iio:deviceX/in_voltage-voltage_scale
 What:		/sys/bus/iio/devices/iio:deviceX/out_voltageY_scale
 What:		/sys/bus/iio/devices/iio:deviceX/out_altvoltageY_scale
+What:		/sys/bus/iio/devices/iio:deviceX/out_phaseY_scale
+What:		/sys/bus/iio/devices/iio:deviceX/out_phase_scale
+What:		/sys/bus/iio/devices/iio:deviceX/out_frequencyY_scale
+What:		/sys/bus/iio/devices/iio:deviceX/out_frequency_scale
 What:		/sys/bus/iio/devices/iio:deviceX/in_currentY_scale
 What:		/sys/bus/iio/devices/iio:deviceX/in_currentY_supply_scale
 What:		/sys/bus/iio/devices/iio:deviceX/in_current_scale

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 03/16] iio: ABI: add parent entry for iio channels
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
  2026-06-18 13:27 ` [PATCH v6 01/16] iio: ABI: add attributes for altcurrent channels Rodrigo Alencar via B4 Relay
  2026-06-18 13:27 ` [PATCH v6 02/16] iio: ABI: scale and offset for frequency/phase channels Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:27 ` [PATCH v6 04/16] iio: add IIO_FREQUENCY channel type Rodrigo Alencar via B4 Relay
                   ` (12 subsequent siblings)
  15 siblings, 0 replies; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add documentation for a read-only sysfs attribute that allows to expose
parent-child relationships between IIO channels.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 Documentation/ABI/testing/sysfs-bus-iio | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/Documentation/ABI/testing/sysfs-bus-iio b/Documentation/ABI/testing/sysfs-bus-iio
index d9c4e3b53f29..e0f11b39511f 100644
--- a/Documentation/ABI/testing/sysfs-bus-iio
+++ b/Documentation/ABI/testing/sysfs-bus-iio
@@ -2154,6 +2154,23 @@ Description:
 		specific attributes. This is useful for userspace to be able to
 		better identify an individual channel.
 
+What:		/sys/bus/iio/devices/iio:deviceX/in_voltageY_parent
+What:		/sys/bus/iio/devices/iio:deviceX/out_voltageY_parent
+What:		/sys/bus/iio/devices/iio:deviceX/in_altvoltageY_parent
+What:		/sys/bus/iio/devices/iio:deviceX/out_altvoltageY_parent
+What:		/sys/bus/iio/devices/iio:deviceX/in_currentY_parent
+What:		/sys/bus/iio/devices/iio:deviceX/out_currentY_parent
+What:		/sys/bus/iio/devices/iio:deviceX/in_altcurrentY_parent
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_parent
+KernelVersion:	7.2
+Contact:	linux-iio@vger.kernel.org
+Description:
+		Read-only attribute containing the sysfs name prefix of the parent
+		channel for hierarchical channel relationships (e.g. "out_altvoltage0").
+		The prefix uniquely identifies the parent channel and can be used by
+		userspace to organize channels in tree-like structures that reflects
+		the physical or logical relationships between them.
+
 What:		/sys/bus/iio/devices/iio:deviceX/in_phaseY_raw
 KernelVersion:	4.18
 Contact:	linux-iio@vger.kernel.org

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 04/16] iio: add IIO_FREQUENCY channel type
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (2 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 03/16] iio: ABI: add parent entry for iio channels Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:27 ` [PATCH v6 05/16] iio: core: support 64-bit register through debugfs Rodrigo Alencar via B4 Relay
                   ` (11 subsequent siblings)
  15 siblings, 0 replies; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add IIO_FREQUENCY channel type to support Direct Digital Synthesizer (DDS)
devices that handles frequency values written directly to its buffered
interface.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 drivers/iio/industrialio-core.c | 1 +
 include/uapi/linux/iio/types.h  | 1 +
 tools/iio/iio_event_monitor.c   | 2 ++
 3 files changed, 4 insertions(+)

diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
index 93c2540d4cd2..f9fd353f79e1 100644
--- a/drivers/iio/industrialio-core.c
+++ b/drivers/iio/industrialio-core.c
@@ -99,6 +99,7 @@ static const char * const iio_chan_type_name_spec[] = {
 	[IIO_ATTENTION] = "attention",
 	[IIO_ALTCURRENT] = "altcurrent",
 	[IIO_COVERAGE] = "coverage",
+	[IIO_FREQUENCY] = "frequency",
 };
 
 static const char * const iio_modifier_names[] = {
diff --git a/include/uapi/linux/iio/types.h b/include/uapi/linux/iio/types.h
index c9295c707041..1996839d1fce 100644
--- a/include/uapi/linux/iio/types.h
+++ b/include/uapi/linux/iio/types.h
@@ -54,6 +54,7 @@ enum iio_chan_type {
 	IIO_ATTENTION,
 	IIO_ALTCURRENT,
 	IIO_COVERAGE,
+	IIO_FREQUENCY,
 };
 
 enum iio_modifier {
diff --git a/tools/iio/iio_event_monitor.c b/tools/iio/iio_event_monitor.c
index bc3ef4c77c2b..7885f0f7562e 100644
--- a/tools/iio/iio_event_monitor.c
+++ b/tools/iio/iio_event_monitor.c
@@ -66,6 +66,7 @@ static const char * const iio_chan_type_name_spec[] = {
 	[IIO_ATTENTION] = "attention",
 	[IIO_ALTCURRENT] = "altcurrent",
 	[IIO_COVERAGE] = "coverage",
+	[IIO_FREQUENCY] = "frequency",
 };
 
 static const char * const iio_ev_type_text[] = {
@@ -196,6 +197,7 @@ static bool event_is_known(struct iio_event_data *event)
 	case IIO_ATTENTION:
 	case IIO_ALTCURRENT:
 	case IIO_COVERAGE:
+	case IIO_FREQUENCY:
 		break;
 	default:
 		return false;

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 05/16] iio: core: support 64-bit register through debugfs
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (3 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 04/16] iio: add IIO_FREQUENCY channel type Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 14:45   ` Nuno Sá
  2026-06-18 13:27 ` [PATCH v6 06/16] iio: core: create local __iio_chan_prefix_emit() for reuse Rodrigo Alencar via B4 Relay
                   ` (10 subsequent siblings)
  15 siblings, 1 reply; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add debugfs_reg64_access function pointer field into iio_info and modify
file operation callbacks to favor 64-bit variant when it is available.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 drivers/iio/industrialio-core.c | 33 ++++++++++++++++++++++++---------
 include/linux/iio/iio-opaque.h  |  2 +-
 include/linux/iio/iio.h         |  4 ++++
 3 files changed, 29 insertions(+), 10 deletions(-)

diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
index f9fd353f79e1..03019bf9327b 100644
--- a/drivers/iio/industrialio-core.c
+++ b/drivers/iio/industrialio-core.c
@@ -388,6 +388,7 @@ static ssize_t iio_debugfs_read_reg(struct file *file, char __user *userbuf,
 	struct iio_dev *indio_dev = file->private_data;
 	struct iio_dev_opaque *iio_dev_opaque = to_iio_dev_opaque(indio_dev);
 	unsigned int val = 0;
+	u64 val64 = 0;
 	int ret;
 
 	if (*ppos > 0)
@@ -395,9 +396,17 @@ static ssize_t iio_debugfs_read_reg(struct file *file, char __user *userbuf,
 					       iio_dev_opaque->read_buf,
 					       iio_dev_opaque->read_buf_len);
 
-	ret = indio_dev->info->debugfs_reg_access(indio_dev,
-						  iio_dev_opaque->cached_reg_addr,
-						  0, &val);
+	if (indio_dev->info->debugfs_reg64_access) {
+		ret = indio_dev->info->debugfs_reg64_access(indio_dev,
+							    iio_dev_opaque->cached_reg_addr,
+							    0, &val64);
+	} else {
+		ret = indio_dev->info->debugfs_reg_access(indio_dev,
+							  iio_dev_opaque->cached_reg_addr,
+							  0, &val);
+		val64 = val;
+	}
+
 	if (ret) {
 		dev_err(indio_dev->dev.parent, "%s: read failed\n", __func__);
 		return ret;
@@ -405,7 +414,7 @@ static ssize_t iio_debugfs_read_reg(struct file *file, char __user *userbuf,
 
 	iio_dev_opaque->read_buf_len = snprintf(iio_dev_opaque->read_buf,
 						sizeof(iio_dev_opaque->read_buf),
-						"0x%X\n", val);
+						"0x%llX\n", val64);
 
 	return simple_read_from_buffer(userbuf, count, ppos,
 				       iio_dev_opaque->read_buf,
@@ -417,8 +426,9 @@ static ssize_t iio_debugfs_write_reg(struct file *file,
 {
 	struct iio_dev *indio_dev = file->private_data;
 	struct iio_dev_opaque *iio_dev_opaque = to_iio_dev_opaque(indio_dev);
-	unsigned int reg, val;
+	unsigned int reg;
 	char buf[80];
+	u64 val64;
 	int ret;
 
 	if (count >= sizeof(buf))
@@ -431,7 +441,7 @@ static ssize_t iio_debugfs_write_reg(struct file *file,
 
 	buf[ret] = '\0';
 
-	ret = sscanf(buf, "%i %i", &reg, &val);
+	ret = sscanf(buf, "%i %lli", &reg, &val64);
 
 	switch (ret) {
 	case 1:
@@ -439,8 +449,12 @@ static ssize_t iio_debugfs_write_reg(struct file *file,
 		break;
 	case 2:
 		iio_dev_opaque->cached_reg_addr = reg;
-		ret = indio_dev->info->debugfs_reg_access(indio_dev, reg,
-							  val, NULL);
+		if (indio_dev->info->debugfs_reg64_access)
+			ret = indio_dev->info->debugfs_reg64_access(indio_dev, reg,
+								    val64, NULL);
+		else
+			ret = indio_dev->info->debugfs_reg_access(indio_dev, reg,
+								  val64, NULL);
 		if (ret) {
 			dev_err(indio_dev->dev.parent, "%s: write failed\n",
 				__func__);
@@ -471,7 +485,8 @@ static void iio_device_register_debugfs(struct iio_dev *indio_dev)
 {
 	struct iio_dev_opaque *iio_dev_opaque;
 
-	if (indio_dev->info->debugfs_reg_access == NULL)
+	if (!indio_dev->info->debugfs_reg_access &&
+	    !indio_dev->info->debugfs_reg64_access)
 		return;
 
 	if (!iio_debugfs_dentry)
diff --git a/include/linux/iio/iio-opaque.h b/include/linux/iio/iio-opaque.h
index b87841a355f8..98330385e08d 100644
--- a/include/linux/iio/iio-opaque.h
+++ b/include/linux/iio/iio-opaque.h
@@ -73,7 +73,7 @@ struct iio_dev_opaque {
 #if defined(CONFIG_DEBUG_FS)
 	struct dentry			*debugfs_dentry;
 	unsigned int			cached_reg_addr;
-	char				read_buf[20];
+	char				read_buf[24];
 	unsigned int			read_buf_len;
 #endif
 };
diff --git a/include/linux/iio/iio.h b/include/linux/iio/iio.h
index 711c00f67371..1c7d12af22da 100644
--- a/include/linux/iio/iio.h
+++ b/include/linux/iio/iio.h
@@ -484,6 +484,7 @@ struct iio_trigger; /* forward declaration */
  * @update_scan_mode:	function to configure device and scan buffer when
  *			channels have changed
  * @debugfs_reg_access:	function to read or write register value of device
+ * @debugfs_reg64_access: function to read or write 64-bit register value of device
  * @fwnode_xlate:	fwnode based function pointer to obtain channel specifier index.
  * @hwfifo_set_watermark: function pointer to set the current hardware
  *			fifo watermark level; see hwfifo_* entries in
@@ -572,6 +573,9 @@ struct iio_info {
 	int (*debugfs_reg_access)(struct iio_dev *indio_dev,
 				  unsigned int reg, unsigned int writeval,
 				  unsigned int *readval);
+	int (*debugfs_reg64_access)(struct iio_dev *indio_dev,
+				    unsigned int reg, u64 writeval,
+				    u64 *readval);
 	int (*fwnode_xlate)(struct iio_dev *indio_dev,
 			    const struct fwnode_reference_args *iiospec);
 	int (*hwfifo_set_watermark)(struct iio_dev *indio_dev, unsigned int val);

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 06/16] iio: core: create local __iio_chan_prefix_emit() for reuse
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (4 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 05/16] iio: core: support 64-bit register through debugfs Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 15:06   ` Nuno Sá
  2026-06-18 13:27 ` [PATCH v6 07/16] iio: core: add hierarchical channel relationships Rodrigo Alencar via B4 Relay
                   ` (9 subsequent siblings)
  15 siblings, 1 reply; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Move logic to create a channel prefix for naming attribute files into a
separate __iio_chan_prefix_emit() function for reuse.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 drivers/iio/industrialio-core.c | 167 ++++++++++++++++------------------------
 1 file changed, 68 insertions(+), 99 deletions(-)

diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
index 03019bf9327b..9373006235c8 100644
--- a/drivers/iio/industrialio-core.c
+++ b/drivers/iio/industrialio-core.c
@@ -26,6 +26,7 @@
 #include <linux/property.h>
 #include <linux/sched.h>
 #include <linux/slab.h>
+#include <linux/sprintf.h>
 #include <linux/wait.h>
 
 #include <linux/iio/buffer.h>
@@ -199,6 +200,64 @@ static const char * const iio_chan_info_postfix[] = {
 	[IIO_CHAN_INFO_CONVDELAY] = "convdelay",
 	[IIO_CHAN_INFO_POWERFACTOR] = "powerfactor",
 };
+
+static int __iio_chan_prefix_emit(const struct iio_chan_spec *chan,
+				  enum iio_shared_by shared_by,
+				  char *buf, size_t len)
+{
+	const char *dir = iio_direction[chan->output];
+	const char *type = iio_chan_type_name_spec[chan->type];
+	int n = 0;
+
+	switch (shared_by) {
+	case IIO_SHARED_BY_ALL:
+		buf[0] = '\0'; /* empty channel prefix */
+		break;
+	case IIO_SHARED_BY_DIR:
+		n = scnprintf(buf, len, "%s", dir);
+		break;
+	case IIO_SHARED_BY_TYPE:
+		n = scnprintf(buf, len, "%s_%s", dir, type);
+		if (chan->differential)
+			n += scnprintf(buf + n, len - n, "-%s", type);
+		break;
+	case IIO_SEPARATE:
+		if (chan->indexed) {
+			n = scnprintf(buf, len, "%s_%s%d", dir, type,
+				      chan->channel);
+			if (chan->differential)
+				n += scnprintf(buf + n, len - n, "-%s%d", type,
+					       chan->channel2);
+		} else {
+			if (chan->differential) {
+				WARN(1, "Differential channels must be indexed\n");
+				return -EINVAL;
+			}
+			n = scnprintf(buf, len, "%s_%s", dir, type);
+		}
+
+		if (chan->modified) {
+			if (chan->differential) {
+				WARN(1, "Differential channels can not have modifier\n");
+				return -EINVAL;
+			}
+			n += scnprintf(buf + n, len - n, "_%s",
+				       iio_modifier_names[chan->channel2]);
+		}
+
+		if (chan->extend_name)
+			n += scnprintf(buf + n, len - n, "_%s", chan->extend_name);
+		break;
+	}
+
+	if (n > 0 && n < len - 1) { /* prefix termination if not empty */
+		buf[n++] = '_';
+		buf[n] = '\0';
+	}
+
+	return n;
+}
+
 /**
  * iio_device_id() - query the unique ID for the device
  * @indio_dev:		Device structure whose ID is being queried
@@ -1100,106 +1159,19 @@ int __iio_device_attr_init(struct device_attribute *dev_attr,
 						size_t len),
 			   enum iio_shared_by shared_by)
 {
-	int ret = 0;
-	char *name = NULL;
-	char *full_postfix;
+	char prefix[NAME_MAX + 1];
+	int ret;
 
 	sysfs_attr_init(&dev_attr->attr);
 
-	/* Build up postfix of <extend_name>_<modifier>_postfix */
-	if (chan->modified && (shared_by == IIO_SEPARATE)) {
-		if (chan->extend_name)
-			full_postfix = kasprintf(GFP_KERNEL, "%s_%s_%s",
-						 iio_modifier_names[chan->channel2],
-						 chan->extend_name,
-						 postfix);
-		else
-			full_postfix = kasprintf(GFP_KERNEL, "%s_%s",
-						 iio_modifier_names[chan->channel2],
-						 postfix);
-	} else {
-		if (chan->extend_name == NULL || shared_by != IIO_SEPARATE)
-			full_postfix = kstrdup(postfix, GFP_KERNEL);
-		else
-			full_postfix = kasprintf(GFP_KERNEL,
-						 "%s_%s",
-						 chan->extend_name,
-						 postfix);
-	}
-	if (full_postfix == NULL)
+	ret = __iio_chan_prefix_emit(chan, shared_by, prefix, sizeof(prefix));
+	if (ret < 0)
+		return ret;
+
+	dev_attr->attr.name = kasprintf(GFP_KERNEL, "%s%s", prefix, postfix);
+	if (!dev_attr->attr.name)
 		return -ENOMEM;
 
-	if (chan->differential) { /* Differential can not have modifier */
-		switch (shared_by) {
-		case IIO_SHARED_BY_ALL:
-			name = kasprintf(GFP_KERNEL, "%s", full_postfix);
-			break;
-		case IIO_SHARED_BY_DIR:
-			name = kasprintf(GFP_KERNEL, "%s_%s",
-						iio_direction[chan->output],
-						full_postfix);
-			break;
-		case IIO_SHARED_BY_TYPE:
-			name = kasprintf(GFP_KERNEL, "%s_%s-%s_%s",
-					    iio_direction[chan->output],
-					    iio_chan_type_name_spec[chan->type],
-					    iio_chan_type_name_spec[chan->type],
-					    full_postfix);
-			break;
-		case IIO_SEPARATE:
-			if (!chan->indexed) {
-				WARN(1, "Differential channels must be indexed\n");
-				ret = -EINVAL;
-				goto error_free_full_postfix;
-			}
-			name = kasprintf(GFP_KERNEL,
-					    "%s_%s%d-%s%d_%s",
-					    iio_direction[chan->output],
-					    iio_chan_type_name_spec[chan->type],
-					    chan->channel,
-					    iio_chan_type_name_spec[chan->type],
-					    chan->channel2,
-					    full_postfix);
-			break;
-		}
-	} else { /* Single ended */
-		switch (shared_by) {
-		case IIO_SHARED_BY_ALL:
-			name = kasprintf(GFP_KERNEL, "%s", full_postfix);
-			break;
-		case IIO_SHARED_BY_DIR:
-			name = kasprintf(GFP_KERNEL, "%s_%s",
-						iio_direction[chan->output],
-						full_postfix);
-			break;
-		case IIO_SHARED_BY_TYPE:
-			name = kasprintf(GFP_KERNEL, "%s_%s_%s",
-					    iio_direction[chan->output],
-					    iio_chan_type_name_spec[chan->type],
-					    full_postfix);
-			break;
-
-		case IIO_SEPARATE:
-			if (chan->indexed)
-				name = kasprintf(GFP_KERNEL, "%s_%s%d_%s",
-						    iio_direction[chan->output],
-						    iio_chan_type_name_spec[chan->type],
-						    chan->channel,
-						    full_postfix);
-			else
-				name = kasprintf(GFP_KERNEL, "%s_%s_%s",
-						    iio_direction[chan->output],
-						    iio_chan_type_name_spec[chan->type],
-						    full_postfix);
-			break;
-		}
-	}
-	if (name == NULL) {
-		ret = -ENOMEM;
-		goto error_free_full_postfix;
-	}
-	dev_attr->attr.name = name;
-
 	if (readfunc) {
 		dev_attr->attr.mode |= 0444;
 		dev_attr->show = readfunc;
@@ -1210,10 +1182,7 @@ int __iio_device_attr_init(struct device_attribute *dev_attr,
 		dev_attr->store = writefunc;
 	}
 
-error_free_full_postfix:
-	kfree(full_postfix);
-
-	return ret;
+	return 0;
 }
 
 static void __iio_device_attr_deinit(struct device_attribute *dev_attr)

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 07/16] iio: core: add hierarchical channel relationships
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (5 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 06/16] iio: core: create local __iio_chan_prefix_emit() for reuse Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:33   ` sashiko-bot
  2026-06-18 13:27 ` [PATCH v6 08/16] dt-bindings: iio: frequency: add ad9910 Rodrigo Alencar via B4 Relay
                   ` (8 subsequent siblings)
  15 siblings, 1 reply; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add parent-child relationship between iio channels by creating a parent
pointer field in iio_chan_spec struct and exposing a sysfs attribute that
returns the parent channel label.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 drivers/iio/industrialio-core.c | 44 +++++++++++++++++++++++++++++++++++++++++
 include/linux/iio/iio.h         |  5 +++++
 2 files changed, 49 insertions(+)

diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
index 9373006235c8..3d12269f26f4 100644
--- a/drivers/iio/industrialio-core.c
+++ b/drivers/iio/industrialio-core.c
@@ -854,6 +854,21 @@ static ssize_t iio_read_channel_label(struct device *dev,
 					 to_iio_dev_attr(attr)->c, buf);
 }
 
+static ssize_t iio_read_channel_parent(struct device *dev,
+				       struct device_attribute *attr,
+				       char *buf)
+{
+	const struct iio_chan_spec *parent = to_iio_dev_attr(attr)->c->parent;
+	int len;
+
+	len = __iio_chan_prefix_emit(parent, IIO_SEPARATE, buf, PAGE_SIZE - 1);
+	if (len < 0)
+		return len;
+
+	buf[len - 1] = '\n'; /* replace underscore termination with newline */
+	return len;
+}
+
 static ssize_t iio_read_channel_info(struct device *dev,
 				     struct device_attribute *attr,
 				     char *buf)
@@ -1263,6 +1278,30 @@ static int iio_device_add_channel_label(struct iio_dev *indio_dev,
 	return 1;
 }
 
+static int iio_device_add_channel_parent(struct iio_dev *indio_dev,
+					 struct iio_chan_spec const *chan)
+{
+	struct iio_dev_opaque *iio_dev_opaque = to_iio_dev_opaque(indio_dev);
+	int ret;
+
+	if (!chan->parent)
+		return 0;
+
+	ret = __iio_add_chan_devattr("parent",
+				     chan,
+				     &iio_read_channel_parent,
+				     NULL,
+				     0,
+				     IIO_SEPARATE,
+				     &indio_dev->dev,
+				     NULL,
+				     &iio_dev_opaque->channel_attr_list);
+	if (ret < 0)
+		return ret;
+
+	return 1;
+}
+
 static int iio_device_add_info_mask_type(struct iio_dev *indio_dev,
 					 struct iio_chan_spec const *chan,
 					 enum iio_shared_by shared_by,
@@ -1401,6 +1440,11 @@ static int iio_device_add_channel_sysfs(struct iio_dev *indio_dev,
 		return ret;
 	attrcount += ret;
 
+	ret = iio_device_add_channel_parent(indio_dev, chan);
+	if (ret < 0)
+		return ret;
+	attrcount += ret;
+
 	if (chan->ext_info) {
 		unsigned int i = 0;
 
diff --git a/include/linux/iio/iio.h b/include/linux/iio/iio.h
index 1c7d12af22da..9470ab8eb726 100644
--- a/include/linux/iio/iio.h
+++ b/include/linux/iio/iio.h
@@ -264,6 +264,10 @@ struct iio_scan_type {
  * @ext_info:		Array of extended info attributes for this channel.
  *			The array is NULL terminated, the last element should
  *			have its name field set to NULL.
+ * @parent:		Optional pointer to the parent channel spec for
+ *			hierarchical channel relationships. When set, a read-only
+ *			"parent" sysfs attribute is created containing the
+ *			parent channel's sysfs name prefix (e.g. "in_voltage0").
  * @extend_name:	Allows labeling of channel attributes with an
  *			informative name. Note this has no effect codes etc,
  *			unlike modifiers.
@@ -309,6 +313,7 @@ struct iio_chan_spec {
 	const struct iio_event_spec *event_spec;
 	unsigned int		num_event_specs;
 	const struct iio_chan_spec_ext_info *ext_info;
+	const struct iio_chan_spec *parent;
 	const char		*extend_name;
 	const char		*datasheet_name;
 	unsigned int		modified:1;

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 08/16] dt-bindings: iio: frequency: add ad9910
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (6 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 07/16] iio: core: add hierarchical channel relationships Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:35   ` sashiko-bot
  2026-06-18 13:27 ` [PATCH v6 09/16] iio: frequency: ad9910: initial driver implementation Rodrigo Alencar via B4 Relay
                   ` (7 subsequent siblings)
  15 siblings, 1 reply; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

DT-bindings for AD9910, a 1 GSPS DDS with 14-bit DAC. It includes
configurations for clocks, DAC current, reset and basic GPIO control.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 .../bindings/iio/frequency/adi,ad9910.yaml         | 189 +++++++++++++++++++++
 MAINTAINERS                                        |   7 +
 2 files changed, 196 insertions(+)

diff --git a/Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml b/Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
new file mode 100644
index 000000000000..a78fe33ba21f
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
@@ -0,0 +1,189 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/iio/frequency/adi,ad9910.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Analog Devices AD9910 Direct Digital Synthesizer
+
+maintainers:
+  - Rodrigo Alencar <rodrigo.alencar@analog.com>
+
+description:
+  The AD9910 is a 1 GSPS direct digital synthesizer (DDS) with an integrated
+  14-bit DAC. It features single tone mode with 8 configurable profiles,
+  a digital ramp generator, RAM control, OSK, and a parallel data port for
+  high-speed streaming.
+
+  https://www.analog.com/en/products/ad9910.html
+
+properties:
+  compatible:
+    const: adi,ad9910
+
+  reg:
+    maxItems: 1
+
+  spi-max-frequency:
+    maximum: 70000000
+
+  clocks:
+    minItems: 1
+    items:
+      - description: Reference clock (REF_CLK).
+      - description: Optional synchronization clock (SYNC_IN).
+
+  clock-names:
+    minItems: 1
+    items:
+      - const: ref_clk
+      - const: sync_in
+
+  '#clock-cells':
+    const: 1
+
+  clock-output-names:
+    minItems: 1
+    maxItems: 3
+    items:
+      enum: [ sync_clk, pdclk, sync_out ]
+
+  interrupts:
+    minItems: 1
+    maxItems: 2
+    description:
+      Requires interrupt-names property with the same number of items. The
+      supported interrupts are 'drover' (digital ramp generator limit) and
+      'ram_swp_ovr' (end of RAM sweep).
+
+  interrupt-names:
+    minItems: 1
+    maxItems: 2
+    items:
+      enum: [ drover, ram_swp_ovr ]
+
+  dvdd-io33-supply:
+    description: 3.3V Digital I/O supply.
+
+  avdd33-supply:
+    description: 3.3V Analog DAC supply.
+
+  dvdd18-supply:
+    description: 1.8V Digital Core supply.
+
+  avdd18-supply:
+    description: 1.8V Analog Core supply.
+
+  reset-gpios:
+    description:
+      GPIOs controlling the Main Device reset.
+
+  io-reset-gpios:
+    maxItems: 1
+    description:
+      GPIO controlling the I/O_RESET pin.
+
+  powerdown-gpios:
+    maxItems: 1
+    description:
+      GPIO controlling the EXT_PWR_DWN pin.
+
+  update-gpios:
+    maxItems: 1
+    description:
+      GPIO controlling the I/O_UPDATE pin.
+
+  profile-gpios:
+    minItems: 3
+    maxItems: 3
+    description:
+      GPIOs controlling the PROFILE[2:0] pins for profile selection.
+
+  sync-err-gpios:
+    maxItems: 1
+    description:
+      GPIO used to read SYNC_SMP_ERR pin status.
+
+  lock-detect-gpios:
+    maxItems: 1
+    description:
+      GPIO used to read PLL_LOCK pin status.
+
+  adi,pll-enable:
+    type: boolean
+    description:
+      Indicates that a loop filter is connected and the internal PLL is enabled.
+      Often used when the reference clock is provided by a crystal or by a
+      single-ended on-board oscillator.
+
+  adi,charge-pump-current-microamp:
+    minimum: 212
+    maximum: 387
+    default: 212
+    description:
+      PLL charge pump current in microamps. Only applicable when the internal
+      PLL is enabled. The value is rounded to the nearest supported step. This
+      value depends mostly on the loop filter design.
+
+  adi,refclk-out-drive-strength:
+    $ref: /schemas/types.yaml#/definitions/string
+    enum: [ disabled, low, medium, high ]
+    default: disabled
+    description:
+      Reference clock output (DRV0) drive strength. Only applicable when
+      the internal PLL is enabled.
+
+dependencies:
+  adi,charge-pump-current-microamp: [ 'adi,pll-enable' ]
+  adi,refclk-out-drive-strength: [ 'adi,pll-enable' ]
+  lock-detect-gpios: [ 'adi,pll-enable' ]
+  interrupts: [ interrupt-names ]
+  clocks: [ clock-names ]
+  '#clock-cells': [ clock-output-names ]
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - dvdd-io33-supply
+  - avdd33-supply
+  - dvdd18-supply
+  - avdd18-supply
+
+allOf:
+  - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    spi {
+        #address-cells = <1>;
+        #size-cells = <0>;
+        dds@0 {
+            compatible = "adi,ad9910";
+            reg = <0>;
+            spi-max-frequency = <1000000>;
+            clocks = <&ad9910_refclk>;
+            clock-names = "ref_clk";
+
+            dvdd-io33-supply = <&vdd_io33>;
+            avdd33-supply = <&vdd_a33>;
+            dvdd18-supply = <&vdd_d18>;
+            avdd18-supply = <&vdd_a18>;
+
+            reset-gpios = <&gpio 0 GPIO_ACTIVE_HIGH>;
+            io-reset-gpios = <&gpio 1 GPIO_ACTIVE_HIGH>;
+            powerdown-gpios = <&gpio 2 GPIO_ACTIVE_HIGH>;
+            update-gpios = <&gpio 3 GPIO_ACTIVE_HIGH>;
+            profile-gpios = <&gpio 4 GPIO_ACTIVE_HIGH>,
+                            <&gpio 5 GPIO_ACTIVE_HIGH>,
+                            <&gpio 6 GPIO_ACTIVE_HIGH>;
+
+            adi,pll-enable;
+            adi,charge-pump-current-microamp = <387>;
+            adi,refclk-out-drive-strength = "disabled";
+        };
+    };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index b051eccafa60..998b06fd97fd 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1645,6 +1645,13 @@ W:	https://ez.analog.com/linux-software-drivers
 F:	Documentation/devicetree/bindings/iio/dac/adi,ad9739a.yaml
 F:	drivers/iio/dac/ad9739a.c
 
+ANALOG DEVICES INC AD9910 DRIVER
+M:	Rodrigo Alencar <rodrigo.alencar@analog.com>
+L:	linux-iio@vger.kernel.org
+S:	Supported
+W:	https://ez.analog.com/linux-software-drivers
+F:	Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
+
 ANALOG DEVICES INC MAX22007 DRIVER
 M:	Janani Sunil <janani.sunil@analog.com>
 L:	linux-iio@vger.kernel.org

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 09/16] iio: frequency: ad9910: initial driver implementation
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (7 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 08/16] dt-bindings: iio: frequency: add ad9910 Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:37   ` sashiko-bot
  2026-06-18 13:27 ` [PATCH v6 10/16] iio: frequency: ad9910: add basic parallel port support Rodrigo Alencar via B4 Relay
                   ` (6 subsequent siblings)
  15 siblings, 1 reply; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add the core AD9910 DDS driver infrastructure with single tone mode
support. This includes SPI register access, profile management via GPIO
pins, PLL/DAC configuration from firmware properties, and single tone
frequency/phase/amplitude control through IIO attributes.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 MAINTAINERS                    |    1 +
 drivers/iio/frequency/Kconfig  |   18 +
 drivers/iio/frequency/Makefile |    1 +
 drivers/iio/frequency/ad9910.c | 1087 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 1107 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 998b06fd97fd..9b8cef7923ca 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1651,6 +1651,7 @@ L:	linux-iio@vger.kernel.org
 S:	Supported
 W:	https://ez.analog.com/linux-software-drivers
 F:	Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
+F:	drivers/iio/frequency/ad9910.c
 
 ANALOG DEVICES INC MAX22007 DRIVER
 M:	Janani Sunil <janani.sunil@analog.com>
diff --git a/drivers/iio/frequency/Kconfig b/drivers/iio/frequency/Kconfig
index 90c6304c4bcd..6033f9155b4f 100644
--- a/drivers/iio/frequency/Kconfig
+++ b/drivers/iio/frequency/Kconfig
@@ -23,6 +23,24 @@ config AD9523
 
 endmenu
 
+menu "Direct Digital Synthesis"
+
+config AD9910
+	tristate "Analog Devices AD9910 Direct Digital Synthesizer"
+	depends on SPI
+	depends on GPIOLIB
+	help
+	  Say yes here to build support for Analog Devices AD9910
+	  1 GSPS, 14-Bit DDS with integrated DAC.
+
+	  Supports single tone mode with 8 configurable profiles
+	  and digital ramp generation.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called ad9910.
+
+endmenu
+
 #
 # Phase-Locked Loop (PLL) frequency synthesizers
 #
diff --git a/drivers/iio/frequency/Makefile b/drivers/iio/frequency/Makefile
index 53b4d01414d8..e3b1d36ed620 100644
--- a/drivers/iio/frequency/Makefile
+++ b/drivers/iio/frequency/Makefile
@@ -5,6 +5,7 @@
 
 # When adding new entries keep the list in alphabetical order
 obj-$(CONFIG_AD9523) += ad9523.o
+obj-$(CONFIG_AD9910) += ad9910.o
 obj-$(CONFIG_ADF41513) += adf41513.o
 obj-$(CONFIG_ADF4350) += adf4350.o
 obj-$(CONFIG_ADF4371) += adf4371.o
diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
new file mode 100644
index 000000000000..95b01295e4a0
--- /dev/null
+++ b/drivers/iio/frequency/ad9910.c
@@ -0,0 +1,1087 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * AD9910 SPI DDS (Direct Digital Synthesizer) driver
+ *
+ * Copyright 2026 Analog Devices Inc.
+ */
+
+#include <linux/array_size.h>
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/device/devres.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/log2.h>
+#include <linux/math64.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/regulator/consumer.h>
+#include <linux/reset.h>
+#include <linux/spi/spi.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/units.h>
+#include <linux/unaligned.h>
+
+#include <linux/iio/iio.h>
+#include <linux/iio/sysfs.h>
+
+/* Register addresses */
+#define AD9910_REG_CFR1			0x00
+#define AD9910_REG_CFR2			0x01
+#define AD9910_REG_CFR3			0x02
+#define AD9910_REG_AUX_DAC		0x03
+#define AD9910_REG_IO_UPDATE_RATE	0x04
+#define AD9910_REG_FTW			0x07
+#define AD9910_REG_POW			0x08
+#define AD9910_REG_ASF			0x09
+#define AD9910_REG_MULTICHIP_SYNC	0x0A
+#define AD9910_REG_DRG_LIMIT		0x0B
+#define AD9910_REG_DRG_STEP		0x0C
+#define AD9910_REG_DRG_RATE		0x0D
+#define AD9910_REG_PROFILE0		0x0E
+#define AD9910_REG_PROFILE1		0x0F
+#define AD9910_REG_PROFILE2		0x10
+#define AD9910_REG_PROFILE3		0x11
+#define AD9910_REG_PROFILE4		0x12
+#define AD9910_REG_PROFILE5		0x13
+#define AD9910_REG_PROFILE6		0x14
+#define AD9910_REG_PROFILE7		0x15
+#define AD9910_REG_RAM			0x16
+
+#define AD9910_REG_NUM_CACHED		0x16
+#define AD9910_REG_PROFILE(x)		(AD9910_REG_PROFILE0 + (x))
+
+/* CFR1 bit definitions */
+#define AD9910_CFR1_RAM_ENABLE_MSK		BIT(31)
+#define AD9910_CFR1_RAM_PLAYBACK_DEST_MSK	GENMASK(30, 29)
+#define AD9910_CFR1_OSK_MANUAL_EXT_CTL_MSK	BIT(23)
+#define AD9910_CFR1_INV_SINC_EN_MSK		BIT(22)
+#define AD9910_CFR1_INT_PROFILE_CTL_MSK		GENMASK(20, 17)
+#define AD9910_CFR1_SELECT_SINE_MSK		BIT(16)
+#define AD9910_CFR1_LOAD_LRR_IO_UPDATE_MSK	BIT(15)
+#define AD9910_CFR1_AUTOCLR_DIG_RAMP_ACCUM_MSK	BIT(14)
+#define AD9910_CFR1_AUTOCLR_PHASE_ACCUM_MSK	BIT(13)
+#define AD9910_CFR1_CLEAR_DIG_RAMP_ACCUM_MSK	BIT(12)
+#define AD9910_CFR1_CLEAR_PHASE_ACCUM_MSK	BIT(11)
+#define AD9910_CFR1_LOAD_ARR_IO_UPDATE_MSK	BIT(10)
+#define AD9910_CFR1_OSK_ENABLE_MSK		BIT(9)
+#define AD9910_CFR1_SELECT_AUTO_OSK_MSK		BIT(8)
+#define AD9910_CFR1_DIGITAL_POWER_DOWN_MSK	BIT(7)
+#define AD9910_CFR1_DAC_POWER_DOWN_MSK		BIT(6)
+#define AD9910_CFR1_REFCLK_INPUT_POWER_DOWN_MSK	BIT(5)
+#define AD9910_CFR1_AUX_DAC_POWER_DOWN_MSK	BIT(4)
+#define AD9910_CFR1_SOFT_POWER_DOWN_MSK		GENMASK(7, 4)
+#define AD9910_CFR1_EXT_POWER_DOWN_CTL_MSK	BIT(3)
+#define AD9910_CFR1_SDIO_INPUT_ONLY_MSK		BIT(1)
+#define AD9910_CFR1_LSB_FIRST_MSK		BIT(0)
+
+/* CFR2 bit definitions */
+#define AD9910_CFR2_AMP_SCALE_SINGLE_TONE_MSK	BIT(24)
+#define AD9910_CFR2_INTERNAL_IO_UPDATE_MSK	BIT(23)
+#define AD9910_CFR2_SYNC_CLK_EN_MSK		BIT(22)
+#define AD9910_CFR2_DRG_DEST_MSK		GENMASK(21, 20)
+#define AD9910_CFR2_DRG_ENABLE_MSK		BIT(19)
+#define AD9910_CFR2_DRG_NO_DWELL_HIGH_MSK	BIT(18)
+#define AD9910_CFR2_DRG_NO_DWELL_LOW_MSK	BIT(17)
+#define AD9910_CFR2_DRG_NO_DWELL_MSK		GENMASK(18, 17)
+#define AD9910_CFR2_READ_EFFECTIVE_FTW_MSK	BIT(16)
+#define AD9910_CFR2_IO_UPDATE_RATE_CTL_MSK	GENMASK(15, 14)
+#define AD9910_CFR2_PDCLK_ENABLE_MSK		BIT(11)
+#define AD9910_CFR2_PDCLK_INVERT_MSK		BIT(10)
+#define AD9910_CFR2_TXENABLE_INVERT_MSK		BIT(9)
+#define AD9910_CFR2_MATCHED_LATENCY_EN_MSK	BIT(7)
+#define AD9910_CFR2_DATA_ASM_HOLD_LAST_MSK	BIT(6)
+#define AD9910_CFR2_SYNC_TIMING_VAL_DISABLE_MSK	BIT(5)
+#define AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK	BIT(4)
+#define AD9910_CFR2_FM_GAIN_MSK			GENMASK(3, 0)
+
+/* CFR3 bit definitions */
+#define AD9910_CFR3_OPEN_MSK			(BIT(27) | GENMASK(18, 16))
+#define AD9910_CFR3_DRV0_MSK			GENMASK(29, 28)
+#define AD9910_CFR3_VCO_SEL_MSK			GENMASK(26, 24)
+#define AD9910_CFR3_ICP_MSK			GENMASK(21, 19)
+#define AD9910_CFR3_REFCLK_DIV_BYPASS_MSK	BIT(15)
+#define AD9910_CFR3_REFCLK_DIV_RESETB_MSK	BIT(14)
+#define AD9910_CFR3_PFD_RESET_MSK		BIT(10)
+#define AD9910_CFR3_PLL_EN_MSK			BIT(8)
+#define AD9910_CFR3_N_MSK			GENMASK(7, 1)
+
+/* Auxiliary DAC Control Register Bits */
+#define AD9910_AUX_DAC_FSC_MSK			GENMASK(7, 0)
+
+/* ASF Register Bits */
+#define AD9910_ASF_RAMP_RATE_MSK		GENMASK(31, 16)
+#define AD9910_ASF_SCALE_FACTOR_MSK		GENMASK(15, 2)
+#define AD9910_ASF_STEP_SIZE_MSK		GENMASK(1, 0)
+
+/* Multichip Sync Register Bits */
+#define AD9910_MC_SYNC_VALIDATION_DELAY_MSK	GENMASK(31, 28)
+#define AD9910_MC_SYNC_RECEIVER_ENABLE_MSK	BIT(27)
+#define AD9910_MC_SYNC_GENERATOR_ENABLE_MSK	BIT(26)
+#define AD9910_MC_SYNC_GENERATOR_POLARITY_MSK	BIT(25)
+#define AD9910_MC_SYNC_STATE_PRESET_MSK		GENMASK(23, 18)
+#define AD9910_MC_SYNC_OUTPUT_DELAY_MSK		GENMASK(15, 11)
+#define AD9910_MC_SYNC_INPUT_DELAY_MSK		GENMASK(7, 3)
+
+/* Profile Register Format (Single Tone Mode) */
+#define AD9910_PROFILE_ST_ASF_MSK		GENMASK_ULL(61, 48)
+#define AD9910_PROFILE_ST_POW_MSK		GENMASK_ULL(47, 32)
+#define AD9910_PROFILE_ST_FTW_MSK		GENMASK_ULL(31, 0)
+
+/* Device constants */
+#define AD9910_PI_NANORAD		3141592653UL
+
+#define AD9910_MAX_SYSCLK_HZ		(1000 * HZ_PER_MHZ)
+#define AD9910_MAX_PHASE_MICRORAD	(AD9910_PI_NANORAD / 500)
+
+#define AD9910_ASF_MAX			FIELD_MAX(AD9910_PROFILE_ST_ASF_MSK)
+#define AD9910_POW_MAX			FIELD_MAX(AD9910_PROFILE_ST_POW_MSK)
+#define AD9910_NUM_PROFILES		8
+
+/* PLL constants */
+#define AD9910_PLL_MIN_N		12
+#define AD9910_PLL_MAX_N		127
+
+#define AD9910_PLL_IN_MIN_FREQ_HZ	(3307 * HZ_PER_KHZ)
+#define AD9910_PLL_IN_MAX_FREQ_HZ	(60 * HZ_PER_MHZ)
+
+#define AD9910_PLL_OUT_MIN_FREQ_HZ	(420 * HZ_PER_MHZ)
+#define AD9910_PLL_OUT_MAX_FREQ_HZ	AD9910_MAX_SYSCLK_HZ
+
+#define AD9910_VCO0_RANGE_AUTO_MAX_HZ	(457 * HZ_PER_MHZ)
+#define AD9910_VCO1_RANGE_AUTO_MAX_HZ	(530 * HZ_PER_MHZ)
+#define AD9910_VCO2_RANGE_AUTO_MAX_HZ	(632 * HZ_PER_MHZ)
+#define AD9910_VCO3_RANGE_AUTO_MAX_HZ	(775 * HZ_PER_MHZ)
+#define AD9910_VCO4_RANGE_AUTO_MAX_HZ	(897 * HZ_PER_MHZ)
+#define AD9910_VCO_RANGE_NUM		6
+
+#define AD9910_ICP_MIN_uA		212
+#define AD9910_ICP_MAX_uA		387
+#define AD9910_ICP_STEP_uA		25
+
+#define AD9910_DAC_IOUT_MAX_uA		31590
+#define AD9910_DAC_IOUT_DEFAULT_uA	20070
+#define AD9910_DAC_IOUT_MIN_uA		8640
+
+/* altcurrent ABI is in mA */
+#define AD9910_NANO_MILLIAMP_PER_MICROAMP	1000000UL
+
+#define AD9910_REFDIV2_MIN_FREQ_HZ	(120 * HZ_PER_MHZ)
+#define AD9910_REFDIV2_MAX_FREQ_HZ	(1900 * HZ_PER_MHZ)
+
+#define AD9910_WAKEUP_DELAY_us		1000	/* 1ms: datasheet Table 1*/
+#define AD9910_RESET_DELAY_us		1	/* 5 sysclk cycles: < 1us */
+
+#define AD9910_SPI_DATA_IDX		1
+#define AD9910_SPI_DATA_LEN_MAX		sizeof(__be64)
+#define AD9910_SPI_MESSAGE_LEN_MAX	(AD9910_SPI_DATA_IDX + AD9910_SPI_DATA_LEN_MAX)
+#define AD9910_SPI_READ_MSK		BIT(7)
+#define AD9910_SPI_ADDR_MSK		GENMASK(4, 0)
+
+/**
+ * enum ad9910_channel - AD9910 channel identifiers in priority order
+ *
+ * @AD9910_CHANNEL_PHY: Physical output channel
+ * @AD9910_CHANNEL_PROFILE_0: Profile 0 output channel
+ * @AD9910_CHANNEL_PROFILE_1: Profile 1 output channel
+ * @AD9910_CHANNEL_PROFILE_2: Profile 2 output channel
+ * @AD9910_CHANNEL_PROFILE_3: Profile 3 output channel
+ * @AD9910_CHANNEL_PROFILE_4: Profile 4 output channel
+ * @AD9910_CHANNEL_PROFILE_5: Profile 5 output channel
+ * @AD9910_CHANNEL_PROFILE_6: Profile 6 output channel
+ * @AD9910_CHANNEL_PROFILE_7: Profile 7 output channel
+ */
+enum ad9910_channel {
+	AD9910_CHANNEL_PHY = 100,
+	AD9910_CHANNEL_PROFILE_0 = 110,
+	AD9910_CHANNEL_PROFILE_1 = 111,
+	AD9910_CHANNEL_PROFILE_2 = 112,
+	AD9910_CHANNEL_PROFILE_3 = 113,
+	AD9910_CHANNEL_PROFILE_4 = 114,
+	AD9910_CHANNEL_PROFILE_5 = 115,
+	AD9910_CHANNEL_PROFILE_6 = 116,
+	AD9910_CHANNEL_PROFILE_7 = 117,
+};
+
+enum {
+	AD9910_CHAN_IDX_PHY,
+	AD9910_CHAN_IDX_PROFILE_0,
+	AD9910_CHAN_IDX_PROFILE_1,
+	AD9910_CHAN_IDX_PROFILE_2,
+	AD9910_CHAN_IDX_PROFILE_3,
+	AD9910_CHAN_IDX_PROFILE_4,
+	AD9910_CHAN_IDX_PROFILE_5,
+	AD9910_CHAN_IDX_PROFILE_6,
+	AD9910_CHAN_IDX_PROFILE_7,
+};
+
+enum {
+	AD9910_POWERDOWN,
+};
+
+struct ad9910_data {
+	u32 sysclk_freq_hz;
+	u32 output_current_uA;
+
+	u16 pll_charge_pump_current;
+	u8 refclk_out_drv;
+	bool pll_enabled;
+};
+
+union ad9910_reg {
+	u64 val64;
+	u32 val32;
+	u16 val16;
+};
+
+struct ad9910_state {
+	struct spi_device *spi;
+	struct clk *refclk;
+
+	struct gpio_desc *gpio_pwdown;
+	struct gpio_desc *gpio_update;
+	struct gpio_descs *gpio_profile;
+
+	/* cached registers */
+	union ad9910_reg reg[AD9910_REG_NUM_CACHED];
+
+	/* Lock for accessing device registers and state variables */
+	struct mutex lock;
+
+	struct ad9910_data data;
+	u8 profile;
+
+	/*
+	 * RAM loading requires a reasonable amount of bytes, at the same time
+	 * DMA capable SPI drivers requires the transfer buffers to live in
+	 * their own cache lines.
+	 */
+	u8 tx_buf[AD9910_SPI_MESSAGE_LEN_MAX] __aligned(IIO_DMA_MINALIGN);
+};
+
+/**
+ * ad9910_rational_scale() - Perform scaling of input given a reference.
+ * @input: The input value to be scaled.
+ * @scale: The numerator of the scaling factor.
+ * @reference: The denominator of the scaling factor.
+ *
+ * Closest rounding with mul_u64_add_u64_div_u64
+ *
+ * Return: The scaled value.
+ */
+static inline u64 ad9910_rational_scale(u64 input, u64 scale, u64 reference)
+{
+	return mul_u64_add_u64_div_u64(input, scale, reference >> 1, reference);
+}
+
+static int ad9910_io_update(struct ad9910_state *st)
+{
+	if (st->gpio_update) {
+		gpiod_set_value_cansleep(st->gpio_update, 1);
+		fsleep(1);
+		gpiod_set_value_cansleep(st->gpio_update, 0);
+	}
+
+	return 0;
+}
+
+static inline int ad9910_spi_read(struct ad9910_state *st, u8 reg, void *data,
+				  size_t len)
+{
+	u8 inst = AD9910_SPI_READ_MSK | FIELD_PREP(AD9910_SPI_ADDR_MSK, reg);
+
+	return spi_write_then_read(st->spi, &inst, sizeof(inst), data, len);
+}
+
+static inline int ad9910_spi_write(struct ad9910_state *st, u8 reg, size_t len,
+				   bool update)
+{
+	int ret;
+
+	st->tx_buf[0] = FIELD_PREP(AD9910_SPI_ADDR_MSK, reg);
+	ret = spi_write(st->spi, st->tx_buf, AD9910_SPI_DATA_IDX + len);
+	if (ret)
+		return ret;
+
+	if (update)
+		return ad9910_io_update(st);
+
+	return 0;
+}
+
+#define AD9910_REG_READ_FN(nb)						\
+static int ad9910_reg##nb##_read(struct ad9910_state *st, u8 reg,	\
+				 u##nb * data)				\
+{									\
+	__be##nb be_data;						\
+	int ret;							\
+									\
+	ret = ad9910_spi_read(st, reg, &be_data, sizeof(be_data));	\
+	if (ret)							\
+		return ret;						\
+									\
+	*data = be##nb##_to_cpu(be_data);				\
+	return ret;							\
+}
+
+AD9910_REG_READ_FN(16)
+AD9910_REG_READ_FN(32)
+AD9910_REG_READ_FN(64)
+
+#define AD9910_REG_WRITE_FN(nb)						\
+static int ad9910_reg##nb##_write(struct ad9910_state *st, u8 reg,	\
+				  u##nb data, bool update)		\
+{									\
+	int ret;							\
+									\
+	put_unaligned_be##nb(data, &st->tx_buf[AD9910_SPI_DATA_IDX]);	\
+	ret = ad9910_spi_write(st, reg, sizeof(data), update);		\
+	if (ret)							\
+		return ret;						\
+									\
+	st->reg[reg].val##nb = data;					\
+	return ret;							\
+}
+
+AD9910_REG_WRITE_FN(16)
+AD9910_REG_WRITE_FN(32)
+AD9910_REG_WRITE_FN(64)
+
+#define AD9910_REG_UPDATE_FN(nb)					\
+static int ad9910_reg##nb##_update(struct ad9910_state *st,		\
+				   u8 reg, u##nb mask,			\
+				   u##nb data, bool update)		\
+{									\
+	u##nb reg_val = (st->reg[reg].val##nb & ~mask) | (data & mask);	\
+									\
+	if (reg_val == st->reg[reg].val##nb && !update)			\
+		return 0;						\
+									\
+	return ad9910_reg##nb##_write(st, reg, reg_val, update);	\
+}
+
+AD9910_REG_UPDATE_FN(16)
+AD9910_REG_UPDATE_FN(32)
+AD9910_REG_UPDATE_FN(64)
+
+static int ad9910_set_dac_current(struct ad9910_state *st, u32 val_uA,
+				  bool update)
+{
+	u32 code;
+
+	/* FSC = (86.4 / Rset) * (1 + CODE/96) where Rset = 10k ohms */
+	val_uA = clamp(val_uA, AD9910_DAC_IOUT_MIN_uA, AD9910_DAC_IOUT_MAX_uA);
+	code = DIV_ROUND_CLOSEST(val_uA - AD9910_DAC_IOUT_MIN_uA, 90);
+	st->data.output_current_uA = AD9910_DAC_IOUT_MIN_uA + code * 90;
+
+	return ad9910_reg32_write(st, AD9910_REG_AUX_DAC, code, update);
+}
+
+static int ad9910_set_sysclk_freq(struct ad9910_state *st, u32 freq_hz,
+				  bool update)
+{
+	struct device *dev = &st->spi->dev;
+	u32 sysclk_freq_hz, refclk_freq_hz;
+	u32 tmp32, vco_sel;
+	int ret;
+
+	if (!freq_hz || freq_hz > AD9910_MAX_SYSCLK_HZ)
+		return -EINVAL;
+
+	refclk_freq_hz = clk_get_rate(st->refclk);
+	if (st->data.pll_enabled) {
+		if (refclk_freq_hz < AD9910_PLL_IN_MIN_FREQ_HZ ||
+		    refclk_freq_hz > AD9910_PLL_IN_MAX_FREQ_HZ) {
+			dev_err(dev,
+				"REF_CLK frequency %u Hz is out of PLL input range\n",
+				refclk_freq_hz);
+			return -ERANGE;
+		}
+
+		tmp32 = DIV_ROUND_CLOSEST(freq_hz, refclk_freq_hz);
+		tmp32 = clamp(tmp32, DIV_ROUND_UP(AD9910_PLL_OUT_MIN_FREQ_HZ, refclk_freq_hz),
+			      AD9910_PLL_OUT_MAX_FREQ_HZ / refclk_freq_hz);
+		tmp32 = clamp(tmp32, AD9910_PLL_MIN_N, AD9910_PLL_MAX_N);
+		sysclk_freq_hz = refclk_freq_hz * tmp32;
+
+		if (sysclk_freq_hz <= AD9910_VCO0_RANGE_AUTO_MAX_HZ)
+			vco_sel = 0;
+		else if (sysclk_freq_hz <= AD9910_VCO1_RANGE_AUTO_MAX_HZ)
+			vco_sel = 1;
+		else if (sysclk_freq_hz <= AD9910_VCO2_RANGE_AUTO_MAX_HZ)
+			vco_sel = 2;
+		else if (sysclk_freq_hz <= AD9910_VCO3_RANGE_AUTO_MAX_HZ)
+			vco_sel = 3;
+		else if (sysclk_freq_hz <= AD9910_VCO4_RANGE_AUTO_MAX_HZ)
+			vco_sel = 4;
+		else
+			vco_sel = 5;
+
+		ret = ad9910_reg32_update(st, AD9910_REG_CFR3,
+					  AD9910_CFR3_N_MSK | AD9910_CFR3_VCO_SEL_MSK,
+					  FIELD_PREP(AD9910_CFR3_N_MSK, tmp32) |
+					  FIELD_PREP(AD9910_CFR3_VCO_SEL_MSK, vco_sel),
+					  update);
+		if (ret)
+			return ret;
+	} else {
+		tmp32 = DIV_ROUND_CLOSEST(refclk_freq_hz, freq_hz);
+		tmp32 = clamp(tmp32, 1U, 2U);
+		sysclk_freq_hz = refclk_freq_hz / tmp32;
+		tmp32 = AD9910_CFR3_VCO_SEL_MSK |
+			FIELD_PREP(AD9910_CFR3_REFCLK_DIV_BYPASS_MSK, tmp32 % 2);
+		ret = ad9910_reg32_update(st, AD9910_REG_CFR3,
+					  AD9910_CFR3_VCO_SEL_MSK |
+					  AD9910_CFR3_REFCLK_DIV_BYPASS_MSK,
+					  tmp32, update);
+		if (ret)
+			return ret;
+	}
+
+	st->data.sysclk_freq_hz = sysclk_freq_hz;
+
+	return 0;
+}
+
+static int ad9910_profile_set(struct ad9910_state *st, u8 profile)
+{
+	DECLARE_BITMAP(values, BITS_PER_TYPE(profile));
+
+	st->profile = profile;
+	values[0] = profile;
+	gpiod_multi_set_value_cansleep(st->gpio_profile, values);
+
+	return 0;
+}
+
+static inline bool ad9910_sw_powerdown_get(struct ad9910_state *st)
+{
+	return FIELD_GET(AD9910_CFR1_SOFT_POWER_DOWN_MSK,
+			 st->reg[AD9910_REG_CFR1].val32) ? true : false;
+}
+
+static int ad9910_sw_powerdown_set(struct ad9910_state *st, bool enable)
+{
+	if (ad9910_sw_powerdown_get(st) == enable)
+		return 0;
+
+	return ad9910_reg32_update(st, AD9910_REG_CFR1,
+				   AD9910_CFR1_SOFT_POWER_DOWN_MSK,
+				   enable ? AD9910_CFR1_SOFT_POWER_DOWN_MSK : 0,
+				   true);
+}
+
+static ssize_t ad9910_ext_info_read(struct iio_dev *indio_dev,
+				    uintptr_t private,
+				    const struct iio_chan_spec *chan,
+				    char *buf)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	int val;
+
+	guard(mutex)(&st->lock);
+
+	switch (private) {
+	case AD9910_POWERDOWN:
+		val = ad9910_sw_powerdown_get(st);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return iio_format_value(buf, IIO_VAL_INT, 1, &val);
+}
+
+static ssize_t ad9910_ext_info_write(struct iio_dev *indio_dev,
+				     uintptr_t private,
+				     const struct iio_chan_spec *chan,
+				     const char *buf, size_t len)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	u32 val32;
+	int ret;
+
+	ret = kstrtou32(buf, 10, &val32);
+	if (ret)
+		return ret;
+
+	guard(mutex)(&st->lock);
+
+	switch (private) {
+	case AD9910_POWERDOWN:
+		ret = ad9910_sw_powerdown_set(st, val32 ? true : false);
+		if (ret)
+			return ret;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return len;
+}
+
+static const struct iio_chan_spec_ext_info ad9910_phy_ext_info[] = {
+	{
+		.name = "powerdown",
+		.read = ad9910_ext_info_read,
+		.write = ad9910_ext_info_write,
+		.private = AD9910_POWERDOWN,
+		.shared = IIO_SEPARATE,
+	},
+	{ }
+};
+
+#define AD9910_PROFILE_CHAN(idx) {				\
+	.type = IIO_ALTCURRENT,					\
+	.indexed = 1,						\
+	.output = 1,						\
+	.channel = AD9910_CHANNEL_PROFILE_ ## idx,		\
+	.address = AD9910_CHAN_IDX_PROFILE_ ## idx,		\
+	.info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE) |	\
+			      BIT(IIO_CHAN_INFO_FREQUENCY) |	\
+			      BIT(IIO_CHAN_INFO_PHASE) |	\
+			      BIT(IIO_CHAN_INFO_RAW),		\
+	.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],	\
+}
+
+static const struct iio_chan_spec ad9910_channels[] = {
+	[AD9910_CHAN_IDX_PHY] = {
+		.type = IIO_ALTCURRENT,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_PHY,
+		.address = AD9910_CHAN_IDX_PHY,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_SAMP_FREQ),
+		.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
+		.ext_info = ad9910_phy_ext_info,
+	},
+	[AD9910_CHAN_IDX_PROFILE_0] = AD9910_PROFILE_CHAN(0),
+	[AD9910_CHAN_IDX_PROFILE_1] = AD9910_PROFILE_CHAN(1),
+	[AD9910_CHAN_IDX_PROFILE_2] = AD9910_PROFILE_CHAN(2),
+	[AD9910_CHAN_IDX_PROFILE_3] = AD9910_PROFILE_CHAN(3),
+	[AD9910_CHAN_IDX_PROFILE_4] = AD9910_PROFILE_CHAN(4),
+	[AD9910_CHAN_IDX_PROFILE_5] = AD9910_PROFILE_CHAN(5),
+	[AD9910_CHAN_IDX_PROFILE_6] = AD9910_PROFILE_CHAN(6),
+	[AD9910_CHAN_IDX_PROFILE_7] = AD9910_PROFILE_CHAN(7),
+};
+
+static int ad9910_read_raw(struct iio_dev *indio_dev,
+			   struct iio_chan_spec const *chan,
+			   int *val, int *val2, long info)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	u64 tmp64;
+	u32 tmp32;
+
+	guard(mutex)(&st->lock);
+
+	switch (info) {
+	case IIO_CHAN_INFO_ENABLE:
+		switch (chan->channel) {
+		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+			if (ad9910_sw_powerdown_get(st)) {
+				*val = 0;
+			} else {
+				tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+				*val = (tmp32 == st->profile);
+			}
+			break;
+		default:
+			return -EINVAL;
+		}
+		return IIO_VAL_INT;
+	case IIO_CHAN_INFO_FREQUENCY:
+		switch (chan->channel) {
+		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+			tmp64 = FIELD_GET(AD9910_PROFILE_ST_FTW_MSK,
+					  st->reg[AD9910_REG_PROFILE(tmp32)].val64);
+			break;
+		default:
+			return -EINVAL;
+		}
+		tmp64 *= st->data.sysclk_freq_hz;
+		*val = tmp64 >> 32;
+		*val2 = ((tmp64 & GENMASK_ULL(31, 0)) * MICRO) >> 32;
+		return IIO_VAL_INT_PLUS_MICRO;
+	case IIO_CHAN_INFO_PHASE:
+		switch (chan->channel) {
+		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+			tmp64 = FIELD_GET(AD9910_PROFILE_ST_POW_MSK,
+					  st->reg[AD9910_REG_PROFILE(tmp32)].val64);
+			break;
+		default:
+			return -EINVAL;
+		}
+		tmp32 = (tmp64 * AD9910_MAX_PHASE_MICRORAD) >> 16;
+		*val = tmp32 / MICRO;
+		*val2 = tmp32 % MICRO;
+		return IIO_VAL_INT_PLUS_MICRO;
+	case IIO_CHAN_INFO_RAW:
+		switch (chan->channel) {
+		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+			*val = FIELD_GET(AD9910_PROFILE_ST_ASF_MSK,
+					 st->reg[AD9910_REG_PROFILE(tmp32)].val64);
+			return IIO_VAL_INT;
+		default:
+			return -EINVAL;
+		}
+	case IIO_CHAN_INFO_SAMP_FREQ:
+		switch (chan->channel) {
+		case AD9910_CHANNEL_PHY:
+			*val = st->data.sysclk_freq_hz;
+			return IIO_VAL_INT;
+		default:
+			return -EINVAL;
+		}
+	case IIO_CHAN_INFO_SCALE:
+		tmp64 = (u64)st->data.output_current_uA *
+			AD9910_NANO_MILLIAMP_PER_MICROAMP;
+		*val = 0;
+		*val2 = tmp64 >> 14;
+		return IIO_VAL_INT_PLUS_NANO;
+	default:
+		return -EINVAL;
+	}
+}
+
+static int ad9910_write_raw(struct iio_dev *indio_dev,
+			    struct iio_chan_spec const *chan,
+			    int val, int val2, long info)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	u64 tmp64;
+	u32 tmp32;
+	int ret;
+
+	guard(mutex)(&st->lock);
+
+	switch (info) {
+	case IIO_CHAN_INFO_ENABLE:
+		switch (chan->channel) {
+		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+			if (!val) {
+				if (tmp32 != st->profile)
+					return 0; /* nothing to do */
+
+				return ad9910_sw_powerdown_set(st, true);
+			}
+
+			ret = ad9910_sw_powerdown_set(st, false);
+			if (ret)
+				return ret;
+
+			return ad9910_profile_set(st, tmp32);
+		default:
+			return -EINVAL;
+		}
+	case IIO_CHAN_INFO_FREQUENCY:
+		if (val < 0 || val2 < 0 || val >= st->data.sysclk_freq_hz / 2)
+			return -EINVAL;
+
+		tmp64 = ad9910_rational_scale((u64)val * MICRO + val2, BIT_ULL(32),
+					      (u64)MICRO * st->data.sysclk_freq_hz);
+		tmp64 = min_t(u64, tmp64, U32_MAX);
+		switch (chan->channel) {
+		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+			tmp64 = FIELD_PREP(AD9910_PROFILE_ST_FTW_MSK, tmp64);
+			return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
+						   AD9910_PROFILE_ST_FTW_MSK,
+						   tmp64, true);
+		default:
+			return -EINVAL;
+		}
+	case IIO_CHAN_INFO_PHASE:
+		if (val < 0 || val2 < 0)
+			return -EINVAL;
+
+		tmp64 = (u64)val * MICRO + val2;
+		if (tmp64 >= AD9910_MAX_PHASE_MICRORAD)
+			return -EINVAL;
+
+		tmp64 = DIV_U64_ROUND_CLOSEST(tmp64 << 16, AD9910_MAX_PHASE_MICRORAD);
+		tmp64 = min(tmp64, AD9910_POW_MAX);
+
+		switch (chan->channel) {
+		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+			tmp64 = FIELD_PREP(AD9910_PROFILE_ST_POW_MSK, tmp64);
+			return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
+						   AD9910_PROFILE_ST_POW_MSK,
+						   tmp64, true);
+		default:
+			return -EINVAL;
+		}
+	case IIO_CHAN_INFO_RAW:
+		if (val < 0)
+			return -EINVAL;
+
+		switch (chan->channel) {
+		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+			tmp64 = FIELD_PREP(AD9910_PROFILE_ST_ASF_MSK,
+					   min_t(u64, val, AD9910_ASF_MAX));
+			return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
+						   AD9910_PROFILE_ST_ASF_MSK,
+						   tmp64, true);
+		default:
+			return -EINVAL;
+		}
+	case IIO_CHAN_INFO_SAMP_FREQ:
+		return ad9910_set_sysclk_freq(st, val, true);
+	case IIO_CHAN_INFO_SCALE:
+		if (val != 0 || val2 < 0)
+			return -EINVAL;
+
+		tmp32 = DIV_U64_ROUND_CLOSEST((u64)val2 << 14,
+					      AD9910_NANO_MILLIAMP_PER_MICROAMP);
+		return ad9910_set_dac_current(st, tmp32, true);
+	default:
+		return -EINVAL;
+	}
+}
+
+static int ad9910_write_raw_get_fmt(struct iio_dev *indio_dev,
+				    struct iio_chan_spec const *chan,
+				    long mask)
+{
+	switch (mask) {
+	case IIO_CHAN_INFO_ENABLE:
+		return IIO_VAL_INT;
+	case IIO_CHAN_INFO_FREQUENCY:
+	case IIO_CHAN_INFO_PHASE:
+		return IIO_VAL_INT_PLUS_MICRO;
+	case IIO_CHAN_INFO_RAW:
+		switch (chan->channel) {
+		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+			return IIO_VAL_INT;
+		default:
+			return -EINVAL;
+		}
+	case IIO_CHAN_INFO_SAMP_FREQ:
+		return IIO_VAL_INT;
+	case IIO_CHAN_INFO_SCALE:
+		return IIO_VAL_INT_PLUS_NANO;
+	default:
+		return -EINVAL;
+	}
+}
+
+static int ad9910_debugfs_reg_access(struct iio_dev *indio_dev,
+				     unsigned int reg, u64 writeval,
+				     u64 *readval)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	union ad9910_reg tmp;
+	int ret;
+
+	if (reg >= AD9910_REG_RAM)
+		return -EINVAL;
+
+	guard(mutex)(&st->lock);
+
+	switch (reg) {
+	case AD9910_REG_DRG_LIMIT:
+	case AD9910_REG_DRG_STEP:
+	case AD9910_REG_PROFILE0 ... AD9910_REG_PROFILE7:
+		if (!readval)
+			return ad9910_reg64_write(st, reg, writeval, true);
+
+		ret = ad9910_reg64_read(st, reg, &tmp.val64);
+		if (ret)
+			return ret;
+		*readval = tmp.val64;
+		return 0;
+	case AD9910_REG_POW:
+		if (!readval)
+			return ad9910_reg16_write(st, reg, writeval, true);
+
+		ret = ad9910_reg16_read(st, reg, &tmp.val16);
+		if (ret)
+			return ret;
+		*readval = tmp.val16;
+		return 0;
+	default:
+		if (!readval)
+			return ad9910_reg32_write(st, reg, writeval, true);
+
+		ret = ad9910_reg32_read(st, reg, &tmp.val32);
+		if (ret)
+			return ret;
+		*readval = tmp.val32;
+		return 0;
+	}
+}
+
+static const char * const ad9910_channel_str[] = {
+	[AD9910_CHAN_IDX_PHY] = "phy",
+	[AD9910_CHAN_IDX_PROFILE_0] = "profile0",
+	[AD9910_CHAN_IDX_PROFILE_1] = "profile1",
+	[AD9910_CHAN_IDX_PROFILE_2] = "profile2",
+	[AD9910_CHAN_IDX_PROFILE_3] = "profile3",
+	[AD9910_CHAN_IDX_PROFILE_4] = "profile4",
+	[AD9910_CHAN_IDX_PROFILE_5] = "profile5",
+	[AD9910_CHAN_IDX_PROFILE_6] = "profile6",
+	[AD9910_CHAN_IDX_PROFILE_7] = "profile7",
+};
+
+static int ad9910_read_label(struct iio_dev *indio_dev,
+			     struct iio_chan_spec const *chan,
+			     char *label)
+{
+	return sysfs_emit(label, "%s\n", ad9910_channel_str[chan->address]);
+}
+
+static const struct iio_info ad9910_info = {
+	.read_raw = ad9910_read_raw,
+	.write_raw = ad9910_write_raw,
+	.write_raw_get_fmt = ad9910_write_raw_get_fmt,
+	.read_label = ad9910_read_label,
+	.debugfs_reg64_access = &ad9910_debugfs_reg_access,
+};
+
+static int ad9910_cfg_sysclk(struct ad9910_state *st, bool update)
+{
+	u32 cfr3 = AD9910_CFR3_OPEN_MSK;
+	u32 tmp32;
+
+	cfr3 |= FIELD_PREP(AD9910_CFR3_DRV0_MSK, st->data.refclk_out_drv);
+
+	if (st->data.pll_enabled) {
+		tmp32 = st->data.pll_charge_pump_current - AD9910_ICP_MIN_uA;
+		tmp32 = DIV_ROUND_CLOSEST(tmp32, AD9910_ICP_STEP_uA);
+		cfr3 |= FIELD_PREP(AD9910_CFR3_ICP_MSK, tmp32) |
+			AD9910_CFR3_PLL_EN_MSK;
+	} else {
+		cfr3 |= AD9910_CFR3_ICP_MSK |
+			AD9910_CFR3_REFCLK_DIV_RESETB_MSK |
+			AD9910_CFR3_PFD_RESET_MSK;
+	}
+	st->reg[AD9910_REG_CFR3].val32 = cfr3;
+
+	return ad9910_set_sysclk_freq(st, AD9910_MAX_SYSCLK_HZ, update);
+}
+
+static int ad9910_parse_fw(struct ad9910_state *st)
+{
+	static const char * const refclk_out_drv0[] = {
+		"disabled", "low", "medium", "high",
+	};
+	struct device *dev = &st->spi->dev;
+	const char *prop;
+	u32 tmp;
+	int ret;
+
+	st->data.pll_enabled = device_property_read_bool(dev, "adi,pll-enable");
+	if (st->data.pll_enabled) {
+		tmp = AD9910_ICP_MIN_uA;
+		prop = "adi,charge-pump-current-microamp";
+		if (device_property_present(dev, prop)) {
+			ret = device_property_read_u32(dev, prop, &tmp);
+			if (ret)
+				return dev_err_probe(dev, ret, "property read: %s\n", prop);
+
+			if (tmp < AD9910_ICP_MIN_uA || tmp > AD9910_ICP_MAX_uA)
+				return dev_err_probe(dev, -ERANGE,
+						     "invalid charge pump current %u\n", tmp);
+		}
+		st->data.pll_charge_pump_current = tmp;
+
+		prop = "adi,refclk-out-drive-strength";
+		if (device_property_present(dev, prop)) {
+			ret = device_property_match_property_string(dev, prop,
+								    refclk_out_drv0,
+								    ARRAY_SIZE(refclk_out_drv0));
+			if (ret < 0)
+				return dev_err_probe(dev, ret, "property read: %s\n", prop);
+
+			st->data.refclk_out_drv = ret;
+		}
+	}
+
+	return 0;
+}
+
+static void ad9910_sw_powerdown_action(void *data)
+{
+	ad9910_sw_powerdown_set(data, true);
+}
+
+static void ad9910_hw_powerdown_action(void *data)
+{
+	struct ad9910_state *st = data;
+
+	gpiod_set_value_cansleep(st->gpio_pwdown, 1);
+}
+
+static int ad9910_setup(struct device *dev, struct ad9910_state *st,
+			struct reset_control *dev_rst)
+{
+	int ret;
+
+	ret = reset_control_assert(dev_rst);
+	if (ret)
+		return ret;
+
+	fsleep(AD9910_RESET_DELAY_us);
+
+	ret = reset_control_deassert(dev_rst);
+	if (ret)
+		return ret;
+
+	ret = ad9910_reg32_write(st, AD9910_REG_CFR1,
+				 (st->spi->mode & SPI_3WIRE ? 0 :
+				 AD9910_CFR1_SDIO_INPUT_ONLY_MSK), false);
+	if (ret)
+		return ret;
+
+	ret = devm_add_action_or_reset(dev, ad9910_sw_powerdown_action, st);
+	if (ret)
+		return ret;
+
+	ret = ad9910_reg32_write(st, AD9910_REG_CFR2,
+				 AD9910_CFR2_AMP_SCALE_SINGLE_TONE_MSK |
+				 AD9910_CFR2_SYNC_TIMING_VAL_DISABLE_MSK |
+				 AD9910_CFR2_DRG_NO_DWELL_MSK |
+				 AD9910_CFR2_DATA_ASM_HOLD_LAST_MSK |
+				 AD9910_CFR2_SYNC_CLK_EN_MSK |
+				 AD9910_CFR2_PDCLK_ENABLE_MSK, false);
+	if (ret)
+		return ret;
+
+	ret = ad9910_cfg_sysclk(st, false);
+	if (ret)
+		return ret;
+
+	ret = ad9910_set_dac_current(st, AD9910_DAC_IOUT_DEFAULT_uA, false);
+	if (ret)
+		return ret;
+
+	return ad9910_io_update(st);
+}
+
+static int ad9910_probe(struct spi_device *spi)
+{
+	static const char * const supplies[] = {
+		"dvdd-io33", "avdd33", "dvdd18", "avdd18",
+	};
+	struct device *dev = &spi->dev;
+	struct reset_control *dev_rst;
+	struct gpio_desc *io_rst_gpio;
+	struct iio_dev *indio_dev;
+	struct ad9910_state *st;
+	int ret;
+
+	indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
+	if (!indio_dev)
+		return -ENOMEM;
+
+	st = iio_priv(indio_dev);
+	st->spi = spi;
+
+	indio_dev->name = "ad9910";
+	indio_dev->info = &ad9910_info;
+	indio_dev->modes = INDIO_DIRECT_MODE;
+	indio_dev->channels = ad9910_channels;
+	indio_dev->num_channels = ARRAY_SIZE(ad9910_channels);
+
+	ret = devm_mutex_init(dev, &st->lock);
+	if (ret)
+		return ret;
+
+	st->refclk = devm_clk_get_enabled(dev, "ref_clk");
+	if (IS_ERR(st->refclk))
+		return dev_err_probe(dev, PTR_ERR(st->refclk),
+				     "Failed to get reference clock\n");
+
+	dev_rst = devm_reset_control_get_optional_exclusive(dev, NULL);
+	if (IS_ERR(dev_rst))
+		return dev_err_probe(dev, PTR_ERR(dev_rst),
+				     "failed to get device reset control\n");
+
+	/*
+	 * The IO RESET pin is not used in this driver, as we assume that all
+	 * SPI transfers are complete, but if it is wired up, we need to make
+	 * sure it is not floating. We can use either a reset controller or a
+	 * GPIO for this.
+	 */
+	io_rst_gpio = devm_gpiod_get_optional(dev, "io-reset", GPIOD_OUT_LOW);
+	if (IS_ERR(io_rst_gpio))
+		return dev_err_probe(dev, PTR_ERR(io_rst_gpio),
+				     "failed to get io reset gpio\n");
+
+	st->gpio_update = devm_gpiod_get_optional(dev, "update", GPIOD_OUT_LOW);
+	if (IS_ERR(st->gpio_update))
+		return dev_err_probe(dev, PTR_ERR(st->gpio_update),
+				     "failed to get update gpio\n");
+
+	st->gpio_profile = devm_gpiod_get_array_optional(dev, "profile",
+							 GPIOD_OUT_LOW);
+	if (IS_ERR(st->gpio_profile))
+		return dev_err_probe(dev, PTR_ERR(st->gpio_profile),
+				     "failed to get profile gpios\n");
+
+	if (st->gpio_profile && st->gpio_profile->ndescs != 3)
+		return dev_err_probe(dev, -EINVAL,
+				     "invalid number of profile gpios\n");
+
+	st->gpio_pwdown = devm_gpiod_get_optional(dev, "powerdown",
+						  GPIOD_OUT_LOW);
+	if (IS_ERR(st->gpio_pwdown))
+		return dev_err_probe(dev, PTR_ERR(st->gpio_pwdown),
+				     "failed to get powerdown gpio\n");
+
+	ret = devm_add_action_or_reset(dev, ad9910_hw_powerdown_action, st);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to add hw powerdown action\n");
+
+	ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(supplies), supplies);
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to get regulators\n");
+
+	fsleep(AD9910_WAKEUP_DELAY_us);
+
+	ret = ad9910_parse_fw(st);
+	if (ret)
+		return ret;
+
+	ret = ad9910_setup(dev, st, dev_rst);
+	if (ret)
+		return dev_err_probe(dev, ret, "device setup failed\n");
+
+	return devm_iio_device_register(dev, indio_dev);
+}
+
+static const struct spi_device_id ad9910_id[] = {
+	{ .name = "ad9910" },
+	{ }
+};
+MODULE_DEVICE_TABLE(spi, ad9910_id);
+
+static const struct of_device_id ad9910_of_match[] = {
+	{ .compatible = "adi,ad9910" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, ad9910_of_match);
+
+static struct spi_driver ad9910_driver = {
+	.driver = {
+		.name = "ad9910",
+		.of_match_table = ad9910_of_match,
+	},
+	.probe = ad9910_probe,
+	.id_table = ad9910_id,
+};
+module_spi_driver(ad9910_driver);
+
+MODULE_AUTHOR("Rodrigo Alencar <rodrigo.alencar@analog.com>");
+MODULE_DESCRIPTION("Analog Devices AD9910 DDS driver");
+MODULE_LICENSE("GPL");

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 10/16] iio: frequency: ad9910: add basic parallel port support
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (8 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 09/16] iio: frequency: ad9910: initial driver implementation Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:41   ` sashiko-bot
  2026-06-18 13:27 ` [PATCH v6 11/16] iio: frequency: ad9910: add digital ramp generator support Rodrigo Alencar via B4 Relay
                   ` (5 subsequent siblings)
  15 siblings, 1 reply; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add parallel port support with amplitude, phase and frequency channels.
Those will be buffered capable channels, but only basic control of offset
and scale are implemented at this point. There are separate amplitude
and phase control for polar destination, which will provide different scan
types. Enabling and disabling of parallel mode will be implemented with
buffer setup ops or with update_scan_mode() once IIO backend integration
is in place.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 drivers/iio/frequency/ad9910.c | 187 +++++++++++++++++++++++++++++++++++++++--
 1 file changed, 178 insertions(+), 9 deletions(-)

diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index 95b01295e4a0..262702b62738 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -112,9 +112,13 @@
 /* Auxiliary DAC Control Register Bits */
 #define AD9910_AUX_DAC_FSC_MSK			GENMASK(7, 0)
 
+/* POW Register Bits */
+#define AD9910_POW_PP_LSB_MSK			GENMASK(7, 0)
+
 /* ASF Register Bits */
 #define AD9910_ASF_RAMP_RATE_MSK		GENMASK(31, 16)
 #define AD9910_ASF_SCALE_FACTOR_MSK		GENMASK(15, 2)
+#define AD9910_ASF_SCALE_FACTOR_PP_LSB_MSK	GENMASK(7, 2)
 #define AD9910_ASF_STEP_SIZE_MSK		GENMASK(1, 0)
 
 /* Multichip Sync Register Bits */
@@ -138,7 +142,9 @@
 #define AD9910_MAX_PHASE_MICRORAD	(AD9910_PI_NANORAD / 500)
 
 #define AD9910_ASF_MAX			FIELD_MAX(AD9910_PROFILE_ST_ASF_MSK)
+#define AD9910_ASF_PP_LSB_MAX		FIELD_MAX(AD9910_ASF_SCALE_FACTOR_PP_LSB_MSK)
 #define AD9910_POW_MAX			FIELD_MAX(AD9910_PROFILE_ST_POW_MSK)
+#define AD9910_POW_PP_LSB_MAX		FIELD_MAX(AD9910_POW_PP_LSB_MSK)
 #define AD9910_NUM_PROFILES		8
 
 /* PLL constants */
@@ -193,6 +199,8 @@
  * @AD9910_CHANNEL_PROFILE_5: Profile 5 output channel
  * @AD9910_CHANNEL_PROFILE_6: Profile 6 output channel
  * @AD9910_CHANNEL_PROFILE_7: Profile 7 output channel
+ * @AD9910_CHANNEL_PARALLEL: Parallel Data output channel
+ * @AD9910_CHANNEL_PARALLEL_POLAR: Parallel Polar Data output channel
  */
 enum ad9910_channel {
 	AD9910_CHANNEL_PHY = 100,
@@ -204,6 +212,8 @@ enum ad9910_channel {
 	AD9910_CHANNEL_PROFILE_5 = 115,
 	AD9910_CHANNEL_PROFILE_6 = 116,
 	AD9910_CHANNEL_PROFILE_7 = 117,
+	AD9910_CHANNEL_PARALLEL = 120,
+	AD9910_CHANNEL_PARALLEL_POLAR = 121,
 };
 
 enum {
@@ -216,6 +226,11 @@ enum {
 	AD9910_CHAN_IDX_PROFILE_5,
 	AD9910_CHAN_IDX_PROFILE_6,
 	AD9910_CHAN_IDX_PROFILE_7,
+	AD9910_CHAN_IDX_PARALLEL_AMP,
+	AD9910_CHAN_IDX_PARALLEL_PHASE,
+	AD9910_CHAN_IDX_PARALLEL_FREQ,
+	AD9910_CHAN_IDX_PARALLEL_POLAR_AMP,
+	AD9910_CHAN_IDX_PARALLEL_POLAR_PHASE,
 };
 
 enum {
@@ -566,6 +581,53 @@ static const struct iio_chan_spec ad9910_channels[] = {
 	[AD9910_CHAN_IDX_PROFILE_5] = AD9910_PROFILE_CHAN(5),
 	[AD9910_CHAN_IDX_PROFILE_6] = AD9910_PROFILE_CHAN(6),
 	[AD9910_CHAN_IDX_PROFILE_7] = AD9910_PROFILE_CHAN(7),
+	[AD9910_CHAN_IDX_PARALLEL_AMP] = {
+		.type = IIO_ALTCURRENT,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_PARALLEL,
+		.address = AD9910_CHAN_IDX_PARALLEL_AMP,
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
+	[AD9910_CHAN_IDX_PARALLEL_PHASE] = {
+		.type = IIO_PHASE,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_PARALLEL,
+		.address = AD9910_CHAN_IDX_PARALLEL_PHASE,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_SCALE),
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
+	[AD9910_CHAN_IDX_PARALLEL_FREQ] = {
+		.type = IIO_FREQUENCY,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_PARALLEL,
+		.address = AD9910_CHAN_IDX_PARALLEL_FREQ,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_OFFSET) |
+				      BIT(IIO_CHAN_INFO_SCALE),
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
+	[AD9910_CHAN_IDX_PARALLEL_POLAR_AMP] = {
+		.type = IIO_ALTCURRENT,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_PARALLEL_POLAR,
+		.address = AD9910_CHAN_IDX_PARALLEL_POLAR_AMP,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_OFFSET) |
+				      BIT(IIO_CHAN_INFO_SCALE),
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
+	[AD9910_CHAN_IDX_PARALLEL_POLAR_PHASE] = {
+		.type = IIO_PHASE,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_PARALLEL_POLAR,
+		.address = AD9910_CHAN_IDX_PARALLEL_POLAR_PHASE,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_OFFSET) |
+				      BIT(IIO_CHAN_INFO_SCALE),
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
 };
 
 static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -640,11 +702,60 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 			return -EINVAL;
 		}
 	case IIO_CHAN_INFO_SCALE:
-		tmp64 = (u64)st->data.output_current_uA *
-			AD9910_NANO_MILLIAMP_PER_MICROAMP;
-		*val = 0;
-		*val2 = tmp64 >> 14;
-		return IIO_VAL_INT_PLUS_NANO;
+		switch (chan->address) {
+		case AD9910_CHAN_IDX_PHY:
+			tmp64 = (u64)st->data.output_current_uA *
+				AD9910_NANO_MILLIAMP_PER_MICROAMP;
+			*val = 0;
+			*val2 = tmp64 >> 14;
+			return IIO_VAL_INT_PLUS_NANO;
+		case AD9910_CHAN_IDX_PARALLEL_PHASE:
+			*val = 0;
+			*val2 = AD9910_PI_NANORAD >> 15;
+			return IIO_VAL_INT_PLUS_NANO;
+		case AD9910_CHAN_IDX_PARALLEL_FREQ:
+			tmp32 = FIELD_GET(AD9910_CFR2_FM_GAIN_MSK,
+					  st->reg[AD9910_REG_CFR2].val32);
+			tmp64 = st->data.sysclk_freq_hz << tmp32;
+			tmp64 = ad9910_rational_scale(tmp64, NANO, BIT_ULL(32));
+			*val = div_s64_rem(tmp64, NANO, val2);
+			return IIO_VAL_INT_PLUS_NANO;
+		case AD9910_CHAN_IDX_PARALLEL_POLAR_AMP:
+			tmp64 = (u64)st->data.output_current_uA *
+				AD9910_NANO_MILLIAMP_PER_MICROAMP;
+			*val = 0;
+			*val2 = tmp64 >> 8;
+			return IIO_VAL_INT_PLUS_NANO;
+		case AD9910_CHAN_IDX_PARALLEL_POLAR_PHASE:
+			*val = 0;
+			*val2 = AD9910_PI_NANORAD >> 7;
+			return IIO_VAL_INT_PLUS_NANO;
+		default:
+			return -EINVAL;
+		}
+	case IIO_CHAN_INFO_OFFSET:
+		switch (chan->address) {
+		case AD9910_CHAN_IDX_PARALLEL_FREQ:
+			tmp64 = (u64)st->reg[AD9910_REG_FTW].val32 * MICRO;
+			tmp64 >>= FIELD_GET(AD9910_CFR2_FM_GAIN_MSK,
+					    st->reg[AD9910_REG_CFR2].val32);
+			*val = div_s64_rem(tmp64, MICRO, val2);
+			return IIO_VAL_INT_PLUS_MICRO;
+		case AD9910_CHAN_IDX_PARALLEL_POLAR_AMP:
+			tmp32 = FIELD_GET(AD9910_ASF_SCALE_FACTOR_PP_LSB_MSK,
+					  st->reg[AD9910_REG_ASF].val32);
+			*val = 0;
+			*val2 = MICRO * tmp32 >> 6;
+			return IIO_VAL_INT_PLUS_MICRO;
+		case AD9910_CHAN_IDX_PARALLEL_POLAR_PHASE:
+			tmp32 = FIELD_GET(AD9910_POW_PP_LSB_MSK,
+					  st->reg[AD9910_REG_POW].val16);
+			*val = 0;
+			*val2 = MICRO * tmp32 >> 8;
+			return IIO_VAL_INT_PLUS_MICRO;
+		default:
+			return -EINVAL;
+		}
 	default:
 		return -EINVAL;
 	}
@@ -737,12 +848,63 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 	case IIO_CHAN_INFO_SAMP_FREQ:
 		return ad9910_set_sysclk_freq(st, val, true);
 	case IIO_CHAN_INFO_SCALE:
-		if (val != 0 || val2 < 0)
+		switch (chan->address) {
+		case AD9910_CHAN_IDX_PHY:
+			if (val != 0 || val2 < 0)
+				return -EINVAL;
+
+			tmp32 = DIV_U64_ROUND_CLOSEST((u64)val2 << 14,
+						      AD9910_NANO_MILLIAMP_PER_MICROAMP);
+			return ad9910_set_dac_current(st, tmp32, true);
+		case AD9910_CHAN_IDX_PARALLEL_FREQ:
+			if (val < 0 || val2 < 0)
+				return -EINVAL;
+
+			tmp64 = ad9910_rational_scale((u64)val * NANO + val2, BIT_ULL(32),
+						      (u64)st->data.sysclk_freq_hz * NANO);
+			tmp64 = roundup_pow_of_two(max(tmp64, 1ULL));
+			tmp32 = min_t(u32, ilog2(tmp64), FIELD_MAX(AD9910_CFR2_FM_GAIN_MSK));
+			tmp32 = FIELD_PREP(AD9910_CFR2_FM_GAIN_MSK, tmp32);
+			return ad9910_reg32_update(st, AD9910_REG_CFR2,
+						   AD9910_CFR2_FM_GAIN_MSK,
+						   tmp32, true);
+		default:
+			return -EINVAL;
+		}
+	case IIO_CHAN_INFO_OFFSET:
+		if (val < 0 || val2 < 0)
 			return -EINVAL;
 
-		tmp32 = DIV_U64_ROUND_CLOSEST((u64)val2 << 14,
-					      AD9910_NANO_MILLIAMP_PER_MICROAMP);
-		return ad9910_set_dac_current(st, tmp32, true);
+		switch (chan->address) {
+		case AD9910_CHAN_IDX_PARALLEL_FREQ:
+			tmp64 = (u64)val * MICRO + val2;
+			tmp64 <<= FIELD_GET(AD9910_CFR2_FM_GAIN_MSK,
+					    st->reg[AD9910_REG_CFR2].val32);
+			tmp64 = min_t(u64, DIV_U64_ROUND_CLOSEST(tmp64, MICRO),
+				      U32_MAX);
+			return ad9910_reg32_write(st, AD9910_REG_FTW, tmp64, true);
+		case AD9910_CHAN_IDX_PARALLEL_POLAR_AMP:
+			if (val != 0 || val2 < 0)
+				return -EINVAL;
+			tmp32 = DIV_U64_ROUND_CLOSEST((u64)val2 << 6, MICRO);
+			tmp32 = min(tmp32, AD9910_ASF_PP_LSB_MAX);
+			tmp32 = FIELD_PREP(AD9910_ASF_SCALE_FACTOR_PP_LSB_MSK, tmp32);
+			return ad9910_reg32_update(st, AD9910_REG_ASF,
+						   AD9910_ASF_SCALE_FACTOR_PP_LSB_MSK,
+						   tmp32, true);
+		case AD9910_CHAN_IDX_PARALLEL_POLAR_PHASE:
+			if (val != 0 || val2 < 0)
+				return -EINVAL;
+
+			tmp32 = DIV_U64_ROUND_CLOSEST((u64)val2 << 8, MICRO);
+			tmp32 = min(tmp32, AD9910_POW_PP_LSB_MAX);
+			tmp32 = FIELD_PREP(AD9910_POW_PP_LSB_MSK, tmp32);
+			return ad9910_reg16_update(st, AD9910_REG_POW,
+						   AD9910_POW_PP_LSB_MSK,
+						   tmp32, true);
+		default:
+			return -EINVAL;
+		}
 	default:
 		return -EINVAL;
 	}
@@ -769,6 +931,8 @@ static int ad9910_write_raw_get_fmt(struct iio_dev *indio_dev,
 		return IIO_VAL_INT;
 	case IIO_CHAN_INFO_SCALE:
 		return IIO_VAL_INT_PLUS_NANO;
+	case IIO_CHAN_INFO_OFFSET:
+		return IIO_VAL_INT_PLUS_MICRO;
 	default:
 		return -EINVAL;
 	}
@@ -830,6 +994,11 @@ static const char * const ad9910_channel_str[] = {
 	[AD9910_CHAN_IDX_PROFILE_5] = "profile5",
 	[AD9910_CHAN_IDX_PROFILE_6] = "profile6",
 	[AD9910_CHAN_IDX_PROFILE_7] = "profile7",
+	[AD9910_CHAN_IDX_PARALLEL_AMP] = "parallel_amplitude",
+	[AD9910_CHAN_IDX_PARALLEL_PHASE] = "parallel_phase",
+	[AD9910_CHAN_IDX_PARALLEL_FREQ] = "parallel_frequency",
+	[AD9910_CHAN_IDX_PARALLEL_POLAR_AMP] = "parallel_polar_amplitude",
+	[AD9910_CHAN_IDX_PARALLEL_POLAR_PHASE] = "parallel_polar_phase",
 };
 
 static int ad9910_read_label(struct iio_dev *indio_dev,

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 11/16] iio: frequency: ad9910: add digital ramp generator support
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (9 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 10/16] iio: frequency: ad9910: add basic parallel port support Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:42   ` sashiko-bot
  2026-06-18 13:27 ` [PATCH v6 12/16] iio: frequency: ad9910: add RAM mode support Rodrigo Alencar via B4 Relay
                   ` (4 subsequent siblings)
  15 siblings, 1 reply; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add Digital Ramp Generator channels with destination selection (frequency,
phase, or amplitude) based on attribute writes, dwell mode control,
configurable upper/lower limits, step size controlled with rate of change
config, and step rate controlled as sampling frequency.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 drivers/iio/frequency/ad9910.c | 345 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 340 insertions(+), 5 deletions(-)

diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index 262702b62738..3fe97aa887c3 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -130,6 +130,18 @@
 #define AD9910_MC_SYNC_OUTPUT_DELAY_MSK		GENMASK(15, 11)
 #define AD9910_MC_SYNC_INPUT_DELAY_MSK		GENMASK(7, 3)
 
+/* Digital Ramp Limit Register */
+#define AD9910_DRG_LIMIT_UPPER_MSK		GENMASK_ULL(63, 32)
+#define AD9910_DRG_LIMIT_LOWER_MSK		GENMASK_ULL(31, 0)
+
+/* Digital Ramp Step Register */
+#define AD9910_DRG_STEP_DEC_MSK			GENMASK_ULL(63, 32)
+#define AD9910_DRG_STEP_INC_MSK			GENMASK_ULL(31, 0)
+
+/* Digital Ramp Rate Register */
+#define AD9910_DRG_RATE_DEC_MSK			GENMASK(31, 16)
+#define AD9910_DRG_RATE_INC_MSK			GENMASK(15, 0)
+
 /* Profile Register Format (Single Tone Mode) */
 #define AD9910_PROFILE_ST_ASF_MSK		GENMASK_ULL(61, 48)
 #define AD9910_PROFILE_ST_POW_MSK		GENMASK_ULL(47, 32)
@@ -137,6 +149,7 @@
 
 /* Device constants */
 #define AD9910_PI_NANORAD		3141592653UL
+#define AD9910_PI_PICORAD		3141592653590ULL
 
 #define AD9910_MAX_SYSCLK_HZ		(1000 * HZ_PER_MHZ)
 #define AD9910_MAX_PHASE_MICRORAD	(AD9910_PI_NANORAD / 500)
@@ -145,6 +158,7 @@
 #define AD9910_ASF_PP_LSB_MAX		FIELD_MAX(AD9910_ASF_SCALE_FACTOR_PP_LSB_MSK)
 #define AD9910_POW_MAX			FIELD_MAX(AD9910_PROFILE_ST_POW_MSK)
 #define AD9910_POW_PP_LSB_MAX		FIELD_MAX(AD9910_POW_PP_LSB_MSK)
+#define AD9910_STEP_RATE_MAX		FIELD_MAX(AD9910_DRG_RATE_DEC_MSK)
 #define AD9910_NUM_PROFILES		8
 
 /* PLL constants */
@@ -174,6 +188,7 @@
 
 /* altcurrent ABI is in mA */
 #define AD9910_NANO_MILLIAMP_PER_MICROAMP	1000000UL
+#define AD9910_PICO_MILLIAMP_PER_MICROAMP	1000000000UL
 
 #define AD9910_REFDIV2_MIN_FREQ_HZ	(120 * HZ_PER_MHZ)
 #define AD9910_REFDIV2_MAX_FREQ_HZ	(1900 * HZ_PER_MHZ)
@@ -201,6 +216,9 @@
  * @AD9910_CHANNEL_PROFILE_7: Profile 7 output channel
  * @AD9910_CHANNEL_PARALLEL: Parallel Data output channel
  * @AD9910_CHANNEL_PARALLEL_POLAR: Parallel Polar Data output channel
+ * @AD9910_CHANNEL_DRG: Digital Ramp Generator output channel
+ * @AD9910_CHANNEL_DRG_RAMP_UP: DRG ramp up channel
+ * @AD9910_CHANNEL_DRG_RAMP_DOWN: DRG ramp down channel
  */
 enum ad9910_channel {
 	AD9910_CHANNEL_PHY = 100,
@@ -214,6 +232,9 @@ enum ad9910_channel {
 	AD9910_CHANNEL_PROFILE_7 = 117,
 	AD9910_CHANNEL_PARALLEL = 120,
 	AD9910_CHANNEL_PARALLEL_POLAR = 121,
+	AD9910_CHANNEL_DRG = 130,
+	AD9910_CHANNEL_DRG_RAMP_UP = 131,
+	AD9910_CHANNEL_DRG_RAMP_DOWN = 132,
 };
 
 enum {
@@ -231,10 +252,17 @@ enum {
 	AD9910_CHAN_IDX_PARALLEL_FREQ,
 	AD9910_CHAN_IDX_PARALLEL_POLAR_AMP,
 	AD9910_CHAN_IDX_PARALLEL_POLAR_PHASE,
+	AD9910_CHAN_IDX_DRG_FREQ,
+	AD9910_CHAN_IDX_DRG_PHASE,
+	AD9910_CHAN_IDX_DRG_AMP,
+	AD9910_CHAN_IDX_DRG_AMP_RAMP_UP,
+	AD9910_CHAN_IDX_DRG_AMP_RAMP_DOWN,
 };
 
 enum {
 	AD9910_POWERDOWN,
+	AD9910_DWELL_EN,
+	AD9910_ROC,
 };
 
 struct ad9910_data {
@@ -503,6 +531,14 @@ static ssize_t ad9910_ext_info_read(struct iio_dev *indio_dev,
 	case AD9910_POWERDOWN:
 		val = ad9910_sw_powerdown_get(st);
 		break;
+	case AD9910_DWELL_EN:
+		if (chan->channel == AD9910_CHANNEL_DRG_RAMP_UP)
+			val = FIELD_GET(AD9910_CFR2_DRG_NO_DWELL_HIGH_MSK,
+					st->reg[AD9910_REG_CFR2].val32) ? 0 : 1;
+		else
+			val = FIELD_GET(AD9910_CFR2_DRG_NO_DWELL_LOW_MSK,
+					st->reg[AD9910_REG_CFR2].val32) ? 0 : 1;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -531,6 +567,113 @@ static ssize_t ad9910_ext_info_write(struct iio_dev *indio_dev,
 		if (ret)
 			return ret;
 		break;
+	case AD9910_DWELL_EN:
+		if (chan->channel == AD9910_CHANNEL_DRG_RAMP_UP) {
+			val32 = val32 ? 0 : AD9910_CFR2_DRG_NO_DWELL_HIGH_MSK;
+			ret = ad9910_reg32_update(st, AD9910_REG_CFR2,
+						  AD9910_CFR2_DRG_NO_DWELL_HIGH_MSK,
+						  val32, true);
+			if (ret)
+				return ret;
+		} else {
+			val32 = val32 ? 0 : AD9910_CFR2_DRG_NO_DWELL_LOW_MSK;
+			ret = ad9910_reg32_update(st, AD9910_REG_CFR2,
+						  AD9910_CFR2_DRG_NO_DWELL_LOW_MSK,
+						  val32, true);
+			if (ret)
+				return ret;
+		}
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return len;
+}
+
+static ssize_t ad9910_drg_roc_read(struct iio_dev *indio_dev, uintptr_t private,
+				   const struct iio_chan_spec *chan,
+				   char *buf)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	u64 roc64;
+	u32 rate;
+
+	guard(mutex)(&st->lock);
+
+	switch (chan->channel) {
+	case AD9910_CHANNEL_DRG_RAMP_UP:
+		roc64 = FIELD_GET(AD9910_DRG_STEP_INC_MSK,
+				  st->reg[AD9910_REG_DRG_STEP].val64);
+		rate = FIELD_GET(AD9910_DRG_RATE_INC_MSK,
+				 st->reg[AD9910_REG_DRG_RATE].val32);
+		break;
+	case AD9910_CHANNEL_DRG_RAMP_DOWN:
+		roc64 = FIELD_GET(AD9910_DRG_STEP_DEC_MSK,
+				  st->reg[AD9910_REG_DRG_STEP].val64);
+		rate = FIELD_GET(AD9910_DRG_RATE_DEC_MSK,
+				 st->reg[AD9910_REG_DRG_RATE].val32);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (!rate)
+		return -ERANGE;
+
+	roc64 *= st->data.sysclk_freq_hz;
+	return sysfs_emit(buf, "%llu\n", div_u64(roc64, 4 * rate));
+}
+
+static ssize_t ad9910_drg_roc_write(struct iio_dev *indio_dev,
+				    uintptr_t private,
+				    const struct iio_chan_spec *chan,
+				    const char *buf, size_t len)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	u64 tmp64;
+	u32 rate;
+	int ret;
+
+	ret = kstrtou64(buf, 10, &tmp64);
+	if (ret)
+		return ret;
+
+	guard(mutex)(&st->lock);
+
+	switch (chan->channel) {
+	case AD9910_CHANNEL_DRG_RAMP_UP:
+		rate = FIELD_GET(AD9910_DRG_RATE_INC_MSK,
+				 st->reg[AD9910_REG_DRG_RATE].val32);
+		if (!rate)
+			return -ERANGE;
+
+		tmp64 = ad9910_rational_scale(tmp64, 4 * rate, st->data.sysclk_freq_hz);
+		tmp64 = min_t(u64, tmp64, U32_MAX);
+
+		ret = ad9910_reg64_update(st, AD9910_REG_DRG_STEP,
+					  AD9910_DRG_STEP_INC_MSK,
+					  FIELD_PREP(AD9910_DRG_STEP_INC_MSK, tmp64),
+					  true);
+		if (ret)
+			return ret;
+		break;
+	case AD9910_CHANNEL_DRG_RAMP_DOWN:
+		rate = FIELD_GET(AD9910_DRG_RATE_DEC_MSK,
+				 st->reg[AD9910_REG_DRG_RATE].val32);
+		if (!rate)
+			return -ERANGE;
+
+		tmp64 = ad9910_rational_scale(tmp64, 4 * rate, st->data.sysclk_freq_hz);
+		tmp64 = min_t(u64, tmp64, U32_MAX);
+
+		ret = ad9910_reg64_update(st, AD9910_REG_DRG_STEP,
+					  AD9910_DRG_STEP_DEC_MSK,
+					  FIELD_PREP(AD9910_DRG_STEP_DEC_MSK, tmp64),
+					  true);
+		if (ret)
+			return ret;
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -549,6 +692,24 @@ static const struct iio_chan_spec_ext_info ad9910_phy_ext_info[] = {
 	{ }
 };
 
+static const struct iio_chan_spec_ext_info ad9910_drg_ramp_ext_info[] = {
+	{
+		.name = "dwell_en",
+		.read = ad9910_ext_info_read,
+		.write = ad9910_ext_info_write,
+		.private = AD9910_DWELL_EN,
+		.shared = IIO_SEPARATE,
+	},
+	{
+		.name = "raw_roc",
+		.read = ad9910_drg_roc_read,
+		.write = ad9910_drg_roc_write,
+		.private = AD9910_ROC,
+		.shared = IIO_SEPARATE,
+	},
+	{ }
+};
+
 #define AD9910_PROFILE_CHAN(idx) {				\
 	.type = IIO_ALTCURRENT,					\
 	.indexed = 1,						\
@@ -628,6 +789,58 @@ static const struct iio_chan_spec ad9910_channels[] = {
 				      BIT(IIO_CHAN_INFO_SCALE),
 		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
 	},
+	[AD9910_CHAN_IDX_DRG_FREQ] = {
+		.type = IIO_FREQUENCY,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_DRG,
+		.address = AD9910_CHAN_IDX_DRG_FREQ,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE) |
+				      BIT(IIO_CHAN_INFO_SCALE),
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
+	[AD9910_CHAN_IDX_DRG_PHASE] = {
+		.type = IIO_PHASE,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_DRG,
+		.address = AD9910_CHAN_IDX_DRG_PHASE,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE) |
+				      BIT(IIO_CHAN_INFO_SCALE),
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
+	[AD9910_CHAN_IDX_DRG_AMP] = {
+		.type = IIO_ALTCURRENT,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_DRG,
+		.address = AD9910_CHAN_IDX_DRG_AMP,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE) |
+				      BIT(IIO_CHAN_INFO_SCALE),
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
+	[AD9910_CHAN_IDX_DRG_AMP_RAMP_UP] = {
+		.type = IIO_ALTCURRENT,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_DRG_RAMP_UP,
+		.address = AD9910_CHAN_IDX_DRG_AMP_RAMP_UP,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+				      BIT(IIO_CHAN_INFO_SAMP_FREQ),
+		.ext_info = ad9910_drg_ramp_ext_info,
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG_AMP],
+	},
+	[AD9910_CHAN_IDX_DRG_AMP_RAMP_DOWN] = {
+		.type = IIO_ALTCURRENT,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_DRG_RAMP_DOWN,
+		.address = AD9910_CHAN_IDX_DRG_AMP_RAMP_DOWN,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+				      BIT(IIO_CHAN_INFO_SAMP_FREQ),
+		.ext_info = ad9910_drg_ramp_ext_info,
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG_AMP],
+	},
 };
 
 static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -651,6 +864,15 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 				*val = (tmp32 == st->profile);
 			}
 			break;
+		case AD9910_CHANNEL_DRG:
+			tmp32 = FIELD_GET(AD9910_CFR2_DRG_DEST_MSK,
+					  st->reg[AD9910_REG_CFR2].val32);
+			if (tmp32 == (chan->address - AD9910_CHAN_IDX_DRG_FREQ))
+				*val = FIELD_GET(AD9910_CFR2_DRG_ENABLE_MSK,
+						 st->reg[AD9910_REG_CFR2].val32);
+			else
+				*val = 0;
+			break;
 		default:
 			return -EINVAL;
 		}
@@ -690,6 +912,16 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 			*val = FIELD_GET(AD9910_PROFILE_ST_ASF_MSK,
 					 st->reg[AD9910_REG_PROFILE(tmp32)].val64);
 			return IIO_VAL_INT;
+		case AD9910_CHANNEL_DRG_RAMP_UP:
+			tmp64 = FIELD_GET(AD9910_DRG_LIMIT_UPPER_MSK,
+					  st->reg[AD9910_REG_DRG_LIMIT].val64);
+			iio_val_s64_decompose(tmp64, val, val2);
+			return IIO_VAL_INT_64;
+		case AD9910_CHANNEL_DRG_RAMP_DOWN:
+			tmp64 = FIELD_GET(AD9910_DRG_LIMIT_LOWER_MSK,
+					  st->reg[AD9910_REG_DRG_LIMIT].val64);
+			iio_val_s64_decompose(tmp64, val, val2);
+			return IIO_VAL_INT_64;
 		default:
 			return -EINVAL;
 		}
@@ -698,9 +930,23 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 		case AD9910_CHANNEL_PHY:
 			*val = st->data.sysclk_freq_hz;
 			return IIO_VAL_INT;
+		case AD9910_CHANNEL_DRG_RAMP_UP:
+			tmp32 = FIELD_GET(AD9910_DRG_RATE_INC_MSK,
+					  st->reg[AD9910_REG_DRG_RATE].val32);
+			break;
+		case AD9910_CHANNEL_DRG_RAMP_DOWN:
+			tmp32 = FIELD_GET(AD9910_DRG_RATE_DEC_MSK,
+					  st->reg[AD9910_REG_DRG_RATE].val32);
+			break;
 		default:
 			return -EINVAL;
 		}
+		if (!tmp32)
+			return -ERANGE;
+		tmp32 *= 4;
+		*val = st->data.sysclk_freq_hz / tmp32;
+		*val2 = div_u64((u64)(st->data.sysclk_freq_hz % tmp32) * MICRO, tmp32);
+		return IIO_VAL_INT_PLUS_MICRO;
 	case IIO_CHAN_INFO_SCALE:
 		switch (chan->address) {
 		case AD9910_CHAN_IDX_PHY:
@@ -730,6 +976,21 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 			*val = 0;
 			*val2 = AD9910_PI_NANORAD >> 7;
 			return IIO_VAL_INT_PLUS_NANO;
+		case AD9910_CHAN_IDX_DRG_FREQ:
+			tmp64 = ad9910_rational_scale(st->data.sysclk_freq_hz,
+						      PICO, BIT_ULL(32));
+			iio_val_s64_decompose(tmp64, val, val2);
+			return IIO_VAL_DECIMAL64_PICO;
+		case AD9910_CHAN_IDX_DRG_PHASE:
+			tmp64 = DIV_U64_ROUND_CLOSEST(AD9910_PI_PICORAD, BIT(31));
+			iio_val_s64_decompose(tmp64, val, val2);
+			return IIO_VAL_DECIMAL64_PICO;
+		case AD9910_CHAN_IDX_DRG_AMP:
+			tmp64 = (u64)st->data.output_current_uA *
+				AD9910_PICO_MILLIAMP_PER_MICROAMP;
+			tmp64 = DIV_U64_ROUND_CLOSEST(tmp64 >> 1, BIT(31));
+			iio_val_s64_decompose(tmp64, val, val2);
+			return IIO_VAL_DECIMAL64_PICO;
 		default:
 			return -EINVAL;
 		}
@@ -789,6 +1050,22 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 				return ret;
 
 			return ad9910_profile_set(st, tmp32);
+		case AD9910_CHANNEL_DRG:
+			tmp32 = chan->address - AD9910_CHAN_IDX_DRG_FREQ;
+			if (val) {
+				tmp32 = AD9910_CFR2_DRG_ENABLE_MSK |
+					FIELD_PREP(AD9910_CFR2_DRG_DEST_MSK, tmp32);
+			} else {
+				if (tmp32 != FIELD_GET(AD9910_CFR2_DRG_DEST_MSK,
+						       st->reg[AD9910_REG_CFR2].val32))
+					return 0; /* nothing to do */
+				tmp32 = 0;
+			}
+
+			return ad9910_reg32_update(st, AD9910_REG_CFR2,
+						   AD9910_CFR2_DRG_DEST_MSK |
+						   AD9910_CFR2_DRG_ENABLE_MSK,
+						   tmp32, true);
 		default:
 			return -EINVAL;
 		}
@@ -831,22 +1108,62 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 			return -EINVAL;
 		}
 	case IIO_CHAN_INFO_RAW:
-		if (val < 0)
-			return -EINVAL;
-
 		switch (chan->channel) {
 		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+			if (val < 0)
+				return -EINVAL;
+
 			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
 			tmp64 = FIELD_PREP(AD9910_PROFILE_ST_ASF_MSK,
 					   min_t(u64, val, AD9910_ASF_MAX));
 			return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
 						   AD9910_PROFILE_ST_ASF_MSK,
 						   tmp64, true);
+		case AD9910_CHANNEL_DRG_RAMP_UP:
+			tmp64 = iio_val_s64_compose(val, val2);
+			tmp64 = min_t(u64, tmp64, U32_MAX);
+			tmp64 = FIELD_PREP(AD9910_DRG_LIMIT_UPPER_MSK, tmp64);
+			return ad9910_reg64_update(st, AD9910_REG_DRG_LIMIT,
+						   AD9910_DRG_LIMIT_UPPER_MSK,
+						   tmp64, true);
+		case AD9910_CHANNEL_DRG_RAMP_DOWN:
+			tmp64 = iio_val_s64_compose(val, val2);
+			tmp64 = min_t(u64, tmp64, U32_MAX);
+			tmp64 = FIELD_PREP(AD9910_DRG_LIMIT_LOWER_MSK, tmp64);
+			return ad9910_reg64_update(st, AD9910_REG_DRG_LIMIT,
+						   AD9910_DRG_LIMIT_LOWER_MSK,
+						   tmp64, true);
 		default:
 			return -EINVAL;
 		}
 	case IIO_CHAN_INFO_SAMP_FREQ:
-		return ad9910_set_sysclk_freq(st, val, true);
+		if (chan->channel == AD9910_CHANNEL_PHY)
+			return ad9910_set_sysclk_freq(st, val, true);
+
+		if (val < 0 || val2 < 0 || val > st->data.sysclk_freq_hz / 4)
+			return -EINVAL;
+
+		tmp64 = ((u64)val * MICRO + val2) * 4;
+		if (!tmp64)
+			return -EINVAL;
+
+		tmp64 = DIV64_U64_ROUND_CLOSEST((u64)st->data.sysclk_freq_hz * MICRO, tmp64);
+		tmp32 = clamp(tmp64, 1U, AD9910_STEP_RATE_MAX);
+
+		switch (chan->channel) {
+		case AD9910_CHANNEL_DRG_RAMP_UP:
+			tmp32 = FIELD_PREP(AD9910_DRG_RATE_INC_MSK, tmp32);
+			return ad9910_reg32_update(st, AD9910_REG_DRG_RATE,
+						   AD9910_DRG_RATE_INC_MSK,
+						   tmp32, true);
+		case AD9910_CHANNEL_DRG_RAMP_DOWN:
+			tmp32 = FIELD_PREP(AD9910_DRG_RATE_DEC_MSK, tmp32);
+			return ad9910_reg32_update(st, AD9910_REG_DRG_RATE,
+						   AD9910_DRG_RATE_DEC_MSK,
+						   tmp32, true);
+		default:
+			return -EINVAL;
+		}
 	case IIO_CHAN_INFO_SCALE:
 		switch (chan->address) {
 		case AD9910_CHAN_IDX_PHY:
@@ -924,11 +1241,16 @@ static int ad9910_write_raw_get_fmt(struct iio_dev *indio_dev,
 		switch (chan->channel) {
 		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
 			return IIO_VAL_INT;
+		case AD9910_CHANNEL_DRG_RAMP_UP:
+		case AD9910_CHANNEL_DRG_RAMP_DOWN:
+			return IIO_VAL_INT_64;
 		default:
 			return -EINVAL;
 		}
 	case IIO_CHAN_INFO_SAMP_FREQ:
-		return IIO_VAL_INT;
+		if (chan->channel == AD9910_CHANNEL_PHY)
+			return IIO_VAL_INT;
+		return IIO_VAL_INT_PLUS_MICRO;
 	case IIO_CHAN_INFO_SCALE:
 		return IIO_VAL_INT_PLUS_NANO;
 	case IIO_CHAN_INFO_OFFSET:
@@ -999,6 +1321,11 @@ static const char * const ad9910_channel_str[] = {
 	[AD9910_CHAN_IDX_PARALLEL_FREQ] = "parallel_frequency",
 	[AD9910_CHAN_IDX_PARALLEL_POLAR_AMP] = "parallel_polar_amplitude",
 	[AD9910_CHAN_IDX_PARALLEL_POLAR_PHASE] = "parallel_polar_phase",
+	[AD9910_CHAN_IDX_DRG_FREQ] = "drg_frequency",
+	[AD9910_CHAN_IDX_DRG_PHASE] = "drg_phase",
+	[AD9910_CHAN_IDX_DRG_AMP] = "drg_amplitude",
+	[AD9910_CHAN_IDX_DRG_AMP_RAMP_UP] = "drg_rising",
+	[AD9910_CHAN_IDX_DRG_AMP_RAMP_DOWN] = "drg_falling",
 };
 
 static int ad9910_read_label(struct iio_dev *indio_dev,
@@ -1133,6 +1460,14 @@ static int ad9910_setup(struct device *dev, struct ad9910_state *st,
 	if (ret)
 		return ret;
 
+	/* configure step rate with default values */
+	ret = ad9910_reg32_write(st, AD9910_REG_DRG_RATE,
+				 FIELD_PREP(AD9910_DRG_RATE_DEC_MSK, 1) |
+				 FIELD_PREP(AD9910_DRG_RATE_INC_MSK, 1),
+				 false);
+	if (ret)
+		return ret;
+
 	return ad9910_io_update(st);
 }
 

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 12/16] iio: frequency: ad9910: add RAM mode support
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (10 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 11/16] iio: frequency: ad9910: add digital ramp generator support Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:43   ` sashiko-bot
  2026-06-18 13:27 ` [PATCH v6 13/16] iio: frequency: ad9910: add output shift keying support Rodrigo Alencar via B4 Relay
                   ` (3 subsequent siblings)
  15 siblings, 1 reply; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add RAM control channel, which includes:
- RAM data loading via firmware upload interface;
- Per-profile configuration and DDS core parameter destination as firmware
  metadata;
- Profile switching relying on profile channels;
- Sampling frequency control of the active profile;
- ram-enable-aware read/write paths that redirect single tone
  frequency/phase/amplitude access through reg_profile cache when RAM is
  active;

When RAM is enabled, the DDS profile parameters (frequency, phase,
amplitude) for the single tone mode are sourced from a shadow register
cache (reg_profile[]) since the profile registers are repurposed for RAM
control.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 drivers/iio/frequency/Kconfig  |   3 +
 drivers/iio/frequency/ad9910.c | 349 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 344 insertions(+), 8 deletions(-)

diff --git a/drivers/iio/frequency/Kconfig b/drivers/iio/frequency/Kconfig
index 6033f9155b4f..994d199080eb 100644
--- a/drivers/iio/frequency/Kconfig
+++ b/drivers/iio/frequency/Kconfig
@@ -29,6 +29,9 @@ config AD9910
 	tristate "Analog Devices AD9910 Direct Digital Synthesizer"
 	depends on SPI
 	depends on GPIOLIB
+	select CRC32
+	select FW_LOADER
+	select FW_UPLOAD
 	help
 	  Say yes here to build support for Analog Devices AD9910
 	  1 GSPS, 14-Bit DDS with integrated DAC.
diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index 3fe97aa887c3..c4e179dda715 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -8,9 +8,12 @@
 #include <linux/array_size.h>
 #include <linux/bitfield.h>
 #include <linux/clk.h>
+#include <linux/crc32.h>
+#include <linux/debugfs.h>
 #include <linux/delay.h>
 #include <linux/device/devres.h>
 #include <linux/err.h>
+#include <linux/firmware.h>
 #include <linux/gpio/consumer.h>
 #include <linux/log2.h>
 #include <linux/math64.h>
@@ -147,6 +150,15 @@
 #define AD9910_PROFILE_ST_POW_MSK		GENMASK_ULL(47, 32)
 #define AD9910_PROFILE_ST_FTW_MSK		GENMASK_ULL(31, 0)
 
+/* Profile Register Format (RAM Mode) */
+#define AD9910_PROFILE_RAM_OPEN_MSK		GENMASK_ULL(61, 56)
+#define AD9910_PROFILE_RAM_STEP_RATE_MSK	GENMASK_ULL(55, 40)
+#define AD9910_PROFILE_RAM_END_ADDR_MSK		GENMASK_ULL(39, 30)
+#define AD9910_PROFILE_RAM_START_ADDR_MSK	GENMASK_ULL(23, 14)
+#define AD9910_PROFILE_RAM_NO_DWELL_HIGH_MSK	BIT_ULL(5)
+#define AD9910_PROFILE_RAM_ZERO_CROSSING_MSK	BIT_ULL(3)
+#define AD9910_PROFILE_RAM_MODE_CONTROL_MSK	GENMASK_ULL(2, 0)
+
 /* Device constants */
 #define AD9910_PI_NANORAD		3141592653UL
 #define AD9910_PI_PICORAD		3141592653590ULL
@@ -161,6 +173,16 @@
 #define AD9910_STEP_RATE_MAX		FIELD_MAX(AD9910_DRG_RATE_DEC_MSK)
 #define AD9910_NUM_PROFILES		8
 
+#define AD9910_RAM_FW_MAGIC		0x00AD9910
+#define AD9910_RAM_FW_V1		0x0001
+#define AD9910_RAM_SIZE_MAX_WORDS	1024
+#define AD9910_RAM_WORD_SIZE		sizeof(u32)
+#define AD9910_RAM_SIZE_MAX_BYTES	(AD9910_RAM_SIZE_MAX_WORDS * AD9910_RAM_WORD_SIZE)
+#define AD9910_RAM_ADDR_MAX		(AD9910_RAM_SIZE_MAX_WORDS - 1)
+
+#define AD9910_RAM_ENABLED(st)		\
+	FIELD_GET(AD9910_CFR1_RAM_ENABLE_MSK, (st)->reg[AD9910_REG_CFR1].val32)
+
 /* PLL constants */
 #define AD9910_PLL_MIN_N		12
 #define AD9910_PLL_MAX_N		127
@@ -197,7 +219,7 @@
 #define AD9910_RESET_DELAY_us		1	/* 5 sysclk cycles: < 1us */
 
 #define AD9910_SPI_DATA_IDX		1
-#define AD9910_SPI_DATA_LEN_MAX		sizeof(__be64)
+#define AD9910_SPI_DATA_LEN_MAX		AD9910_RAM_SIZE_MAX_BYTES
 #define AD9910_SPI_MESSAGE_LEN_MAX	(AD9910_SPI_DATA_IDX + AD9910_SPI_DATA_LEN_MAX)
 #define AD9910_SPI_READ_MSK		BIT(7)
 #define AD9910_SPI_ADDR_MSK		GENMASK(4, 0)
@@ -219,6 +241,7 @@
  * @AD9910_CHANNEL_DRG: Digital Ramp Generator output channel
  * @AD9910_CHANNEL_DRG_RAMP_UP: DRG ramp up channel
  * @AD9910_CHANNEL_DRG_RAMP_DOWN: DRG ramp down channel
+ * @AD9910_CHANNEL_RAM: RAM control output channel
  */
 enum ad9910_channel {
 	AD9910_CHANNEL_PHY = 100,
@@ -235,8 +258,32 @@ enum ad9910_channel {
 	AD9910_CHANNEL_DRG = 130,
 	AD9910_CHANNEL_DRG_RAMP_UP = 131,
 	AD9910_CHANNEL_DRG_RAMP_DOWN = 132,
+	AD9910_CHANNEL_RAM = 140,
 };
 
+/**
+ * struct ad9910_ram_fw - AD9910 RAM firmware format
+ * @magic:	Magic number for RAM firmware validation
+ * @version:	Firmware version number
+ * @wcount:	Number of RAM words to be written
+ * @crc:	CRC32 checksum of the RAM data for integrity verification
+ * @cfr1:	Value of CFR1 register to be configured (not all fields are
+ *		used, but this is included here for convenience)
+ * @profiles:	Array of RAM profile configurations
+ * @words:	Array of RAM words to be written. Data pattern should be set in
+ *		reverse order and wcount specifies the number of words in this
+ *		array
+ */
+struct ad9910_ram_fw {
+	__be32 magic;
+	__be16 version;
+	__be16 wcount;
+	__be32 crc;
+	__be32 cfr1;
+	__be64 profiles[AD9910_NUM_PROFILES];
+	__be32 words[] __counted_by_be(wcount);
+} __packed;
+
 enum {
 	AD9910_CHAN_IDX_PHY,
 	AD9910_CHAN_IDX_PROFILE_0,
@@ -257,6 +304,7 @@ enum {
 	AD9910_CHAN_IDX_DRG_AMP,
 	AD9910_CHAN_IDX_DRG_AMP_RAMP_UP,
 	AD9910_CHAN_IDX_DRG_AMP_RAMP_DOWN,
+	AD9910_CHAN_IDX_RAM,
 };
 
 enum {
@@ -283,6 +331,7 @@ union ad9910_reg {
 struct ad9910_state {
 	struct spi_device *spi;
 	struct clk *refclk;
+	struct fw_upload *ram_fwu;
 
 	struct gpio_desc *gpio_pwdown;
 	struct gpio_desc *gpio_update;
@@ -291,12 +340,22 @@ struct ad9910_state {
 	/* cached registers */
 	union ad9910_reg reg[AD9910_REG_NUM_CACHED];
 
+	/*
+	 * alternate profile registers used to store RAM profile settings when
+	 * RAM mode is disabled and Single Tone profile settings when RAM mode
+	 * is enabled.
+	 */
+	u64 reg_profile[AD9910_NUM_PROFILES];
+
 	/* Lock for accessing device registers and state variables */
 	struct mutex lock;
 
 	struct ad9910_data data;
 	u8 profile;
 
+	bool ram_fwu_cancel;
+	char ram_fwu_name[20];
+
 	/*
 	 * RAM loading requires a reasonable amount of bytes, at the same time
 	 * DMA capable SPI drivers requires the transfer buffers to live in
@@ -320,6 +379,22 @@ static inline u64 ad9910_rational_scale(u64 input, u64 scale, u64 reference)
 	return mul_u64_add_u64_div_u64(input, scale, reference >> 1, reference);
 }
 
+static inline u64 ad9910_ram_profile_val(struct ad9910_state *st)
+{
+	if (AD9910_RAM_ENABLED(st))
+		return st->reg[AD9910_REG_PROFILE(st->profile)].val64;
+	else
+		return st->reg_profile[st->profile];
+}
+
+static inline u64 ad9910_st_profile_val(struct ad9910_state *st, u8 profile)
+{
+	if (AD9910_RAM_ENABLED(st))
+		return st->reg_profile[profile];
+	else
+		return st->reg[AD9910_REG_PROFILE(profile)].val64;
+}
+
 static int ad9910_io_update(struct ad9910_state *st)
 {
 	if (st->gpio_update) {
@@ -841,6 +916,18 @@ static const struct iio_chan_spec ad9910_channels[] = {
 		.ext_info = ad9910_drg_ramp_ext_info,
 		.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG_AMP],
 	},
+	[AD9910_CHAN_IDX_RAM] = {
+		.type = IIO_ALTCURRENT,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_RAM,
+		.address = AD9910_CHAN_IDX_RAM,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE) |
+				      BIT(IIO_CHAN_INFO_FREQUENCY) |
+				      BIT(IIO_CHAN_INFO_PHASE) |
+				      BIT(IIO_CHAN_INFO_SAMP_FREQ),
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
 };
 
 static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -873,6 +960,10 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 			else
 				*val = 0;
 			break;
+		case AD9910_CHANNEL_RAM:
+			*val = FIELD_GET(AD9910_CFR1_RAM_ENABLE_MSK,
+					 st->reg[AD9910_REG_CFR1].val32);
+			break;
 		default:
 			return -EINVAL;
 		}
@@ -882,7 +973,10 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
 			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
 			tmp64 = FIELD_GET(AD9910_PROFILE_ST_FTW_MSK,
-					  st->reg[AD9910_REG_PROFILE(tmp32)].val64);
+					  ad9910_st_profile_val(st, tmp32));
+			break;
+		case AD9910_CHANNEL_RAM:
+			tmp64 = st->reg[AD9910_REG_FTW].val32;
 			break;
 		default:
 			return -EINVAL;
@@ -896,7 +990,10 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
 			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
 			tmp64 = FIELD_GET(AD9910_PROFILE_ST_POW_MSK,
-					  st->reg[AD9910_REG_PROFILE(tmp32)].val64);
+					  ad9910_st_profile_val(st, tmp32));
+			break;
+		case AD9910_CHANNEL_RAM:
+			tmp64 = st->reg[AD9910_REG_POW].val16;
 			break;
 		default:
 			return -EINVAL;
@@ -910,7 +1007,7 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
 			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
 			*val = FIELD_GET(AD9910_PROFILE_ST_ASF_MSK,
-					 st->reg[AD9910_REG_PROFILE(tmp32)].val64);
+					 ad9910_st_profile_val(st, tmp32));
 			return IIO_VAL_INT;
 		case AD9910_CHANNEL_DRG_RAMP_UP:
 			tmp64 = FIELD_GET(AD9910_DRG_LIMIT_UPPER_MSK,
@@ -938,6 +1035,10 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 			tmp32 = FIELD_GET(AD9910_DRG_RATE_DEC_MSK,
 					  st->reg[AD9910_REG_DRG_RATE].val32);
 			break;
+		case AD9910_CHANNEL_RAM:
+			tmp32 = FIELD_GET(AD9910_PROFILE_RAM_STEP_RATE_MSK,
+					  ad9910_ram_profile_val(st));
+			break;
 		default:
 			return -EINVAL;
 		}
@@ -1029,7 +1130,7 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 	struct ad9910_state *st = iio_priv(indio_dev);
 	u64 tmp64;
 	u32 tmp32;
-	int ret;
+	int ret, i;
 
 	guard(mutex)(&st->lock);
 
@@ -1066,6 +1167,41 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 						   AD9910_CFR2_DRG_DEST_MSK |
 						   AD9910_CFR2_DRG_ENABLE_MSK,
 						   tmp32, true);
+		case AD9910_CHANNEL_RAM:
+			if (AD9910_RAM_ENABLED(st) == !!val)
+				return 0;
+
+			/* swap profile configs */
+			for (i = 0; i < AD9910_NUM_PROFILES; i++) {
+				tmp64 = st->reg[AD9910_REG_PROFILE(i)].val64;
+				ret = ad9910_reg64_write(st,
+							 AD9910_REG_PROFILE(i),
+							 st->reg_profile[i],
+							 false);
+				if (ret)
+					break;
+				st->reg_profile[i] = tmp64;
+			}
+
+			if (ret) {
+				/*
+				 * After the write failure, profiles 0..i-1 were
+				 * already swapped in SW, but Hw registers are
+				 * still pending an IO update, so swap them back
+				 * in SW to keep the state consistent.
+				 */
+				while (i--) {
+					tmp64 = st->reg[AD9910_REG_PROFILE(i)].val64;
+					st->reg[AD9910_REG_PROFILE(i)].val64 = st->reg_profile[i];
+					st->reg_profile[i] = tmp64;
+				}
+				return ret;
+			}
+
+			tmp32 = FIELD_PREP(AD9910_CFR1_RAM_ENABLE_MSK, !!val);
+			return ad9910_reg32_update(st, AD9910_REG_CFR1,
+						   AD9910_CFR1_RAM_ENABLE_MSK,
+						   tmp32, true);
 		default:
 			return -EINVAL;
 		}
@@ -1079,10 +1215,17 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 		switch (chan->channel) {
 		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
 			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+			if (AD9910_RAM_ENABLED(st)) {
+				FIELD_MODIFY(AD9910_PROFILE_ST_FTW_MSK,
+					     &st->reg_profile[tmp32], tmp64);
+				return 0;
+			}
 			tmp64 = FIELD_PREP(AD9910_PROFILE_ST_FTW_MSK, tmp64);
 			return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
 						   AD9910_PROFILE_ST_FTW_MSK,
 						   tmp64, true);
+		case AD9910_CHANNEL_RAM:
+			return ad9910_reg32_write(st, AD9910_REG_FTW, tmp64, true);
 		default:
 			return -EINVAL;
 		}
@@ -1100,10 +1243,19 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 		switch (chan->channel) {
 		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
 			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+
+			if (AD9910_RAM_ENABLED(st)) {
+				FIELD_MODIFY(AD9910_PROFILE_ST_POW_MSK,
+					     &st->reg_profile[tmp32], tmp64);
+				return 0;
+			}
+
 			tmp64 = FIELD_PREP(AD9910_PROFILE_ST_POW_MSK, tmp64);
 			return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
 						   AD9910_PROFILE_ST_POW_MSK,
 						   tmp64, true);
+		case AD9910_CHANNEL_RAM:
+			return ad9910_reg16_write(st, AD9910_REG_POW, tmp64, true);
 		default:
 			return -EINVAL;
 		}
@@ -1114,8 +1266,15 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 				return -EINVAL;
 
 			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
-			tmp64 = FIELD_PREP(AD9910_PROFILE_ST_ASF_MSK,
-					   min_t(u64, val, AD9910_ASF_MAX));
+			tmp64 = min_t(u64, val, AD9910_ASF_MAX);
+
+			if (AD9910_RAM_ENABLED(st)) {
+				FIELD_MODIFY(AD9910_PROFILE_ST_ASF_MSK,
+					     &st->reg_profile[tmp32], tmp64);
+				return 0;
+			}
+
+			tmp64 = FIELD_PREP(AD9910_PROFILE_ST_ASF_MSK, tmp64);
 			return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
 						   AD9910_PROFILE_ST_ASF_MSK,
 						   tmp64, true);
@@ -1161,6 +1320,17 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 			return ad9910_reg32_update(st, AD9910_REG_DRG_RATE,
 						   AD9910_DRG_RATE_DEC_MSK,
 						   tmp32, true);
+		case AD9910_CHANNEL_RAM:
+			if (!AD9910_RAM_ENABLED(st)) {
+				FIELD_MODIFY(AD9910_PROFILE_RAM_STEP_RATE_MSK,
+					     &st->reg_profile[st->profile], tmp32);
+				return 0;
+			}
+
+			tmp64 = FIELD_PREP(AD9910_PROFILE_RAM_STEP_RATE_MSK, tmp32);
+			return ad9910_reg64_update(st, AD9910_REG_PROFILE(st->profile),
+						   AD9910_PROFILE_RAM_STEP_RATE_MSK,
+						   tmp64, true);
 		default:
 			return -EINVAL;
 		}
@@ -1326,6 +1496,7 @@ static const char * const ad9910_channel_str[] = {
 	[AD9910_CHAN_IDX_DRG_AMP] = "drg_amplitude",
 	[AD9910_CHAN_IDX_DRG_AMP_RAMP_UP] = "drg_rising",
 	[AD9910_CHAN_IDX_DRG_AMP_RAMP_DOWN] = "drg_falling",
+	[AD9910_CHAN_IDX_RAM] = "ram",
 };
 
 static int ad9910_read_label(struct iio_dev *indio_dev,
@@ -1335,6 +1506,126 @@ static int ad9910_read_label(struct iio_dev *indio_dev,
 	return sysfs_emit(label, "%s\n", ad9910_channel_str[chan->address]);
 }
 
+static enum fw_upload_err ad9910_ram_fwu_prepare(struct fw_upload *fw_upload,
+						 const u8 *data, u32 size)
+{
+	struct ad9910_state *st = fw_upload->dd_handle;
+	const struct ad9910_ram_fw *fw_data = (const struct ad9910_ram_fw *)data;
+	size_t wcount, bcount;
+
+	if (size < sizeof(struct ad9910_ram_fw))
+		return FW_UPLOAD_ERR_INVALID_SIZE;
+
+	if (get_unaligned_be32(&fw_data->magic) != AD9910_RAM_FW_MAGIC)
+		return FW_UPLOAD_ERR_FW_INVALID;
+
+	if (get_unaligned_be16(&fw_data->version) != AD9910_RAM_FW_V1)
+		return FW_UPLOAD_ERR_FW_INVALID;
+
+	wcount = get_unaligned_be16(&fw_data->wcount);
+	bcount = size - sizeof(struct ad9910_ram_fw);
+	if (wcount > AD9910_RAM_SIZE_MAX_WORDS ||
+	    bcount != (wcount * AD9910_RAM_WORD_SIZE))
+		return FW_UPLOAD_ERR_INVALID_SIZE;
+
+	bcount += sizeof(fw_data->cfr1) + sizeof(fw_data->profiles);
+	if (crc32(0, &fw_data->cfr1, bcount) != get_unaligned_be32(&fw_data->crc))
+		return FW_UPLOAD_ERR_FW_INVALID;
+
+	guard(mutex)(&st->lock);
+	st->ram_fwu_cancel = false;
+
+	return FW_UPLOAD_ERR_NONE;
+}
+
+static enum fw_upload_err ad9910_ram_fwu_write(struct fw_upload *fw_upload,
+					       const u8 *data, u32 offset,
+					       u32 size, u32 *written)
+{
+	const struct ad9910_ram_fw *fw_data = (const struct ad9910_ram_fw *)data;
+	struct ad9910_state *st = fw_upload->dd_handle;
+	int ret, ret2, idx, wcount;
+	u64 tmp64, backup;
+
+	if (offset != 0)
+		return FW_UPLOAD_ERR_INVALID_SIZE;
+
+	guard(mutex)(&st->lock);
+
+	if (st->ram_fwu_cancel)
+		return FW_UPLOAD_ERR_CANCELED;
+
+	if (AD9910_RAM_ENABLED(st))
+		return FW_UPLOAD_ERR_HW_ERROR;
+
+	for (idx = 0; idx < AD9910_NUM_PROFILES; idx++)
+		st->reg_profile[idx] = get_unaligned_be64(&fw_data->profiles[idx]) |
+				       AD9910_PROFILE_RAM_OPEN_MSK;
+
+	ret = ad9910_reg32_update(st, AD9910_REG_CFR1,
+				  AD9910_CFR1_RAM_PLAYBACK_DEST_MSK |
+				  AD9910_CFR1_INT_PROFILE_CTL_MSK,
+				  get_unaligned_be32(&fw_data->cfr1), true);
+	if (ret)
+		return FW_UPLOAD_ERR_RW_ERROR;
+
+	wcount = get_unaligned_be16(&fw_data->wcount);
+	if (!wcount) {
+		*written = size;
+		return FW_UPLOAD_ERR_NONE; /* nothing else to write */
+	}
+
+	ret = ad9910_profile_set(st, st->profile);
+	if (ret)
+		return FW_UPLOAD_ERR_HW_ERROR;
+
+	/* backup profile register and update it with required address range */
+	backup = st->reg[AD9910_REG_PROFILE(st->profile)].val64;
+	tmp64 = AD9910_PROFILE_RAM_STEP_RATE_MSK |
+		FIELD_PREP(AD9910_PROFILE_RAM_START_ADDR_MSK, 0) |
+		FIELD_PREP(AD9910_PROFILE_RAM_END_ADDR_MSK, wcount - 1);
+	ret = ad9910_reg64_write(st, AD9910_REG_PROFILE(st->profile), tmp64, true);
+	if (ret)
+		return FW_UPLOAD_ERR_RW_ERROR;
+
+	memcpy(&st->tx_buf[1], fw_data->words, wcount * AD9910_RAM_WORD_SIZE);
+
+	/* write ram data and restore profile register */
+	ret = ad9910_spi_write(st, AD9910_REG_RAM,
+			       wcount * AD9910_RAM_WORD_SIZE, false);
+	ret2 = ad9910_reg64_write(st, AD9910_REG_PROFILE(st->profile), backup, true);
+	if (ret || ret2)
+		return FW_UPLOAD_ERR_RW_ERROR;
+
+	*written = size;
+	return FW_UPLOAD_ERR_NONE;
+}
+
+static enum fw_upload_err ad9910_ram_fwu_poll_complete(struct fw_upload *fw_upload)
+{
+	return FW_UPLOAD_ERR_NONE;
+}
+
+static void ad9910_ram_fwu_cancel(struct fw_upload *fw_upload)
+{
+	struct ad9910_state *st = fw_upload->dd_handle;
+
+	guard(mutex)(&st->lock);
+	st->ram_fwu_cancel = true;
+}
+
+static void ad9910_ram_fwu_unregister(void *data)
+{
+	firmware_upload_unregister(data);
+}
+
+static const struct fw_upload_ops ad9910_ram_fwu_ops = {
+	.prepare = ad9910_ram_fwu_prepare,
+	.write = ad9910_ram_fwu_write,
+	.poll_complete = ad9910_ram_fwu_poll_complete,
+	.cancel = ad9910_ram_fwu_cancel
+};
+
 static const struct iio_info ad9910_info = {
 	.read_raw = ad9910_read_raw,
 	.write_raw = ad9910_write_raw,
@@ -1468,9 +1759,33 @@ static int ad9910_setup(struct device *dev, struct ad9910_state *st,
 	if (ret)
 		return ret;
 
+	for (unsigned int i = 0; i < AD9910_NUM_PROFILES; i++) {
+		st->reg_profile[i] = AD9910_PROFILE_RAM_OPEN_MSK;
+		st->reg_profile[i] |= FIELD_PREP(AD9910_PROFILE_RAM_STEP_RATE_MSK, 1);
+		st->reg_profile[i] |= FIELD_PREP(AD9910_PROFILE_RAM_END_ADDR_MSK,
+						 AD9910_RAM_ADDR_MAX);
+	}
+
 	return ad9910_io_update(st);
 }
 
+static inline void ad9910_debugfs_init(struct ad9910_state *st,
+				       struct iio_dev *indio_dev)
+{
+	struct dentry *d = iio_get_debugfs_dentry(indio_dev);
+	char buf[64];
+
+	/*
+	 * symlinks are created here so iio userspace tools can refer to them
+	 * as debug attributes.
+	 */
+	snprintf(buf, sizeof(buf), "/sys/class/firmware/%s/loading", st->ram_fwu_name);
+	debugfs_create_symlink("ram_loading", d, buf);
+
+	snprintf(buf, sizeof(buf), "/sys/class/firmware/%s/data", st->ram_fwu_name);
+	debugfs_create_symlink("ram_data", d, buf);
+}
+
 static int ad9910_probe(struct spi_device *spi)
 {
 	static const char * const supplies[] = {
@@ -1561,7 +1876,25 @@ static int ad9910_probe(struct spi_device *spi)
 	if (ret)
 		return dev_err_probe(dev, ret, "device setup failed\n");
 
-	return devm_iio_device_register(dev, indio_dev);
+	snprintf(st->ram_fwu_name, sizeof(st->ram_fwu_name), "%s:ram",
+		 dev_name(&indio_dev->dev));
+	st->ram_fwu = firmware_upload_register(THIS_MODULE, dev, st->ram_fwu_name,
+					       &ad9910_ram_fwu_ops, st);
+	if (IS_ERR(st->ram_fwu))
+		return dev_err_probe(dev, PTR_ERR(st->ram_fwu),
+				     "failed to register ram upload ops\n");
+
+	ret = devm_add_action_or_reset(dev, ad9910_ram_fwu_unregister, st->ram_fwu);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to add ram upload unregister action\n");
+
+	ret = devm_iio_device_register(dev, indio_dev);
+	if (ret)
+		return ret;
+
+	ad9910_debugfs_init(st, indio_dev);
+	return 0;
 }
 
 static const struct spi_device_id ad9910_id[] = {

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 13/16] iio: frequency: ad9910: add output shift keying support
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (11 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 12/16] iio: frequency: ad9910: add RAM mode support Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:27 ` [PATCH v6 14/16] iio: frequency: ad9910: show channel priority in debugfs Rodrigo Alencar via B4 Relay
                   ` (2 subsequent siblings)
  15 siblings, 0 replies; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add OSK channel with amplitude envelope control capabilities:
- OSK enable/disable via IIO_CHAN_INFO_ENABLE;
- Amplitude ramp rate control via IIO_CHAN_INFO_SAMP_FREQ;
- Amplitude scale factor readback via IIO_CHAN_INFO_RAW (ASF register);
- Automatic OSK step size configurable through the raw_roc extended
  attribute, which allows for selectable step sizes in raw units:
	- 0: no step, means manual mode (NOT pin controlled)
	- 1: I_FS / 2^14 step, automatic mode (pin controlled)
	- 2: 2 I_FS / 2^14 step, automatic mode (pin controlled)
	- 4: 4 I_FS /2^14 step, automatic mode (pin controlled)
	- 8: 8 I_FS /2^14 step, automatic mode (pin controlled)
	- 16383: I_FS step (max), manual mode (pin controlled)

The ASF register is initialized with a default amplitude ramp rate during
device setup to ensure valid readback.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 drivers/iio/frequency/ad9910.c | 191 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 191 insertions(+)

diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index c4e179dda715..890499f67bd5 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -242,6 +242,7 @@
  * @AD9910_CHANNEL_DRG_RAMP_UP: DRG ramp up channel
  * @AD9910_CHANNEL_DRG_RAMP_DOWN: DRG ramp down channel
  * @AD9910_CHANNEL_RAM: RAM control output channel
+ * @AD9910_CHANNEL_OSK: Output Shift Keying output channel
  */
 enum ad9910_channel {
 	AD9910_CHANNEL_PHY = 100,
@@ -259,6 +260,7 @@ enum ad9910_channel {
 	AD9910_CHANNEL_DRG_RAMP_UP = 131,
 	AD9910_CHANNEL_DRG_RAMP_DOWN = 132,
 	AD9910_CHANNEL_RAM = 140,
+	AD9910_CHANNEL_OSK = 150,
 };
 
 /**
@@ -305,12 +307,14 @@ enum {
 	AD9910_CHAN_IDX_DRG_AMP_RAMP_UP,
 	AD9910_CHAN_IDX_DRG_AMP_RAMP_DOWN,
 	AD9910_CHAN_IDX_RAM,
+	AD9910_CHAN_IDX_OSK,
 };
 
 enum {
 	AD9910_POWERDOWN,
 	AD9910_DWELL_EN,
 	AD9910_ROC,
+	AD9910_ROC_AVAIL,
 };
 
 struct ad9910_data {
@@ -756,6 +760,125 @@ static ssize_t ad9910_drg_roc_write(struct iio_dev *indio_dev,
 	return len;
 }
 
+static const u32 ad9910_osk_raw_step[] = {
+	0,		/* no step: manual mode (NOT pin controlled) */
+	1,		/* step size 1: automatic mode (pin controlled) */
+	2,		/* step size 2: automatic mode (pin controlled) */
+	4,		/* step size 4: automatic mode (pin controlled) */
+	8,		/* step size 8: automatic mode (pin controlled) */
+	GENMASK(13, 0),	/* max step: manual mode (pin controlled) */
+};
+
+static ssize_t ad9910_osk_attrs_read(struct iio_dev *indio_dev,
+				     uintptr_t private,
+				     const struct iio_chan_spec *chan,
+				     char *buf)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	bool auto_en, pinctrl_en;
+	u32 rate, step;
+	u64 roc64;
+
+	guard(mutex)(&st->lock);
+
+	rate = FIELD_GET(AD9910_ASF_RAMP_RATE_MSK, st->reg[AD9910_REG_ASF].val32);
+	if (!rate)
+		return -ERANGE;
+
+	switch (private) {
+	case AD9910_ROC:
+		auto_en = FIELD_GET(AD9910_CFR1_SELECT_AUTO_OSK_MSK,
+				    st->reg[AD9910_REG_CFR1].val32);
+		pinctrl_en = FIELD_GET(AD9910_CFR1_OSK_MANUAL_EXT_CTL_MSK,
+				       st->reg[AD9910_REG_CFR1].val32);
+		if (auto_en) {
+			step = FIELD_GET(AD9910_ASF_STEP_SIZE_MSK,
+					 st->reg[AD9910_REG_ASF].val32);
+			step = ad9910_osk_raw_step[step + 1];
+		} else if (pinctrl_en) {
+			step = ad9910_osk_raw_step[ARRAY_SIZE(ad9910_osk_raw_step) - 1];
+		} else {
+			step = ad9910_osk_raw_step[0];
+		}
+
+		roc64 = div_u64((u64)step * st->data.sysclk_freq_hz, 4 * rate);
+		return sysfs_emit(buf, "%llu\n", roc64);
+	case AD9910_ROC_AVAIL: {
+		ssize_t len = 0;
+
+		for (unsigned int i = 0; i < ARRAY_SIZE(ad9910_osk_raw_step); i++) {
+			roc64 = div_u64((u64)ad9910_osk_raw_step[i] * st->data.sysclk_freq_hz,
+					4 * rate);
+			len += sysfs_emit_at(buf, len, "%llu ", roc64);
+		}
+
+		buf[len - 1] = '\n'; /* replace last space with a newline */
+		return len;
+	}
+	default:
+		return -EINVAL;
+	}
+}
+
+static ssize_t ad9910_osk_attrs_write(struct iio_dev *indio_dev,
+				      uintptr_t private,
+				      const struct iio_chan_spec *chan,
+				      const char *buf, size_t len)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	u32 idx, reg_val, rate;
+	u64 step;
+	int ret;
+
+	ret = kstrtou64(buf, 10, &step);
+	if (ret)
+		return ret;
+
+	guard(mutex)(&st->lock);
+
+	rate = FIELD_GET(AD9910_ASF_RAMP_RATE_MSK, st->reg[AD9910_REG_ASF].val32);
+	if (!rate)
+		return -ERANGE;
+
+	switch (private) {
+	case AD9910_ROC:
+		step = ad9910_rational_scale(step, 4 * rate,
+					     st->data.sysclk_freq_hz);
+		step = min(step, AD9910_ASF_MAX);
+		idx = find_closest(step, ad9910_osk_raw_step,
+				   ARRAY_SIZE(ad9910_osk_raw_step));
+		if (idx == ARRAY_SIZE(ad9910_osk_raw_step) - 1) {
+			/* manual mode: pin-controlled */
+			reg_val = AD9910_CFR1_OSK_MANUAL_EXT_CTL_MSK;
+		} else if (idx == 0) {
+			/* manual mode, NOT pin-controlled */
+			reg_val = 0;
+		} else {
+			/* auto mode: pin-controlled */
+			reg_val = FIELD_PREP(AD9910_ASF_STEP_SIZE_MSK, idx - 1);
+			ret = ad9910_reg32_update(st, AD9910_REG_ASF,
+						  AD9910_ASF_STEP_SIZE_MSK,
+						  reg_val, false);
+			if (ret)
+				return ret;
+
+			reg_val = AD9910_CFR1_SELECT_AUTO_OSK_MSK;
+		}
+
+		ret = ad9910_reg32_update(st, AD9910_REG_CFR1,
+					  AD9910_CFR1_SELECT_AUTO_OSK_MSK |
+					  AD9910_CFR1_OSK_MANUAL_EXT_CTL_MSK,
+					  reg_val, true);
+		if (ret)
+			return ret;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return len;
+}
+
 static const struct iio_chan_spec_ext_info ad9910_phy_ext_info[] = {
 	{
 		.name = "powerdown",
@@ -785,6 +908,23 @@ static const struct iio_chan_spec_ext_info ad9910_drg_ramp_ext_info[] = {
 	{ }
 };
 
+static const struct iio_chan_spec_ext_info ad9910_osk_ext_info[] = {
+	{
+		.name = "raw_roc",
+		.read = ad9910_osk_attrs_read,
+		.write = ad9910_osk_attrs_write,
+		.private = AD9910_ROC,
+		.shared = IIO_SEPARATE,
+	},
+	{
+		.name = "raw_roc_available",
+		.read = ad9910_osk_attrs_read,
+		.private = AD9910_ROC_AVAIL,
+		.shared = IIO_SEPARATE,
+	},
+	{ }
+};
+
 #define AD9910_PROFILE_CHAN(idx) {				\
 	.type = IIO_ALTCURRENT,					\
 	.indexed = 1,						\
@@ -928,6 +1068,18 @@ static const struct iio_chan_spec ad9910_channels[] = {
 				      BIT(IIO_CHAN_INFO_SAMP_FREQ),
 		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
 	},
+	[AD9910_CHAN_IDX_OSK] = {
+		.type = IIO_ALTCURRENT,
+		.indexed = 1,
+		.output = 1,
+		.channel = AD9910_CHANNEL_OSK,
+		.address = AD9910_CHAN_IDX_OSK,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE) |
+				      BIT(IIO_CHAN_INFO_RAW) |
+				      BIT(IIO_CHAN_INFO_SAMP_FREQ),
+		.ext_info = ad9910_osk_ext_info,
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
 };
 
 static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -964,6 +1116,10 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 			*val = FIELD_GET(AD9910_CFR1_RAM_ENABLE_MSK,
 					 st->reg[AD9910_REG_CFR1].val32);
 			break;
+		case AD9910_CHANNEL_OSK:
+			*val = FIELD_GET(AD9910_CFR1_OSK_ENABLE_MSK,
+					 st->reg[AD9910_REG_CFR1].val32);
+			break;
 		default:
 			return -EINVAL;
 		}
@@ -1019,6 +1175,10 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 					  st->reg[AD9910_REG_DRG_LIMIT].val64);
 			iio_val_s64_decompose(tmp64, val, val2);
 			return IIO_VAL_INT_64;
+		case AD9910_CHANNEL_OSK:
+			*val = FIELD_GET(AD9910_ASF_SCALE_FACTOR_MSK,
+					 st->reg[AD9910_REG_ASF].val32);
+			return IIO_VAL_INT;
 		default:
 			return -EINVAL;
 		}
@@ -1039,6 +1199,10 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 			tmp32 = FIELD_GET(AD9910_PROFILE_RAM_STEP_RATE_MSK,
 					  ad9910_ram_profile_val(st));
 			break;
+		case AD9910_CHANNEL_OSK:
+			tmp32 = FIELD_GET(AD9910_ASF_RAMP_RATE_MSK,
+					  st->reg[AD9910_REG_ASF].val32);
+			break;
 		default:
 			return -EINVAL;
 		}
@@ -1202,6 +1366,11 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 			return ad9910_reg32_update(st, AD9910_REG_CFR1,
 						   AD9910_CFR1_RAM_ENABLE_MSK,
 						   tmp32, true);
+		case AD9910_CHANNEL_OSK:
+			tmp32 = FIELD_PREP(AD9910_CFR1_OSK_ENABLE_MSK, !!val);
+			return ad9910_reg32_update(st, AD9910_REG_CFR1,
+						   AD9910_CFR1_OSK_ENABLE_MSK,
+						   tmp32, true);
 		default:
 			return -EINVAL;
 		}
@@ -1292,6 +1461,15 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 			return ad9910_reg64_update(st, AD9910_REG_DRG_LIMIT,
 						   AD9910_DRG_LIMIT_LOWER_MSK,
 						   tmp64, true);
+		case AD9910_CHANNEL_OSK:
+			if (val < 0)
+				return -EINVAL;
+
+			tmp32 = min_t(u32, val, AD9910_ASF_MAX);
+			tmp32 = FIELD_PREP(AD9910_ASF_SCALE_FACTOR_MSK, tmp32);
+			return ad9910_reg32_update(st, AD9910_REG_ASF,
+						   AD9910_ASF_SCALE_FACTOR_MSK,
+						   tmp32, true);
 		default:
 			return -EINVAL;
 		}
@@ -1331,6 +1509,11 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
 			return ad9910_reg64_update(st, AD9910_REG_PROFILE(st->profile),
 						   AD9910_PROFILE_RAM_STEP_RATE_MSK,
 						   tmp64, true);
+		case AD9910_CHANNEL_OSK:
+			return ad9910_reg32_update(st, AD9910_REG_ASF,
+						   AD9910_ASF_RAMP_RATE_MSK,
+						   FIELD_PREP(AD9910_ASF_RAMP_RATE_MSK, tmp32),
+						   true);
 		default:
 			return -EINVAL;
 		}
@@ -1410,6 +1593,7 @@ static int ad9910_write_raw_get_fmt(struct iio_dev *indio_dev,
 	case IIO_CHAN_INFO_RAW:
 		switch (chan->channel) {
 		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+		case AD9910_CHANNEL_OSK:
 			return IIO_VAL_INT;
 		case AD9910_CHANNEL_DRG_RAMP_UP:
 		case AD9910_CHANNEL_DRG_RAMP_DOWN:
@@ -1497,6 +1681,7 @@ static const char * const ad9910_channel_str[] = {
 	[AD9910_CHAN_IDX_DRG_AMP_RAMP_UP] = "drg_rising",
 	[AD9910_CHAN_IDX_DRG_AMP_RAMP_DOWN] = "drg_falling",
 	[AD9910_CHAN_IDX_RAM] = "ram",
+	[AD9910_CHAN_IDX_OSK] = "osk",
 };
 
 static int ad9910_read_label(struct iio_dev *indio_dev,
@@ -1752,6 +1937,12 @@ static int ad9910_setup(struct device *dev, struct ad9910_state *st,
 		return ret;
 
 	/* configure step rate with default values */
+	ret = ad9910_reg32_write(st, AD9910_REG_ASF,
+				 FIELD_PREP(AD9910_ASF_RAMP_RATE_MSK, 1),
+				 false);
+	if (ret)
+		return ret;
+
 	ret = ad9910_reg32_write(st, AD9910_REG_DRG_RATE,
 				 FIELD_PREP(AD9910_DRG_RATE_DEC_MSK, 1) |
 				 FIELD_PREP(AD9910_DRG_RATE_INC_MSK, 1),

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 14/16] iio: frequency: ad9910: show channel priority in debugfs
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (12 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 13/16] iio: frequency: ad9910: add output shift keying support Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:45   ` sashiko-bot
  2026-06-18 13:27 ` [PATCH v6 15/16] iio: ABI: add docs for ad9910 sysfs entries Rodrigo Alencar via B4 Relay
  2026-06-18 13:27 ` [PATCH v6 16/16] docs: iio: add documentation for ad9910 driver Rodrigo Alencar via B4 Relay
  15 siblings, 1 reply; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Expose frequency_source, phase_source and amplitude_source attributes in
debugfs. Those indicate from which channel the specific DDS parameter is
being sourced by returning its label. The implementation follows the
priority table found in the datasheet.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 drivers/iio/frequency/ad9910.c | 173 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 173 insertions(+)

diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index 890499f67bd5..b39eeb8d9cd4 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -22,6 +22,7 @@
 #include <linux/property.h>
 #include <linux/regulator/consumer.h>
 #include <linux/reset.h>
+#include <linux/seq_file.h>
 #include <linux/spi/spi.h>
 #include <linux/sysfs.h>
 #include <linux/types.h>
@@ -183,6 +184,11 @@
 #define AD9910_RAM_ENABLED(st)		\
 	FIELD_GET(AD9910_CFR1_RAM_ENABLE_MSK, (st)->reg[AD9910_REG_CFR1].val32)
 
+#define AD9910_DEST_FREQUENCY		0
+#define AD9910_DEST_PHASE		1
+#define AD9910_DEST_AMPLITUDE		2
+#define AD9910_DEST_POLAR		3
+
 /* PLL constants */
 #define AD9910_PLL_MIN_N		12
 #define AD9910_PLL_MAX_N		127
@@ -310,6 +316,14 @@ enum {
 	AD9910_CHAN_IDX_OSK,
 };
 
+enum {
+	AD9910_SCAN_IDX_AMP,
+	AD9910_SCAN_IDX_PHASE,
+	AD9910_SCAN_IDX_FREQ,
+	AD9910_SCAN_IDX_POLAR_AMP,
+	AD9910_SCAN_IDX_POLAR_PHASE,
+};
+
 enum {
 	AD9910_POWERDOWN,
 	AD9910_DWELL_EN,
@@ -1960,6 +1974,158 @@ static int ad9910_setup(struct device *dev, struct ad9910_state *st,
 	return ad9910_io_update(st);
 }
 
+static inline const char *ad9910_frequency_source_get(struct iio_dev *indio_dev)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	bool ram_en, mode_en;
+
+	guard(mutex)(&st->lock);
+
+	/* RAM enabled and data destination is frequency */
+	ram_en = AD9910_RAM_ENABLED(st);
+	if (ram_en && AD9910_DEST_FREQUENCY ==
+		      FIELD_GET(AD9910_CFR1_RAM_PLAYBACK_DEST_MSK,
+				st->reg[AD9910_REG_CFR1].val32))
+		return ad9910_channel_str[AD9910_CHAN_IDX_RAM];
+
+	/* DRG enabled and data destination is frequency */
+	mode_en = FIELD_GET(AD9910_CFR2_DRG_ENABLE_MSK,
+			    st->reg[AD9910_REG_CFR2].val32);
+	if (mode_en && AD9910_DEST_FREQUENCY ==
+		       FIELD_GET(AD9910_CFR2_DRG_DEST_MSK,
+				 st->reg[AD9910_REG_CFR2].val32))
+		return ad9910_channel_str[AD9910_CHAN_IDX_DRG_FREQ];
+
+	/* Parallel data port enabled and data destination is frequency */
+	mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
+			    st->reg[AD9910_REG_CFR2].val32);
+	if (mode_en && indio_dev->active_scan_mask &&
+	    test_bit(AD9910_SCAN_IDX_FREQ, indio_dev->active_scan_mask))
+		return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_FREQ];
+
+	/* FTW: RAM enabled and data destination is phase, amplitude, or polar */
+	if (ram_en)
+		return ad9910_channel_str[AD9910_CHAN_IDX_RAM];
+
+	/* single tone profiles */
+	return ad9910_channel_str[AD9910_CHAN_IDX_PROFILE_0 + st->profile];
+}
+
+static int ad9910_frequency_source_show(struct seq_file *s, void *ignored)
+{
+	seq_printf(s, "%s\n", ad9910_frequency_source_get(s->private));
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(ad9910_frequency_source);
+
+static inline const char *ad9910_phase_source_get(struct iio_dev *indio_dev)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	bool ram_en, mode_en;
+	u32 destination;
+
+	guard(mutex)(&st->lock);
+
+	/* RAM enabled and data destination is phase or polar  */
+	ram_en = AD9910_RAM_ENABLED(st);
+	if (ram_en) {
+		destination = FIELD_GET(AD9910_CFR1_RAM_PLAYBACK_DEST_MSK,
+					st->reg[AD9910_REG_CFR1].val32);
+		if (destination == AD9910_DEST_PHASE ||
+		    destination == AD9910_DEST_POLAR)
+			return ad9910_channel_str[AD9910_CHAN_IDX_RAM];
+	}
+
+	/* DRG enabled and data destination is phase */
+	mode_en = FIELD_GET(AD9910_CFR2_DRG_ENABLE_MSK,
+			    st->reg[AD9910_REG_CFR2].val32);
+	if (mode_en && AD9910_DEST_PHASE ==
+		       FIELD_GET(AD9910_CFR2_DRG_DEST_MSK,
+				 st->reg[AD9910_REG_CFR2].val32))
+		return ad9910_channel_str[AD9910_CHAN_IDX_DRG_PHASE];
+
+	/* Parallel data port enabled and data destination is phase */
+	mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
+			    st->reg[AD9910_REG_CFR2].val32);
+	if (mode_en && indio_dev->active_scan_mask) {
+		if (test_bit(AD9910_SCAN_IDX_PHASE, indio_dev->active_scan_mask))
+			return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_PHASE];
+		if (test_bit(AD9910_SCAN_IDX_POLAR_PHASE, indio_dev->active_scan_mask))
+			return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_POLAR_PHASE];
+	}
+
+	/* POW: RAM enabled and data destination is frequency or amplitude */
+	if (ram_en)
+		return ad9910_channel_str[AD9910_CHAN_IDX_RAM];
+
+	/* single tone profiles */
+	return ad9910_channel_str[AD9910_CHAN_IDX_PROFILE_0 + st->profile];
+}
+
+static int ad9910_phase_source_show(struct seq_file *s, void *ignored)
+{
+	seq_printf(s, "%s\n", ad9910_phase_source_get(s->private));
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(ad9910_phase_source);
+
+static inline const char *ad9910_amplitude_source_get(struct iio_dev *indio_dev)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	bool ram_en, mode_en;
+	u32 destination;
+
+	guard(mutex)(&st->lock);
+
+	/* OSK enabled */
+	mode_en = FIELD_GET(AD9910_CFR1_OSK_ENABLE_MSK,
+			    st->reg[AD9910_REG_CFR1].val32);
+	if (mode_en)
+		return ad9910_channel_str[AD9910_CHAN_IDX_OSK];
+
+	/* RAM enabled and data destination is amplitude or polar */
+	ram_en = AD9910_RAM_ENABLED(st);
+	if (ram_en) {
+		destination = FIELD_GET(AD9910_CFR1_RAM_PLAYBACK_DEST_MSK,
+					st->reg[AD9910_REG_CFR1].val32);
+		if (destination == AD9910_DEST_AMPLITUDE ||
+		    destination == AD9910_DEST_POLAR)
+			return ad9910_channel_str[AD9910_CHAN_IDX_RAM];
+	}
+
+	/* DRG enabled and data destination is amplitude */
+	mode_en = FIELD_GET(AD9910_CFR2_DRG_ENABLE_MSK,
+			    st->reg[AD9910_REG_CFR2].val32);
+	if (mode_en && AD9910_DEST_AMPLITUDE ==
+		       FIELD_GET(AD9910_CFR2_DRG_DEST_MSK,
+				 st->reg[AD9910_REG_CFR2].val32))
+		return ad9910_channel_str[AD9910_CHAN_IDX_DRG_AMP];
+
+	/* Parallel data port enabled and data destination is amplitude */
+	mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
+			    st->reg[AD9910_REG_CFR2].val32);
+	if (mode_en && indio_dev->active_scan_mask) {
+		if (test_bit(AD9910_SCAN_IDX_AMP, indio_dev->active_scan_mask))
+			return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_AMP];
+		if (test_bit(AD9910_SCAN_IDX_POLAR_AMP, indio_dev->active_scan_mask))
+			return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_POLAR_AMP];
+	}
+
+	/* only way to control amplitude at this point is through OSK */
+	if (ram_en)
+		return ad9910_channel_str[AD9910_CHAN_IDX_OSK];
+
+	/* single tone profiles */
+	return ad9910_channel_str[AD9910_CHAN_IDX_PROFILE_0 + st->profile];
+}
+
+static int ad9910_amplitude_source_show(struct seq_file *s, void *ignored)
+{
+	seq_printf(s, "%s\n", ad9910_amplitude_source_get(s->private));
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(ad9910_amplitude_source);
+
 static inline void ad9910_debugfs_init(struct ad9910_state *st,
 				       struct iio_dev *indio_dev)
 {
@@ -1975,6 +2141,13 @@ static inline void ad9910_debugfs_init(struct ad9910_state *st,
 
 	snprintf(buf, sizeof(buf), "/sys/class/firmware/%s/data", st->ram_fwu_name);
 	debugfs_create_symlink("ram_data", d, buf);
+
+	debugfs_create_file("frequency_source", 0400, d, indio_dev,
+			    &ad9910_frequency_source_fops);
+	debugfs_create_file("phase_source", 0400, d, indio_dev,
+			    &ad9910_phase_source_fops);
+	debugfs_create_file("amplitude_source", 0400, d, indio_dev,
+			    &ad9910_amplitude_source_fops);
 }
 
 static int ad9910_probe(struct spi_device *spi)

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 15/16] iio: ABI: add docs for ad9910 sysfs entries
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (13 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 14/16] iio: frequency: ad9910: show channel priority in debugfs Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  2026-06-18 13:44   ` sashiko-bot
  2026-06-18 13:27 ` [PATCH v6 16/16] docs: iio: add documentation for ad9910 driver Rodrigo Alencar via B4 Relay
  15 siblings, 1 reply; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add custom ABI documentation file for the DDS AD9910 with sysfs entries to
control some parameters from the Digital Ramp Generator and OSK engine.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 .../ABI/testing/sysfs-bus-iio-frequency-ad9910     | 27 ++++++++++++++++++++++
 MAINTAINERS                                        |  1 +
 2 files changed, 28 insertions(+)

diff --git a/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910 b/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
new file mode 100644
index 000000000000..a54afeb64302
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
@@ -0,0 +1,27 @@
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_dwell_en
+KernelVersion:
+Contact:	linux-iio@vger.kernel.org
+Description:
+		For a channel that produces parametric sweeps, this attribute controls
+		the sweep behavior at the configured limit. It enables dwell mode at a
+		sweep limit when set to 1, i.e., after the sweep is complete the output
+		value stays at the limit. Otherwise (setting this value to 0), the sweep
+		may stop or restart from the initial position, or even continue by
+		reversing the sweep direction.
+
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_raw_roc
+KernelVersion:
+Contact:	linux-iio@vger.kernel.org
+Description:
+		For a channel that produces parametric sweeps, this attribute controls
+		the rate of change of the parameter in raw units per second (slope).
+		This value may be influenced by the channel sampling_frequency attribute
+		if available. Multiplying this value by the scale attribute of the
+		channel yields the rate of change in physical units per second.
+
+What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_raw_roc_available
+KernelVersion:
+Contact:	linux-iio@vger.kernel.org
+Description:
+		Lists the available roc values for the channel. Values are
+		space-separated in ascending order.
diff --git a/MAINTAINERS b/MAINTAINERS
index 9b8cef7923ca..a76a15f02183 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1650,6 +1650,7 @@ M:	Rodrigo Alencar <rodrigo.alencar@analog.com>
 L:	linux-iio@vger.kernel.org
 S:	Supported
 W:	https://ez.analog.com/linux-software-drivers
+F:	Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
 F:	Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
 F:	drivers/iio/frequency/ad9910.c
 

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* [PATCH v6 16/16] docs: iio: add documentation for ad9910 driver
  2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
                   ` (14 preceding siblings ...)
  2026-06-18 13:27 ` [PATCH v6 15/16] iio: ABI: add docs for ad9910 sysfs entries Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:27 ` Rodrigo Alencar via B4 Relay
  15 siblings, 0 replies; 28+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-06-18 13:27 UTC (permalink / raw)
  To: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening
  Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva, Rodrigo Alencar

From: Rodrigo Alencar <rodrigo.alencar@analog.com>

Add documentation for the AD9910 DDS IIO driver, which describes channels,
DDS modes, attributes and ABI usage examples.

Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 Documentation/iio/ad9910.rst | 759 +++++++++++++++++++++++++++++++++++++++++++
 Documentation/iio/index.rst  |   1 +
 MAINTAINERS                  |   1 +
 3 files changed, 761 insertions(+)

diff --git a/Documentation/iio/ad9910.rst b/Documentation/iio/ad9910.rst
new file mode 100644
index 000000000000..113521fead3e
--- /dev/null
+++ b/Documentation/iio/ad9910.rst
@@ -0,0 +1,759 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+=============
+AD9910 driver
+=============
+
+Direct Digital Synthesizer (DDS) driver for the Analog Devices Inc. AD9910.
+The module name is ``ad9910``.
+
+* `AD9910 <https://www.analog.com/en/products/ad9910.html>`_
+
+The AD9910 is a 1 GSPS DDS with a 14-bit DAC, controlled over SPI. The driver
+exposes the device through a hierarchy of typed IIO output channels. The root
+``phy`` channel controls the system clock and full-scale output current.
+Sub-channels provide independent control over single tone profiles, parallel
+port modulation, digital ramp generation (DRG), RAM playback and output shift
+keying (OSK).
+
+
+Channel hierarchy
+=================
+
+The driver exposes the following IIO output channels, each identified by a
+unique channel number and a human-readable label. The ``phy`` channel is the
+root of the hierarchy. Changing its ``sampling_frequency`` reconfigures the
+system clock (SYSCLK) which affects all other channels.
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Channel
+     - Label
+     - Parent
+     - Description
+
+   * - ``out_altcurrent100``
+     - ``phy``
+     -
+     - Physical output: system clock and full-scale output current (:math:`I_{FS}`).
+       See `Physical channel`_.
+
+   * - ``out_altcurrent110`` ... ``out_altcurrent117``
+     - ``profile0`` ... ``profile7``
+     - ``phy``
+     - Single tone control: frequency, phase, amplitude, enable.
+       See `Single Tone mode`_.
+
+   * - ``out_altcurrent120``
+     - ``parallel_amplitude``
+     - ``phy``
+     - Parallel port amplitude channel.
+       See `Parallel Port mode`_.
+
+   * - ``out_phase120``
+     - ``parallel_phase``
+     - ``phy``
+     - Parallel port phase channel.
+
+   * - ``out_frequency120``
+     - ``parallel_frequency``
+     - ``phy``
+     - Parallel port frequency channel: ``scale`` sets the FM gain
+       (power-of-2 multiplier), ``offset`` sets the base FTW.
+
+   * - ``out_altcurrent121``
+     - ``parallel_polar_amplitude``
+     - ``phy``
+     - Parallel polar amplitude channel: ``scale`` is the amplitude
+       resolution, ``offset`` is the amplitude bias (lower 6 bits of ASF).
+
+   * - ``out_phase121``
+     - ``parallel_polar_phase``
+     - ``phy``
+     - Parallel polar phase channel: ``scale`` is the phase resolution,
+       ``offset`` is the phase bias (lower 8 bits of POW).
+
+   * - ``out_frequency130``
+     - ``drg_frequency``
+     - ``phy``
+     - DRG frequency channel: ``en`` selects and enables the DRG with
+       frequency as the ramp target.
+
+   * - ``out_phase130``
+     - ``drg_phase``
+     - ``phy``
+     - DRG phase channel: ``en`` selects and enables the DRG with phase
+       as the ramp target.
+
+   * - ``out_altcurrent130``
+     - ``drg_amplitude``
+     - ``phy``
+     - DRG amplitude channel: ``en`` selects and enables the DRG with
+       amplitude as the ramp target.
+       See `Digital ramp generator (DRG)`_.
+
+   * - ``out_altcurrent131``
+     - ``drg_rising``
+     - ``drg_amplitude``
+     - DRG rising-ramp parameters: limit code, dwell enable, ramp clock,
+       rate of change.
+
+   * - ``out_altcurrent132``
+     - ``drg_falling``
+     - ``drg_amplitude``
+     - DRG falling-ramp parameters: limit code, dwell enable, ramp clock,
+       rate of change.
+
+   * - ``out_altcurrent140``
+     - ``ram``
+     - ``phy``
+     - RAM playback: enable, frequency, phase and sampling frequency for
+       the active profile. See `RAM mode`_.
+
+   * - ``out_altcurrent150``
+     - ``osk``
+     - ``phy``
+     - Output shift keying (OSK): enable, amplitude code, ramp rate,
+       rate of change. See `Output Shift Keying (OSK)`_.
+
+DDS modes
+=========
+
+The AD9910 supports multiple modes of operation that can be configured
+independently or in combination. Each DDS core parameter (frequency, phase
+and amplitude) can come from different sources, but only one is active at a
+time. This activation depends on a priority list, which is based on the enable
+and destination configurations for such modes. The following tables are
+extracted from the AD9910 datasheet and summarize the control parameters for
+each mode and their priority when multiple sources are enabled simultaneously:
+
+.. flat-table:: DDS Frequency Control
+   :header-rows: 1
+
+   * - Priority
+     - Data Source
+     - Conditions
+
+   * - Highest Priority
+     - RAM
+     - RAM enabled and data destination is frequency
+
+   * -
+     - DRG
+     - DRG enabled and data destination is frequency
+
+   * -
+     - Parallel data and Frequency Tuning Word, FTW (frequency_offset)
+     - Parallel data port enabled and data destination is frequency
+
+   * -
+     - FTW register (frequency)
+     - RAM enabled and data destination is not frequency
+
+   * - Lowest Priority
+     - FTW (frequency) in single tone channel for the active profile
+     - All other cases
+
+.. flat-table:: DDS Phase Control
+   :header-rows: 1
+
+   * - Priority
+     - Data Source
+     - Conditions
+
+   * - Highest Priority
+     - RAM
+     - RAM enabled and data destination is phase or polar
+
+   * -
+     - DRG
+     - DRG enabled and data destination is phase
+
+   * -
+     - Parallel data port
+     - Parallel data port enabled and data destination is phase
+
+   * -
+     - Parallel data port and Phase Offset Word, POW register LSBs (phase_offset)
+     - Parallel data port enabled and data destination is polar
+
+   * -
+     - POW register (phase)
+     - RAM enabled and destination is not phase nor polar
+
+   * - Lowest Priority
+     - POW (phase) in single tone channel for the active profile
+     - All other cases
+
+.. flat-table:: DDS Amplitude Control
+   :header-rows: 1
+
+   * - Priority
+     - Data Source
+     - Conditions
+
+   * - Highest Priority
+     - Amplitude Scale Factor, ASF register and OSK generator
+     - OSK enabled
+
+   * -
+     - RAM
+     - RAM enabled and data destination is amplitude or polar
+
+   * -
+     - DRG
+     - DRG enabled and data destination is amplitude
+
+   * -
+     - Parallel data port
+     - Parallel data port enabled and data destination is amplitude
+
+   * -
+     - Parallel data port and ASF register LSBs (scale_offset)
+     - Parallel data port enabled and data destination is polar
+
+   * - Lowest Priority
+     - ASF (scale) in single tone channel for the active profile
+     - (Amplitude scale is already enabled by default)
+
+While debugging or testing, the debug attributes ``frequency_source``,
+``phase_source`` and ``amplitude_source`` can be used to read the label of
+the channel that is actively controlling the correspondent DDS parameter,
+which reflects the priority list described above.
+
+Single Tone mode
+----------------
+
+Single tone is the baseline operating mode. The ``profileY`` channels
+provide enable, frequency, phase and amplitude control:
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Attribute
+     - Unit
+     - Description
+
+   * - ``en``
+     - boolean (0 or 1)
+     - Enable/disable profile Y. Only one profile can be active at a
+       time. When enabling a profile it disables the current active profile.
+       Disabling an active profile brings the device to a powered down state.
+
+   * - ``frequency``
+     - Hz
+     - Output frequency. Range :math:`[0, f_{SYSCLK}/2)`. Stored in the
+       profile's frequency tuning word (FTW).
+
+   * - ``phase``
+     - rad
+     - Phase offset. Range :math:`[0, 2\pi)`. Stored in the profile's phase
+       offset word (POW).
+
+   * - ``raw``
+     - integer
+     - Amplitude scale factor code. Range :math:`[0, 16383]`. Stored in the
+       profile's amplitude scale factor (ASF) register. The physical output
+       amplitude is ``raw * scale`` where ``scale`` is read from the ``phy``
+       channel.
+
+Profile switching is allowed while RAM mode is enabled. In that case single
+tone parameters are stored in a shadow register and are not written to
+hardware until RAM mode is disabled.
+
+Usage examples
+^^^^^^^^^^^^^^
+
+Configure a 100 MHz tone in profile 2 and set it as the active profile:
+
+.. code-block:: bash
+
+  echo 100000000 > /sys/bus/iio/devices/iio\:device0/out_altcurrent112_frequency
+  echo 0 > /sys/bus/iio/devices/iio\:device0/out_altcurrent112_phase
+  echo 16383 > /sys/bus/iio/devices/iio\:device0/out_altcurrent112_raw
+
+  # Activate profile 2
+  echo 1 > /sys/bus/iio/devices/iio\:device0/out_altcurrent112_en
+
+Read back the current single tone frequency:
+
+.. code-block:: bash
+
+  cat /sys/bus/iio/devices/iio\:device0/out_altcurrent112_frequency
+
+Parallel Port mode
+------------------
+
+The parallel port allows real-time modulation of DDS parameters through a
+16-bit external data bus. The driver exposes separate typed channels for each
+modulation target.
+
+Non-polar modulation
+^^^^^^^^^^^^^^^^^^^^
+
+In non-polar mode each DDS parameter is controlled by an independent 16-bit bus
+input. The parallel port channels expose the resolution and base offset of their
+respective bus inputs:
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Channel
+     - Attribute
+     - Unit
+     - Description
+
+   * - ``out_phase120``
+     - ``scale``
+     - rad
+     - Phase resolution per parallel bus LSB. Fixed at :math:`\pi / 2^{15}`
+       rad/LSB (full 16-bit POW resolution).
+
+   * - ``out_frequency120``
+     - ``scale``
+     - Hz
+     - Outputs the frequency scale evaluated as
+       :math:`f_{SYSCLK} \cdot FM / 2^{32}`. Assuming that :math:`f_{SYSCLK}` is
+       fixed, it is used to configure the modulation gain :math:`FM`, which is a
+       power-of-2 multiplier in range :math:`[1, 32768]`. Writing to this
+       attribute rounds up to the nearest :math:`FM` power of 2.
+
+   * - ``out_frequency120``
+     - ``offset``
+     - Hz
+     - Outputs the frequency offset in raw units evaluated as :math:`FTW / FM`.
+       Assuming that :math:`FM` is fixed, it is used to configure the FTW
+       register, which is a 32-bit unsigned integer.
+
+Polar modulation
+^^^^^^^^^^^^^^^^
+
+In polar mode a single 16-bit bus word carries both amplitude (high byte)
+and phase (low byte). The ``parallel_polar_amplitude`` and
+``parallel_polar_phase`` channels configure the bias applied to the low-order
+bits of the ASF and POW registers respectively:
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Channel
+     - Attribute
+     - Unit
+     - Description
+
+   * - ``out_altcurrent121``
+     - ``scale``
+     - mA/LSB
+     - Full-scale amplitude resolution (read-only, fixed at :math:`I_{FS} / 2^{8}`).
+
+   * - ``out_altcurrent121``
+     - ``offset``
+     - fractional (raw units)
+     - Amplitude bias. Lower 6 bits of ASF register. Range :math:`[0, 1)`.
+
+   * - ``out_phase121``
+     - ``scale``
+     - rad/LSB
+     - Phase resolution (read-only, fixed at :math:`\pi / 2^{7}`).
+
+   * - ``out_phase121``
+     - ``offset``
+     - fractional (raw units)
+     - Phase bias. Lower 8 bits of POW register. Range :math:`[0, 1)`.
+
+Usage examples
+^^^^^^^^^^^^^^
+
+Set parallel port frequency modulation with a modulation gain of 16 and a 50 MHz
+offset:
+
+.. code-block:: bash
+
+  # f_SYSCLK = 1 GHz, FM = 16
+  # frequency scale = f_SYSCLK * FM / 2^32 = 3.725290298
+  echo 3.725290298 > /sys/bus/iio/devices/iio\:device0/out_frequency120_scale
+  # frequency offset = 50 MHz / scale = 50e6 / 13421772.8
+  echo 13421772.8 > /sys/bus/iio/devices/iio\:device0/out_frequency120_offset
+
+One should choose a frequency scale that allows all the desired frequencies
+to be represented in the 16-bit bus range, i.e.,
+:math:`scale = (f_{max} - f_{min}) / 2^{16}`.
+
+
+Digital ramp generator (DRG)
+----------------------------
+
+The DRG produces linear frequency, phase or amplitude sweeps using dedicated
+hardware. The active ramp target (destination) is selected by enabling the
+corresponding typed channel at channel number 130:
+
+- ``out_frequency130`` (label ``drg_frequency``) — ramp targets frequency
+- ``out_phase130`` (label ``drg_phase``) — ramp targets phase
+- ``out_altcurrent130`` (label ``drg_amplitude``) — ramp targets amplitude
+
+Writing ``en=1`` to one of these channels enables the DRG and switches its
+destination. Writing ``en=0`` disables the DRG if the channel is the current
+active destination; writing to an already-inactive destination is a no-op.
+
+Each destination channel also exposes a read-only ``scale`` attribute
+reporting the physical quantity per ramp register LSB, which allows converting
+raw limit codes to physical values.
+
+The two ramp channels ``out_altcurrent131`` (``drg_rising``) and
+``out_altcurrent132`` (``drg_falling``) configure ascending and descending
+ramp parameters independently.
+
+Destination channel attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Attribute
+     - Unit
+     - Description
+
+   * - ``en``
+     - boolean
+     - Enable the DRG with this channel as the active destination. Only one
+       destination can be active at a time.
+
+   * - ``scale``
+     - Hz/LSB, rad/LSB or mA/LSB
+     - Read-only. Physical quantity per raw units. Multiply a ramp
+       rising/falling channel ``raw`` value by this scale to get the physical
+       ramp target.
+
+Ramp channel attributes
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Attribute
+     - Unit
+     - Description
+
+   * - ``dwell_en``
+     - boolean
+     - Enable dwell at the ramp limit. When disabled, the ramp
+       auto-transitions at this limit without waiting for the DRCTL pin.
+       Disabling both creates a bidirectional continuous ramp (triangular
+       pattern). Other combinations create single-shot ramps at the DRCTL
+       pin transition.
+
+   * - ``raw``
+     - integer (64-bit)
+     - Ramp limit expressed as a raw DRG register code in
+       :math:`[0, 2^{32}-1]`. The physical value is ``raw * scale`` where
+       ``scale`` is read from the active destination channel.
+
+   * - ``sampling_frequency``
+     - Hz
+     - Ramp clock rate. Controlled by an integer divider; the written value
+       is adjusted to the nearest supported rate.
+
+   * - ``raw_roc``
+     - /s
+     - Rate of change. Number of register codes advanced per second, computed
+       from the hardware step size and the current ramp clock. Writing
+       requires ``sampling_frequency`` to be configured first.
+
+Usage examples
+^^^^^^^^^^^^^^
+
+Configure a frequency sweep from 40 MHz to 60 MHz with a rate of change of
+25 GHz/s:
+
+.. code-block:: bash
+
+  # Disable dwell on both limits for a bidirectional continuous ramp
+  echo 0 > /sys/bus/iio/devices/iio\:device0/out_altcurrent131_dwell_en
+  echo 0 > /sys/bus/iio/devices/iio\:device0/out_altcurrent132_dwell_en
+
+  # Set ramp rate at 250 MHz
+  echo 250000000 > /sys/bus/iio/devices/iio\:device0/out_altcurrent131_sampling_frequency
+  echo 250000000 > /sys/bus/iio/devices/iio\:device0/out_altcurrent132_sampling_frequency
+
+  # read the frequency scale to convert physical values to raw units
+  cat /sys/bus/iio/devices/iio\:device0/out_frequency130_scale
+  0.232830643650
+
+  # 40 MHz / 0.232830643650 = 171798692
+  echo 171798692 > /sys/bus/iio/devices/iio\:device0/out_altcurrent131_raw
+  # 60 MHz / 0.232830643650 = 257698038
+  echo 257698038 > /sys/bus/iio/devices/iio\:device0/out_altcurrent132_raw
+
+  # 25 GHz/s / 0.232830643650 = 107374182402
+  echo 107374182402 > /sys/bus/iio/devices/iio\:device0/out_altcurrent131_raw_roc
+  echo 107374182402 > /sys/bus/iio/devices/iio\:device0/out_altcurrent132_raw_roc
+
+  # Enable the DRG with frequency as the destination
+  echo 1 > /sys/bus/iio/devices/iio\:device0/out_frequency130_en
+
+RAM mode
+--------
+
+The AD9910 contains a 1024 x 32-bit RAM that can be loaded with waveform data
+and played back to modulate frequency, phase, amplitude, or polar (phase +
+amplitude) parameters.
+
+RAM control channel attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Attribute
+     - Unit
+     - Description
+
+   * - ``en``
+     - boolean
+     - Enable/disable RAM playback. Toggling swaps profile registers between
+       single tone and RAM configurations across all 8 profiles.
+
+   * - ``frequency``
+     - Hz
+     - Frequency tuning word used as the single tone frequency when
+       RAM destination is not ``frequency``. Range: :math:`[0, f_{SYSCLK}/2)`.
+
+   * - ``phase``
+     - rad
+     - Phase offset word used as the single tone phase when RAM destination
+       is not ``phase``. Range: :math:`[0, 2\pi)`.
+
+   * - ``sampling_frequency``
+     - Hz
+     - RAM playback step rate of the active profile, which controls how fast
+       the address counter advances. Controlled by an integer divider; the
+       written value is adjusted to the nearest supported rate.
+
+Loading RAM data
+^^^^^^^^^^^^^^^^
+
+RAM data is loaded through the firmware upload framework. The driver registers
+a firmware upload sysfs entry named ``iio_deviceX:ram``. The firmware data
+follows a binary format (version 1) with an 80-byte header followed by data
+words. All fields are big-endian.
+
+.. flat-table:: RAM firmware header (80 bytes)
+   :header-rows: 1
+
+   * - Offset
+     - Size
+     - Field
+     - Description
+
+   * - 0
+     - 4
+     - ``magic``
+     - Magic number: ``0x00AD9910``
+
+   * - 4
+     - 2
+     - ``version``
+     - Format version: ``0x0001``
+
+   * - 6
+     - 2
+     - ``wcount``
+     - Number of 32-bit RAM data words (0--1024)
+
+   * - 8
+     - 4
+     - ``crc``
+     - CRC32 checksum over ``cfr1``, ``profiles`` and ``words``
+
+   * - 12
+     - 4
+     - ``cfr1``
+     - CFR1 register value. Only RAM-relevant bits are used:
+       bits [30:29] set data destination (00: frequency, 01: phase,
+       10: amplitude, 11: polar); bits [20:17] set internal profile
+       control (see datasheet Table 14)
+
+   * - 16
+     - 64
+     - ``profiles[0..7]``
+     - 8 sets of 8-byte RAM profile configurations (see below)
+
+   * - 80
+     - 4 x wcount
+     - ``words[]``
+     - RAM data words in reverse order
+
+Each 8-byte profile entry contains:
+
+.. flat-table:: RAM profile entry (8 bytes)
+   :header-rows: 1
+
+   * - Bits
+     - Field
+     - Description
+
+   * - [55:40]
+     - Address step rate
+     - Controls playback speed for this profile
+
+   * - [39:30]
+     - End address
+     - Last RAM address for this profile
+
+   * - [23:14]
+     - Start address
+     - First RAM address for this profile
+
+   * - [5]
+     - No-dwell high
+     - No-dwell at high limit (ramp-up mode)
+
+   * - [3]
+     - Zero-crossing
+     - Zero-crossing enable (direct-switch mode)
+
+   * - [2:0]
+     - Operating mode
+     - 000: direct switch, 001: ramp-up, 010: bidirectional,
+       011: bidirectional continuous, 100: ramp-up continuous
+
+Usage examples
+^^^^^^^^^^^^^^
+
+Configure RAM mode with firmware data and enable it:
+
+.. code-block:: bash
+
+  # Load RAM data via firmware upload
+  echo 1 > /sys/class/firmware/iio\:device0\:ram/loading
+  cat ad9910-ram.bin > /sys/class/firmware/iio\:device0\:ram/data
+  echo 0 > /sys/class/firmware/iio\:device0\:ram/loading
+
+  # Enable RAM mode
+  echo 1 > /sys/bus/iio/devices/iio\:device0/out_altcurrent140_en
+
+Output Shift Keying (OSK)
+-------------------------
+
+OSK controls the output amplitude envelope, allowing the output to be ramped
+on/off rather than switched abruptly.
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Attribute
+     - Unit
+     - Description
+
+   * - ``en``
+     - boolean (0 or 1)
+     - Enable/disable OSK.
+
+   * - ``raw``
+     - integer
+     - Target amplitude code. 14-bit ASF field. Range: :math:`[0, 16383]`.
+       The physical output amplitude is ``raw * scale`` where ``scale`` is read
+       from the ``phy`` channel.
+
+   * - ``raw_roc``
+     - /s
+     - Amplitude ramp rate. Writing a non-zero value enables automatic OSK
+       and selects the closest hardware step size. Writing ``0`` disables
+       automatic ramping (manual control via ``raw``). Writing the maximum
+       available value enables pin-controlled immediate transition.
+
+   * - ``raw_roc_available``
+     - /s
+     - Lists the available ``raw_roc`` values based on the current
+       ``sampling_frequency``. The first value is always ``0`` (disabled) and
+       the last value corresponds to pin-controlled immediate mode.
+
+   * - ``sampling_frequency``
+     - Hz
+     - OSK ramp clock. Controlled by an integer divider; the written value
+       is adjusted to the nearest supported rate.
+
+Usage examples
+^^^^^^^^^^^^^^
+
+Enable OSK with automatic ramping:
+
+.. code-block:: bash
+
+  # Set ramp rate 1MHz
+  echo 1000000 > /sys/bus/iio/devices/iio\:device0/out_altcurrent150_sampling_frequency
+
+  # Check available rate of change values
+  cat /sys/bus/iio/devices/iio\:device0/out_altcurrent150_raw_roc_available
+  0 1000000 2000000 4000000 8000000 16383000000
+
+  # Enable automatic OSK with a rate of change of 8000000 raw units/s
+  echo 8000000 > /sys/bus/iio/devices/iio\:device0/out_altcurrent150_raw_roc
+
+  # Enable OSK
+  echo 1 > /sys/bus/iio/devices/iio\:device0/out_altcurrent150_en
+
+Enable pin-controlled immediate OSK:
+
+.. code-block:: bash
+
+  # Enable OSK in manual mode (no ramp)
+  echo 0 > /sys/bus/iio/devices/iio\:device0/out_altcurrent150_raw_roc
+  echo 1 > /sys/bus/iio/devices/iio\:device0/out_altcurrent150_en
+
+  # Set target amplitude to full scale
+  echo 16383 > /sys/bus/iio/devices/iio\:device0/out_altcurrent150_raw
+
+Physical channel
+================
+
+The ``phy`` channel provides device-level control:
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Attribute
+     - Unit
+     - Description
+
+   * - ``sampling_frequency``
+     - Hz
+     - System clock (SYSCLK) frequency. When the internal PLL is enabled
+       (via the ``adi,pll-enable`` devicetree property), configures the PLL
+       multiplier (Range: :math:`[420, 1000]` MHz). Without PLL, the reference
+       clock can only be divided by 2.
+
+   * - ``scale``
+     - mA/LSB
+     - Full-scale DAC output current per amplitude code LSB, which is evaluated
+       as :math:`I_{FS}/2^{14}`. Shared across all ``altcurrent`` channels.
+       Setting this attribute reconfigures the auxiliary DAC full-scale code and
+       updates the effective amplitude resolution for single tone profiles,
+       DRG amplitude ramps and OSK.
+
+   * - ``powerdown``
+     - boolean (0 or 1)
+     - Software power-down. Writing 1 powers down the digital core, DAC,
+       reference clock input and auxiliary DAC simultaneously.
+
+Usage examples
+--------------
+
+Set the system clock to 1 GHz:
+
+.. code-block:: bash
+
+  echo 1000000000 > /sys/bus/iio/devices/iio\:device0/out_altcurrent100_sampling_frequency
+
+Read current system clock frequency:
+
+.. code-block:: bash
+
+  cat /sys/bus/iio/devices/iio\:device0/out_altcurrent100_sampling_frequency
+
+Power down the device:
+
+.. code-block:: bash
+
+  echo 1 > /sys/bus/iio/devices/iio\:device0/out_altcurrent100_powerdown
\ No newline at end of file
diff --git a/Documentation/iio/index.rst b/Documentation/iio/index.rst
index b02b879b053a..4c30ef033685 100644
--- a/Documentation/iio/index.rst
+++ b/Documentation/iio/index.rst
@@ -30,6 +30,7 @@ Industrial I/O Kernel Drivers
    ad7606
    ad7625
    ad7944
+   ad9910
    ade9000
    adf41513
    adis16475
diff --git a/MAINTAINERS b/MAINTAINERS
index a76a15f02183..38e7fd5e3c34 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1652,6 +1652,7 @@ S:	Supported
 W:	https://ez.analog.com/linux-software-drivers
 F:	Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
 F:	Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
+F:	Documentation/iio/ad9910.rst
 F:	drivers/iio/frequency/ad9910.c
 
 ANALOG DEVICES INC MAX22007 DRIVER

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 07/16] iio: core: add hierarchical channel relationships
  2026-06-18 13:27 ` [PATCH v6 07/16] iio: core: add hierarchical channel relationships Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:33   ` sashiko-bot
  0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-06-18 13:33 UTC (permalink / raw)
  To: Rodrigo Alencar via B4 Relay; +Cc: robh, devicetree, conor+dt

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The commit message claims the new attribute returns the 'parent channel label', but the implementation actually returns the parent channel's sysfs prefix.
--

commit 1d085ae0a672cb964aa54dcb759867393a24b06b
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>

iio: core: add hierarchical channel relationships

This commit adds a parent-child relationship between IIO channels. It
introduces a parent pointer field in the iio_chan_spec struct and exposes a
sysfs attribute for userspace to read the parent channel information.

>     Add parent-child relationship between iio channels by creating a parent
>     pointer field in iio_chan_spec struct and exposing a sysfs attribute that
>     returns the parent channel label.
>
> Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>

> diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
> index 9373006235c87..3d12269f26f4a 100644
> --- a/drivers/iio/industrialio-core.c
> +++ b/drivers/iio/industrialio-core.c
> @@ -854,6 +854,21 @@ static ssize_t iio_read_channel_label(struct device *dev,
>  					 to_iio_dev_attr(attr)->c, buf);
>  }
>  
> +static ssize_t iio_read_channel_parent(struct device *dev,
> +				       struct device_attribute *attr,
> +				       char *buf)
> +{
> +	const struct iio_chan_spec *parent = to_iio_dev_attr(attr)->c->parent;
> +	int len;
> +
> +	len = __iio_chan_prefix_emit(parent, IIO_SEPARATE, buf, PAGE_SIZE - 1);

[Severity: Medium]
Is the commit description accurate regarding what this attribute returns?

The commit message states it returns the parent channel label. However,
looking at iio_read_channel_parent() in drivers/iio/industrialio-core.c,
this implementation uses __iio_chan_prefix_emit() to return the parent
channel's sysfs prefix instead (e.g., in_voltage0).

While returning the sysfs prefix makes sense for userspace to locate the
parent channel, using the term label in the commit message breaks IIO API
terminology. Could the commit message be updated to state that it returns
the sysfs prefix rather than the label?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260618-ad9910-iio-driver-v6-0-79125ffbe430@analog.com?part=7

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 08/16] dt-bindings: iio: frequency: add ad9910
  2026-06-18 13:27 ` [PATCH v6 08/16] dt-bindings: iio: frequency: add ad9910 Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:35   ` sashiko-bot
  0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-06-18 13:35 UTC (permalink / raw)
  To: Rodrigo Alencar via B4 Relay; +Cc: conor+dt, robh, devicetree

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] The `#clock-cells` property is missing from the `required` list and the `examples` section, violating Device Tree subsystem guidelines.
- [Medium] The commit message claims to include 'DAC current' configurations, but no such property exists in the schema.
- [Low] The `reset-gpios` property is missing the `maxItems: 1` constraint.
--

commit 14de29fc570f7c4ee9b2e39250151ecb7c9edb56
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>

dt-bindings: iio: frequency: add ad9910

DT-bindings for AD9910, a 1 GSPS DDS with 14-bit DAC. It includes
configurations for clocks, DAC current, reset and basic GPIO control.

> DT-bindings for AD9910, a 1 GSPS DDS with 14-bit DAC. It includes
> configurations for clocks, DAC current, reset and basic GPIO control.

[Severity: Medium]
The commit message states the binding includes configurations for
"DAC current". Does the schema actually define this property?

Looking at the schema, there is adi,charge-pump-current-microamp for the
PLL charge pump, but no property appears to configure the DAC current.

Could the commit message be updated to match the schema, or is the DAC current
property missing from the YAML?

> diff --git a/Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml b/Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
> new file mode 100644
> index 0000000000000..a78fe33ba21f9
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml

[ ... ]

> +  avdd18-supply:
> +    description: 1.8V Analog Core supply.
> +
> +  reset-gpios:
> +    description:
> +      GPIOs controlling the Main Device reset.

[Severity: Low]
Should reset-gpios have a maxItems: 1 constraint?

Unlike io-reset-gpios and powerdown-gpios below, this property lacks a
maxItems constraint. In dt-schema, properties ending in -gpios allow an
array of GPIOs unless explicitly constrained.

This omission allows invalid device tree configurations with multiple reset
GPIOs to pass schema validation.

> +  io-reset-gpios:
> +    maxItems: 1
> +    description:
> +      GPIO controlling the I/O_RESET pin.

[ ... ]

> +required:
> +  - compatible
> +  - reg
> +  - clocks
> +  - dvdd-io33-supply
> +  - avdd33-supply
> +  - dvdd18-supply
> +  - avdd18-supply

[Severity: High]
Should #clock-cells be included in this required list?

According to the Device Tree Bindings subsystem guidelines, when hardware
defines provider capabilities (acting as a clock provider as indicated by
the #clock-cells: const: 1 property defined earlier in this schema), the
corresponding standard properties must be explicitly included in the
required list.

> +allOf:
> +  - $ref: /schemas/spi/spi-peripheral-props.yaml#
> +
> +unevaluatedProperties: false
> +
> +examples:
> +  - |
> +    #include <dt-bindings/gpio/gpio.h>
> +    spi {
> +        #address-cells = <1>;
> +        #size-cells = <0>;
> +        dds@0 {
> +            compatible = "adi,ad9910";
> +            reg = <0>;

[Severity: High]
Does this example need the #clock-cells property?

The example device tree node is missing the #clock-cells property.

Subsystem guidelines dictate that the examples section must demonstrate
all required properties to pass dt_binding_check validation.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260618-ad9910-iio-driver-v6-0-79125ffbe430@analog.com?part=8

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 09/16] iio: frequency: ad9910: initial driver implementation
  2026-06-18 13:27 ` [PATCH v6 09/16] iio: frequency: ad9910: initial driver implementation Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:37   ` sashiko-bot
  0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-06-18 13:37 UTC (permalink / raw)
  To: Rodrigo Alencar via B4 Relay; +Cc: devicetree, robh, conor+dt

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Hardware sequencing violation: reference clock and GPIO powerdown action are initialized before device regulators, causing backpowering.
--

commit 71fb9226243ea37baeab1cd6841703ff6cde346d
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>
Subject: iio: frequency: ad9910: initial driver implementation

This commit adds the core AD9910 DDS driver infrastructure with single tone
mode support, including SPI register access, profile management via GPIO pins,
and PLL/DAC configuration from firmware properties.

> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> new file mode 100644
> index 0000000000000..95b01295e4a0a
> --- /dev/null
> +++ b/drivers/iio/frequency/ad9910.c
[ ... ]
> +static int ad9910_probe(struct spi_device *spi)
> +{
[ ... ]
> +	st->refclk = devm_clk_get_enabled(dev, "ref_clk");
> +	if (IS_ERR(st->refclk))
> +		return dev_err_probe(dev, PTR_ERR(st->refclk),
> +				     "Failed to get reference clock\n");
[ ... ]
> +	st->gpio_pwdown = devm_gpiod_get_optional(dev, "powerdown",
> +						  GPIOD_OUT_LOW);
> +	if (IS_ERR(st->gpio_pwdown))
> +		return dev_err_probe(dev, PTR_ERR(st->gpio_pwdown),
> +				     "failed to get powerdown gpio\n");
> +
> +	ret = devm_add_action_or_reset(dev, ad9910_hw_powerdown_action, st);
> +	if (ret)
> +		return dev_err_probe(dev, ret,
> +				     "failed to add hw powerdown action\n");
> +
> +	ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(supplies), supplies);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "Failed to get regulators\n");

[Severity: High]
Could this initialization order cause the device to be backpowered?

In ad9910_probe(), the reference clock is enabled and the hardware powerdown
cleanup action is registered before the device's regulators are enabled. This
might drive an active clock signal into the unpowered IC during the probe
sequence.

Additionally, because devres executes cleanup actions in reverse order (LIFO),
if a probe failure occurs later or the driver unbinds, the regulators will be
turned off first. The powerdown GPIO would then be driven high and the
reference clock would continue to toggle into an unpowered chip until they
are subsequently disabled.

Driving active digital signals into an unpowered CMOS device forward-biases
its internal ESD protection diodes, which can lead to latch-up or permanent
hardware damage. Should the regulators be enabled prior to applying any
clock or active GPIO signals?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260618-ad9910-iio-driver-v6-0-79125ffbe430@analog.com?part=9

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 10/16] iio: frequency: ad9910: add basic parallel port support
  2026-06-18 13:27 ` [PATCH v6 10/16] iio: frequency: ad9910: add basic parallel port support Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:41   ` sashiko-bot
  0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-06-18 13:41 UTC (permalink / raw)
  To: Rodrigo Alencar via B4 Relay; +Cc: robh, devicetree, conor+dt

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:

New issues:
- [Medium] 32-bit truncation in the parallel frequency scale calculation causes incorrect readback.
- [Medium] 64-bit integer overflow in parallel frequency offset calculation circumvents U32_MAX capping.
- [Medium] `roundup_pow_of_two` truncates 64-bit scale calculation on 32-bit architectures.
- [Medium] Integer overflow when reading and writing the frequency offset for the parallel frequency channel.

Pre-existing issues:
- [High] Kernel panic due to division by zero if the reference clock rate is zero.
--

commit 5740831576c6dff0dd6ca6d5b5930978950cfff8
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>

iio: frequency: ad9910: add basic parallel port support

Add parallel port support with amplitude, phase and frequency channels.
Those will be buffered capable channels, but only basic control of offset
and scale are implemented at this point.

> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> index 95b01295e4a0a..262702b62738d 100644
> --- a/drivers/iio/frequency/ad9910.c
> +++ b/drivers/iio/frequency/ad9910.c

[ ... ]

> @@ -640,11 +702,60 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
>  			return -EINVAL;
>  		}
>  	case IIO_CHAN_INFO_SCALE:
> -		tmp64 = (u64)st->data.output_current_uA *
> -			AD9910_NANO_MILLIAMP_PER_MICROAMP;
> -		*val = 0;
> -		*val2 = tmp64 >> 14;
> -		return IIO_VAL_INT_PLUS_NANO;
> +		switch (chan->address) {
> +		case AD9910_CHAN_IDX_PHY:
> +			tmp64 = (u64)st->data.output_current_uA *
> +				AD9910_NANO_MILLIAMP_PER_MICROAMP;
> +			*val = 0;
> +			*val2 = tmp64 >> 14;
> +			return IIO_VAL_INT_PLUS_NANO;
> +		case AD9910_CHAN_IDX_PARALLEL_PHASE:
> +			*val = 0;
> +			*val2 = AD9910_PI_NANORAD >> 15;
> +			return IIO_VAL_INT_PLUS_NANO;
> +		case AD9910_CHAN_IDX_PARALLEL_FREQ:
> +			tmp32 = FIELD_GET(AD9910_CFR2_FM_GAIN_MSK,
> +					  st->reg[AD9910_REG_CFR2].val32);
> +			tmp64 = st->data.sysclk_freq_hz << tmp32;

[Severity: Medium]
Will this 32-bit shift overflow before being assigned to the 64-bit tmp64?

Since sysclk_freq_hz is typically around 1 GHz and tmp32 can be up to 15,
this operation appears to be strictly 32-bit and could wrap around, resulting
in a corrupted readback.

> +			tmp64 = ad9910_rational_scale(tmp64, NANO, BIT_ULL(32));
> +			*val = div_s64_rem(tmp64, NANO, val2);
> +			return IIO_VAL_INT_PLUS_NANO;
> +		case AD9910_CHAN_IDX_PARALLEL_POLAR_AMP:

[ ... ]

> +	case IIO_CHAN_INFO_OFFSET:
> +		switch (chan->address) {
> +		case AD9910_CHAN_IDX_PARALLEL_FREQ:
> +			tmp64 = (u64)st->reg[AD9910_REG_FTW].val32 * MICRO;
> +			tmp64 >>= FIELD_GET(AD9910_CFR2_FM_GAIN_MSK,
> +					    st->reg[AD9910_REG_CFR2].val32);
> +			*val = div_s64_rem(tmp64, MICRO, val2);

[Severity: Medium]
Does this operation overflow the signed 32-bit pointer val?

The Frequency Tuning Word (FTW) is an unsigned 32-bit register capable of
holding values up to 4,294,967,295. If the offset exceeds INT_MAX, the
assignment could overflow, leading to a negative value on readback.

> +			return IIO_VAL_INT_PLUS_MICRO;
> +		case AD9910_CHAN_IDX_PARALLEL_POLAR_AMP:

[ ... ]

> @@ -737,12 +848,63 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
>  	case IIO_CHAN_INFO_SAMP_FREQ:
>  		return ad9910_set_sysclk_freq(st, val, true);
>  	case IIO_CHAN_INFO_SCALE:
> -		if (val != 0 || val2 < 0)
> +		switch (chan->address) {
> +		case AD9910_CHAN_IDX_PHY:
> +			if (val != 0 || val2 < 0)
> +				return -EINVAL;
> +
> +			tmp32 = DIV_U64_ROUND_CLOSEST((u64)val2 << 14,
> +						      AD9910_NANO_MILLIAMP_PER_MICROAMP);
> +			return ad9910_set_dac_current(st, tmp32, true);
> +		case AD9910_CHAN_IDX_PARALLEL_FREQ:
> +			if (val < 0 || val2 < 0)
> +				return -EINVAL;
> +
> +			tmp64 = ad9910_rational_scale((u64)val * NANO + val2, BIT_ULL(32),
> +						      (u64)st->data.sysclk_freq_hz * NANO);

[Severity: High]
This is a pre-existing issue, but can sysclk_freq_hz be zero here? If the
reference clock returns 0 from clk_get_rate(), the driver initializes
sysclk_freq_hz to 0 without validation.

If sysclk_freq_hz is 0, the denominator passed to ad9910_rational_scale()
is 0:

drivers/iio/frequency/ad9910.c:ad9910_rational_scale() {
    return mul_u64_add_u64_div_u64(input, scale, reference >> 1, reference);
}

This forwards the zero denominator into mul_u64_add_u64_div_u64(), leading
to a kernel panic from division by zero, which is now triggerable from user
space via the newly added parallel frequency scale attribute.

> +			tmp64 = roundup_pow_of_two(max(tmp64, 1ULL));

[Severity: Medium]
Will roundup_pow_of_two() truncate the 64-bit tmp64 calculation on 32-bit
architectures?

The roundup_pow_of_two() macro explicitly casts its argument to unsigned
long. On 32-bit systems, this will drop the upper 32 bits, causing the scale
input to be incorrectly configured.

> +			tmp32 = min_t(u32, ilog2(tmp64), FIELD_MAX(AD9910_CFR2_FM_GAIN_MSK));
> +			tmp32 = FIELD_PREP(AD9910_CFR2_FM_GAIN_MSK, tmp32);
> +			return ad9910_reg32_update(st, AD9910_REG_CFR2,
> +						   AD9910_CFR2_FM_GAIN_MSK,
> +						   tmp32, true);
> +		default:
> +			return -EINVAL;
> +		}
> +	case IIO_CHAN_INFO_OFFSET:
> +		if (val < 0 || val2 < 0)
>  			return -EINVAL;
>  
> -		tmp32 = DIV_U64_ROUND_CLOSEST((u64)val2 << 14,
> -					      AD9910_NANO_MILLIAMP_PER_MICROAMP);
> -		return ad9910_set_dac_current(st, tmp32, true);
> +		switch (chan->address) {
> +		case AD9910_CHAN_IDX_PARALLEL_FREQ:
> +			tmp64 = (u64)val * MICRO + val2;

[Severity: Medium]
Is it possible to set the Frequency Tuning Word to its full unsigned 32-bit
range?

The IIO subsystem's parser restricts the val parameter to INT_MAX. This
limitation seems to prevent users from accessing or configuring the upper
half of the device's tuning word range when FM_GAIN is 0.

> +			tmp64 <<= FIELD_GET(AD9910_CFR2_FM_GAIN_MSK,
> +					    st->reg[AD9910_REG_CFR2].val32);

[Severity: Medium]
Can this unbounded left shift trigger a 64-bit overflow?

If val is a large value up to INT_MAX, tmp64 can be around 5.63e14. Shifting
this left by an FM_GAIN of 15 would yield a value greater than UINT64_MAX,
which wraps around to a small positive value.

Because the wrapped value is small, it would bypass the subsequent U32_MAX
cap check below, resulting in arbitrary wrapped-around garbage being written
to the hardware.

> +			tmp64 = min_t(u64, DIV_U64_ROUND_CLOSEST(tmp64, MICRO),
> +				      U32_MAX);
> +			return ad9910_reg32_write(st, AD9910_REG_FTW, tmp64, true);

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260618-ad9910-iio-driver-v6-0-79125ffbe430@analog.com?part=10

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 11/16] iio: frequency: ad9910: add digital ramp generator support
  2026-06-18 13:27 ` [PATCH v6 11/16] iio: frequency: ad9910: add digital ramp generator support Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:42   ` sashiko-bot
  0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-06-18 13:42 UTC (permalink / raw)
  To: Rodrigo Alencar via B4 Relay; +Cc: devicetree, robh, conor+dt

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Negative values written to DRG_RAMP_UP or DRG_RAMP_DOWN raw attributes are silently converted and clamped to U32_MAX instead of returning an error.
--

commit add95ac7476587b7d5eda18e659f33df4cc8cb31
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>

iio: frequency: ad9910: add digital ramp generator support

This patch adds support for Digital Ramp Generator channels to the AD9910
driver. It includes configuration for destination selection, dwell mode,
limits, step sizes, and step rates.

> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> index 262702b62738d..3fe97aa887c3a 100644
> --- a/drivers/iio/frequency/ad9910.c
> +++ b/drivers/iio/frequency/ad9910.c

[ ... ]

> @@ -831,22 +1108,62 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
>  			return -EINVAL;
>  		}
>  	case IIO_CHAN_INFO_RAW:
> -		if (val < 0)
> -			return -EINVAL;
> -
>  		switch (chan->channel) {
>  		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
> +			if (val < 0)
> +				return -EINVAL;
> +
>  			tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
>  			tmp64 = FIELD_PREP(AD9910_PROFILE_ST_ASF_MSK,
>  					   min_t(u64, val, AD9910_ASF_MAX));
>  			return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
>  						   AD9910_PROFILE_ST_ASF_MSK,
>  						   tmp64, true);
> +		case AD9910_CHANNEL_DRG_RAMP_UP:
> +			tmp64 = iio_val_s64_compose(val, val2);
> +			tmp64 = min_t(u64, tmp64, U32_MAX);

[Severity: Medium]
When a negative value is written to the DRG_RAMP_UP or DRG_RAMP_DOWN raw
attributes, does this bypass error checking?

Since iio_val_s64_compose() returns an s64, a negative input will be
implicitly cast to u64 when assigned to tmp64. This causes it to wrap to a
large positive number, which min_t() will then clamp to U32_MAX rather than
rejecting the invalid negative input.

Should we check for negative values before calling iio_val_s64_compose(),
similar to what is done for the profile channels?

> +			tmp64 = FIELD_PREP(AD9910_DRG_LIMIT_UPPER_MSK, tmp64);
> +			return ad9910_reg64_update(st, AD9910_REG_DRG_LIMIT,
> +						   AD9910_DRG_LIMIT_UPPER_MSK,
> +						   tmp64, true);
> +		case AD9910_CHANNEL_DRG_RAMP_DOWN:
> +			tmp64 = iio_val_s64_compose(val, val2);
> +			tmp64 = min_t(u64, tmp64, U32_MAX);
> +			tmp64 = FIELD_PREP(AD9910_DRG_LIMIT_LOWER_MSK, tmp64);
> +			return ad9910_reg64_update(st, AD9910_REG_DRG_LIMIT,
> +						   AD9910_DRG_LIMIT_LOWER_MSK,
> +						   tmp64, true);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260618-ad9910-iio-driver-v6-0-79125ffbe430@analog.com?part=11

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 12/16] iio: frequency: ad9910: add RAM mode support
  2026-06-18 13:27 ` [PATCH v6 12/16] iio: frequency: ad9910: add RAM mode support Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:43   ` sashiko-bot
  0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-06-18 13:43 UTC (permalink / raw)
  To: Rodrigo Alencar via B4 Relay; +Cc: conor+dt, robh, devicetree

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Hardware and software profile states become permanently desynchronized if an error occurs while enabling or disabling the RAM channel.
- [High] Firmware upload cancellation requests can be silently ignored due to unconditionally resetting the cancel flag.
- [Low] Selecting the `FW_UPLOAD` boolean config symbol from a tristate driver causes an unmet dependency warning when built as a module.
--

commit 75d72698ebee0a7f4f951b2be1d393940055952b
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>

iio: frequency: ad9910: add RAM mode support

Add RAM control channel, which includes RAM data loading via firmware upload
interface and per-profile configuration.

> diff --git a/drivers/iio/frequency/Kconfig b/drivers/iio/frequency/Kconfig
> index 6033f9155b4f7..994d199080ebf 100644
> --- a/drivers/iio/frequency/Kconfig
> +++ b/drivers/iio/frequency/Kconfig
> @@ -29,6 +29,9 @@ config AD9910
>  	tristate "Analog Devices AD9910 Direct Digital Synthesizer"
>  	depends on SPI
>  	depends on GPIOLIB
> +	select CRC32
> +	select FW_LOADER
> +	select FW_UPLOAD

[Severity: Low]
Does selecting FW_UPLOAD from a tristate driver trigger an unmet dependency
warning when built as a module?

Because FW_UPLOAD is a boolean symbol that depends on FW_LOADER, and
FW_LOADER can resolve to 'm', selecting FW_UPLOAD forces it to 'y'. Could
this be avoided by using "depends on FW_UPLOAD" instead?

> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> index 3fe97aa887c3a..c4e179dda715e 100644
> --- a/drivers/iio/frequency/ad9910.c
> +++ b/drivers/iio/frequency/ad9910.c
[ ... ]
> @@ -1066,6 +1167,41 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
>  						   AD9910_CFR2_DRG_DEST_MSK |
>  						   AD9910_CFR2_DRG_ENABLE_MSK,
>  						   tmp32, true);
> +		case AD9910_CHANNEL_RAM:
> +			if (AD9910_RAM_ENABLED(st) == !!val)
> +				return 0;
> +
> +			/* swap profile configs */
> +			for (i = 0; i < AD9910_NUM_PROFILES; i++) {
> +				tmp64 = st->reg[AD9910_REG_PROFILE(i)].val64;
> +				ret = ad9910_reg64_write(st,
> +							 AD9910_REG_PROFILE(i),
> +							 st->reg_profile[i],
> +							 false);
> +				if (ret)
> +					break;
> +				st->reg_profile[i] = tmp64;
> +			}
> +
> +			if (ret) {
> +				/*
> +				 * After the write failure, profiles 0..i-1 were
> +				 * already swapped in SW, but Hw registers are
> +				 * still pending an IO update, so swap them back
> +				 * in SW to keep the state consistent.
> +				 */
> +				while (i--) {
> +					tmp64 = st->reg[AD9910_REG_PROFILE(i)].val64;
> +					st->reg[AD9910_REG_PROFILE(i)].val64 = st->reg_profile[i];
> +					st->reg_profile[i] = tmp64;
> +				}
> +				return ret;
> +			}
> +
> +			tmp32 = FIELD_PREP(AD9910_CFR1_RAM_ENABLE_MSK, !!val);
> +			return ad9910_reg32_update(st, AD9910_REG_CFR1,
> +						   AD9910_CFR1_RAM_ENABLE_MSK,
> +						   tmp32, true);

[Severity: High]
Can the hardware and software states become permanently desynchronized if an
error occurs during this transition?

If a write fails midway through the profile loop, the rollback loop restores
the software cache but doesn't seem to rewrite the original values to the
hardware SPI buffers. This leaves the successfully written swapped profiles
still pending in the hardware. 

Additionally, if the entire profile swap completes but the subsequent
ad9910_reg32_update() for CFR1 fails, the function returns immediately
without any rollback.

Would the next unrelated operation that triggers an IO update unintentionally
commit these partially or completely swapped profiles to the active registers?

[ ... ]
> @@ -1335,6 +1506,126 @@ static int ad9910_read_label(struct iio_dev *indio_dev,
>  	return sysfs_emit(label, "%s\n", ad9910_channel_str[chan->address]);
>  }
>  
> +static enum fw_upload_err ad9910_ram_fwu_prepare(struct fw_upload *fw_upload,
> +						 const u8 *data, u32 size)
> +{
> +	struct ad9910_state *st = fw_upload->dd_handle;
> +	const struct ad9910_ram_fw *fw_data = (const struct ad9910_ram_fw *)data;
> +	size_t wcount, bcount;
> +
> +	if (size < sizeof(struct ad9910_ram_fw))
> +		return FW_UPLOAD_ERR_INVALID_SIZE;
> +
> +	if (get_unaligned_be32(&fw_data->magic) != AD9910_RAM_FW_MAGIC)
> +		return FW_UPLOAD_ERR_FW_INVALID;
> +
> +	if (get_unaligned_be16(&fw_data->version) != AD9910_RAM_FW_V1)
> +		return FW_UPLOAD_ERR_FW_INVALID;
> +
> +	wcount = get_unaligned_be16(&fw_data->wcount);
> +	bcount = size - sizeof(struct ad9910_ram_fw);
> +	if (wcount > AD9910_RAM_SIZE_MAX_WORDS ||
> +	    bcount != (wcount * AD9910_RAM_WORD_SIZE))
> +		return FW_UPLOAD_ERR_INVALID_SIZE;
> +
> +	bcount += sizeof(fw_data->cfr1) + sizeof(fw_data->profiles);
> +	if (crc32(0, &fw_data->cfr1, bcount) != get_unaligned_be32(&fw_data->crc))
> +		return FW_UPLOAD_ERR_FW_INVALID;
> +
> +	guard(mutex)(&st->lock);
> +	st->ram_fwu_cancel = false;

[Severity: High]
Does unconditionally clearing the cancellation flag here introduce a race
condition where cancellations requested prior to the worker thread starting
are lost?

If a user cancels the upload via sysfs immediately after triggering it, the
framework calls ad9910_ram_fwu_cancel(), which correctly sets
st->ram_fwu_cancel = true.

However, when the background worker thread begins and calls
ad9910_ram_fwu_prepare(), it will overwrite the cancellation state with
false. Could this allow the firmware upload to proceed and flash the device
instead of aborting?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260618-ad9910-iio-driver-v6-0-79125ffbe430@analog.com?part=12

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 15/16] iio: ABI: add docs for ad9910 sysfs entries
  2026-06-18 13:27 ` [PATCH v6 15/16] iio: ABI: add docs for ad9910 sysfs entries Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:44   ` sashiko-bot
  0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-06-18 13:44 UTC (permalink / raw)
  To: Rodrigo Alencar via B4 Relay; +Cc: conor+dt, robh, devicetree

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The device tree binding defines the hardware as a clock provider but omits `#clock-cells` from the `required` list.
- [Low] The newly added ABI documentation file is missing the mandatory 'Date:' field and has empty 'KernelVersion:' fields.
--

commit 87a63b1a571e4fe2e7cfbfd552bf448b474a11d0
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>

iio: ABI: add docs for ad9910 sysfs entries

Add custom ABI documentation file for the DDS AD9910 with sysfs entries to
control some parameters from the Digital Ramp Generator and OSK engine.

> diff --git a/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910 b/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
> new file mode 100644
> index 0000000000000..a54afeb643029
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
> @@ -0,0 +1,27 @@
> +What:		/sys/bus/iio/devices/iio:deviceX/out_altcurrentY_dwell_en
> +KernelVersion:
> +Contact:	linux-iio@vger.kernel.org

[Severity: Low]
Does this new ABI documentation file omit the mandatory 'Date:' field?

According to Documentation/ABI/README, every file in the ABI directories
must contain a 'Date:' field specifying when the interface was created.
Additionally, the 'KernelVersion:' fields appear to be left empty, which
might trip automated ABI parsers.

[ ... ]

While reviewing this commit, I also noticed a related issue in the device
tree binding file introduced earlier in this series:

Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml:
> required:
>   - compatible
>   - reg
>   - clocks
>   - dvdd-io33-supply
>   - avdd33-supply
>   - dvdd18-supply
>   - avdd18-supply

[Severity: Medium]
Should '#clock-cells' be included in this required block?

Since the device tree binding defines the hardware as a clock provider with
'#clock-cells' and 'clock-output-names', omitting it from the required list
could allow incomplete device tree nodes to pass schema validation. This
might cause dependent subsystems or drivers to fail unexpectedly at runtime
when they attempt to use the undocumented functionality.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260618-ad9910-iio-driver-v6-0-79125ffbe430@analog.com?part=15

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 14/16] iio: frequency: ad9910: show channel priority in debugfs
  2026-06-18 13:27 ` [PATCH v6 14/16] iio: frequency: ad9910: show channel priority in debugfs Rodrigo Alencar via B4 Relay
@ 2026-06-18 13:45   ` sashiko-bot
  0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-06-18 13:45 UTC (permalink / raw)
  To: Rodrigo Alencar via B4 Relay; +Cc: conor+dt, devicetree, robh

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The parallel data port logic in the debugfs source attributes is dead code because the driver does not configure IIO buffers and omits `.scan_index` for the parallel channels.
--

commit 1430aa5467e635315d439ae21891376a98143557
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>

iio: frequency: ad9910: show channel priority in debugfs

Expose frequency_source, phase_source and amplitude_source attributes in
debugfs. The implementation follows the priority table found in the
datasheet.

> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> index 890499f67bd56..b39eeb8d9cd45 100644
> --- a/drivers/iio/frequency/ad9910.c
> +++ b/drivers/iio/frequency/ad9910.c
[ ... ]
> @@ -1960,6 +1974,158 @@ static int ad9910_setup(struct device *dev, struct ad9910_state *st,
>  	return ad9910_io_update(st);
>  }
>  
> +static inline const char *ad9910_frequency_source_get(struct iio_dev *indio_dev)
> +{
[ ... ]
> +	/* Parallel data port enabled and data destination is frequency */
> +	mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
> +			    st->reg[AD9910_REG_CFR2].val32);
> +	if (mode_en && indio_dev->active_scan_mask &&
> +	    test_bit(AD9910_SCAN_IDX_FREQ, indio_dev->active_scan_mask))
> +		return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_FREQ];

[Severity: Medium]
Is this logic effectively dead code?

The driver configures INDIO_DIRECT_MODE without setting up any IIO buffers,
so the IIO core will never allocate active_scan_mask.  This means
indio_dev->active_scan_mask will permanently evaluate to NULL and the
condition will evaluate to false.

Furthermore, even if buffers were enabled, the channel specifications for
the parallel channels do not populate the .scan_index field, so the bits
would never be set by the IIO core anyway:

drivers/iio/frequency/ad9910.c:ad9910_channels[] = {
	...
	[AD9910_CHAN_IDX_PARALLEL_FREQ] = {
		.type = IIO_FREQUENCY,
		.indexed = 1,
		.output = 1,
		.channel = AD9910_CHANNEL_PARALLEL,
		.address = AD9910_CHAN_IDX_PARALLEL_FREQ,
	...
}

Could this debugfs output silently fail to report the parallel data port and
fall through to incorrect fallback states?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260618-ad9910-iio-driver-v6-0-79125ffbe430@analog.com?part=14

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 05/16] iio: core: support 64-bit register through debugfs
  2026-06-18 13:27 ` [PATCH v6 05/16] iio: core: support 64-bit register through debugfs Rodrigo Alencar via B4 Relay
@ 2026-06-18 14:45   ` Nuno Sá
  0 siblings, 0 replies; 28+ messages in thread
From: Nuno Sá @ 2026-06-18 14:45 UTC (permalink / raw)
  To: rodrigo.alencar
  Cc: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening,
	Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva

On Thu, Jun 18, 2026 at 02:27:21PM +0100, Rodrigo Alencar via B4 Relay wrote:
> From: Rodrigo Alencar <rodrigo.alencar@analog.com>
> 
> Add debugfs_reg64_access function pointer field into iio_info and modify
> file operation callbacks to favor 64-bit variant when it is available.
> 
> Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
> ---
>  drivers/iio/industrialio-core.c | 33 ++++++++++++++++++++++++---------
>  include/linux/iio/iio-opaque.h  |  2 +-
>  include/linux/iio/iio.h         |  4 ++++
>  3 files changed, 29 insertions(+), 10 deletions(-)
> 
> @@ -471,7 +485,8 @@ static void iio_device_register_debugfs(struct iio_dev *indio_dev)
>  {
>  	struct iio_dev_opaque *iio_dev_opaque;
>  
> -	if (indio_dev->info->debugfs_reg_access == NULL)
> +	if (!indio_dev->info->debugfs_reg_access &&
> +	    !indio_dev->info->debugfs_reg64_access)
>  		return;

Not really that important but should dev_warn() in case someone gives
both callbacks? Can't use both anyways.

(We now have agentic help reviewing the code so maybe even if someone
does it for some reason it won't pass review :))

- Nuno Sá
>  
>  	if (!iio_debugfs_dentry)
> diff --git a/include/linux/iio/iio-opaque.h b/include/linux/iio/iio-opaque.h
> index b87841a355f8..98330385e08d 100644
> --- a/include/linux/iio/iio-opaque.h
> +++ b/include/linux/iio/iio-opaque.h
> @@ -73,7 +73,7 @@ struct iio_dev_opaque {
>  #if defined(CONFIG_DEBUG_FS)
>  	struct dentry			*debugfs_dentry;
>  	unsigned int			cached_reg_addr;
> -	char				read_buf[20];
> +	char				read_buf[24];
>  	unsigned int			read_buf_len;
>  #endif
>  };
> diff --git a/include/linux/iio/iio.h b/include/linux/iio/iio.h
> index 711c00f67371..1c7d12af22da 100644
> --- a/include/linux/iio/iio.h
> +++ b/include/linux/iio/iio.h
> @@ -484,6 +484,7 @@ struct iio_trigger; /* forward declaration */
>   * @update_scan_mode:	function to configure device and scan buffer when
>   *			channels have changed
>   * @debugfs_reg_access:	function to read or write register value of device
> + * @debugfs_reg64_access: function to read or write 64-bit register value of device
>   * @fwnode_xlate:	fwnode based function pointer to obtain channel specifier index.
>   * @hwfifo_set_watermark: function pointer to set the current hardware
>   *			fifo watermark level; see hwfifo_* entries in
> @@ -572,6 +573,9 @@ struct iio_info {
>  	int (*debugfs_reg_access)(struct iio_dev *indio_dev,
>  				  unsigned int reg, unsigned int writeval,
>  				  unsigned int *readval);
> +	int (*debugfs_reg64_access)(struct iio_dev *indio_dev,
> +				    unsigned int reg, u64 writeval,
> +				    u64 *readval);
>  	int (*fwnode_xlate)(struct iio_dev *indio_dev,
>  			    const struct fwnode_reference_args *iiospec);
>  	int (*hwfifo_set_watermark)(struct iio_dev *indio_dev, unsigned int val);
> 
> -- 
> 2.43.0
> 
> 

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 06/16] iio: core: create local __iio_chan_prefix_emit() for reuse
  2026-06-18 13:27 ` [PATCH v6 06/16] iio: core: create local __iio_chan_prefix_emit() for reuse Rodrigo Alencar via B4 Relay
@ 2026-06-18 15:06   ` Nuno Sá
  2026-06-18 16:14     ` Rodrigo Alencar
  0 siblings, 1 reply; 28+ messages in thread
From: Nuno Sá @ 2026-06-18 15:06 UTC (permalink / raw)
  To: rodrigo.alencar
  Cc: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening,
	Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva

On Thu, Jun 18, 2026 at 02:27:22PM +0100, Rodrigo Alencar via B4 Relay wrote:
> From: Rodrigo Alencar <rodrigo.alencar@analog.com>
> 
> Move logic to create a channel prefix for naming attribute files into a
> separate __iio_chan_prefix_emit() function for reuse.
> 
> Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
> ---
>  drivers/iio/industrialio-core.c | 167 ++++++++++++++++------------------------
>  1 file changed, 68 insertions(+), 99 deletions(-)
> 
> diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
> index 03019bf9327b..9373006235c8 100644
> --- a/drivers/iio/industrialio-core.c
> +++ b/drivers/iio/industrialio-core.c
> @@ -26,6 +26,7 @@
>  #include <linux/property.h>
>  #include <linux/sched.h>
>  #include <linux/slab.h>
> +#include <linux/sprintf.h>
>  #include <linux/wait.h>
>  
>  #include <linux/iio/buffer.h>
> @@ -199,6 +200,64 @@ static const char * const iio_chan_info_postfix[] = {
>  	[IIO_CHAN_INFO_CONVDELAY] = "convdelay",
>  	[IIO_CHAN_INFO_POWERFACTOR] = "powerfactor",
>  };
> +
> +static int __iio_chan_prefix_emit(const struct iio_chan_spec *chan,
> +				  enum iio_shared_by shared_by,
> +				  char *buf, size_t len)
> +{
> +	const char *dir = iio_direction[chan->output];
> +	const char *type = iio_chan_type_name_spec[chan->type];
> +	int n = 0;
> +
> +	switch (shared_by) {
> +	case IIO_SHARED_BY_ALL:
> +		buf[0] = '\0'; /* empty channel prefix */
> +		break;
> +	case IIO_SHARED_BY_DIR:
> +		n = scnprintf(buf, len, "%s", dir);
> +		break;
> +	case IIO_SHARED_BY_TYPE:
> +		n = scnprintf(buf, len, "%s_%s", dir, type);
> +		if (chan->differential)
> +			n += scnprintf(buf + n, len - n, "-%s", type);
> +		break;
> +	case IIO_SEPARATE:
> +		if (chan->indexed) {
> +			n = scnprintf(buf, len, "%s_%s%d", dir, type,
> +				      chan->channel);
> +			if (chan->differential)
> +				n += scnprintf(buf + n, len - n, "-%s%d", type,
> +					       chan->channel2);
> +		} else {
> +			if (chan->differential) {
> +				WARN(1, "Differential channels must be indexed\n");
> +				return -EINVAL;
> +			}
> +			n = scnprintf(buf, len, "%s_%s", dir, type);
> +		}
> +
> +		if (chan->modified) {
> +			if (chan->differential) {
> +				WARN(1, "Differential channels can not have modifier\n");
> +				return -EINVAL;

WARN() looks too much to me. dev_error() as we're treating it as such. I
guess you don't want to pass struct device but not really an issue IMHO.


> +			}
> +			n += scnprintf(buf + n, len - n, "_%s",
> +				       iio_modifier_names[chan->channel2]);
> +		}
> +
> +		if (chan->extend_name)
> +			n += scnprintf(buf + n, len - n, "_%s", chan->extend_name);
> +		break;
> +	}
> +
> +	if (n > 0 && n < len - 1) { /* prefix termination if not empty */
> +		buf[n++] = '_';
> +		buf[n] = '\0';
> +	}
> +

Can't we handle the above in the caller on kasprintf()? Then we could
simplify and return in place.

> +	return n;
> +}
> +
>  /**
>   * iio_device_id() - query the unique ID for the device
>   * @indio_dev:		Device structure whose ID is being queried
> @@ -1100,106 +1159,19 @@ int __iio_device_attr_init(struct device_attribute *dev_attr,
>  						size_t len),
>  			   enum iio_shared_by shared_by)
>  {
> -	int ret = 0;
> -	char *name = NULL;
> -	char *full_postfix;
> +	char prefix[NAME_MAX + 1];
> +	int ret;
>  
>  	sysfs_attr_init(&dev_attr->attr);
>  
> -	/* Build up postfix of <extend_name>_<modifier>_postfix */
> -	if (chan->modified && (shared_by == IIO_SEPARATE)) {
> -		if (chan->extend_name)
> -			full_postfix = kasprintf(GFP_KERNEL, "%s_%s_%s",
> -						 iio_modifier_names[chan->channel2],
> -						 chan->extend_name,
> -						 postfix);
> -		else
> -			full_postfix = kasprintf(GFP_KERNEL, "%s_%s",
> -						 iio_modifier_names[chan->channel2],
> -						 postfix);
> -	} else {
> -		if (chan->extend_name == NULL || shared_by != IIO_SEPARATE)
> -			full_postfix = kstrdup(postfix, GFP_KERNEL);
> -		else
> -			full_postfix = kasprintf(GFP_KERNEL,
> -						 "%s_%s",
> -						 chan->extend_name,
> -						 postfix);
> -	}
> -	if (full_postfix == NULL)
> +	ret = __iio_chan_prefix_emit(chan, shared_by, prefix, sizeof(prefix));
> +	if (ret < 0)
> +		return ret;
> +
> +	dev_attr->attr.name = kasprintf(GFP_KERNEL, "%s%s", prefix, postfix);
> +	if (!dev_attr->attr.name)
>  		return -ENOMEM;

I don't oppose the change. Looks like a nice cleanup. But bear in mind
this very sensible as any subtle mistake means ABI breakage.

- Nuno Sá

>  
> -	if (chan->differential) { /* Differential can not have modifier */
> -		switch (shared_by) {
> -		case IIO_SHARED_BY_ALL:
> -			name = kasprintf(GFP_KERNEL, "%s", full_postfix);
> -			break;
> -		case IIO_SHARED_BY_DIR:
> -			name = kasprintf(GFP_KERNEL, "%s_%s",
> -						iio_direction[chan->output],
> -						full_postfix);
> -			break;
> -		case IIO_SHARED_BY_TYPE:
> -			name = kasprintf(GFP_KERNEL, "%s_%s-%s_%s",
> -					    iio_direction[chan->output],
> -					    iio_chan_type_name_spec[chan->type],
> -					    iio_chan_type_name_spec[chan->type],
> -					    full_postfix);
> -			break;
> -		case IIO_SEPARATE:
> -			if (!chan->indexed) {
> -				WARN(1, "Differential channels must be indexed\n");
> -				ret = -EINVAL;
> -				goto error_free_full_postfix;
> -			}
> -			name = kasprintf(GFP_KERNEL,
> -					    "%s_%s%d-%s%d_%s",
> -					    iio_direction[chan->output],
> -					    iio_chan_type_name_spec[chan->type],
> -					    chan->channel,
> -					    iio_chan_type_name_spec[chan->type],
> -					    chan->channel2,
> -					    full_postfix);
> -			break;
> -		}
> -	} else { /* Single ended */
> -		switch (shared_by) {
> -		case IIO_SHARED_BY_ALL:
> -			name = kasprintf(GFP_KERNEL, "%s", full_postfix);
> -			break;
> -		case IIO_SHARED_BY_DIR:
> -			name = kasprintf(GFP_KERNEL, "%s_%s",
> -						iio_direction[chan->output],
> -						full_postfix);
> -			break;
> -		case IIO_SHARED_BY_TYPE:
> -			name = kasprintf(GFP_KERNEL, "%s_%s_%s",
> -					    iio_direction[chan->output],
> -					    iio_chan_type_name_spec[chan->type],
> -					    full_postfix);
> -			break;
> -
> -		case IIO_SEPARATE:
> -			if (chan->indexed)
> -				name = kasprintf(GFP_KERNEL, "%s_%s%d_%s",
> -						    iio_direction[chan->output],
> -						    iio_chan_type_name_spec[chan->type],
> -						    chan->channel,
> -						    full_postfix);
> -			else
> -				name = kasprintf(GFP_KERNEL, "%s_%s_%s",
> -						    iio_direction[chan->output],
> -						    iio_chan_type_name_spec[chan->type],
> -						    full_postfix);
> -			break;
> -		}
> -	}
> -	if (name == NULL) {
> -		ret = -ENOMEM;
> -		goto error_free_full_postfix;
> -	}
> -	dev_attr->attr.name = name;
> -
>  	if (readfunc) {
>  		dev_attr->attr.mode |= 0444;
>  		dev_attr->show = readfunc;
> @@ -1210,10 +1182,7 @@ int __iio_device_attr_init(struct device_attribute *dev_attr,
>  		dev_attr->store = writefunc;
>  	}
>  
> -error_free_full_postfix:
> -	kfree(full_postfix);
> -
> -	return ret;
> +	return 0;
>  }
>  
>  static void __iio_device_attr_deinit(struct device_attribute *dev_attr)
> 
> -- 
> 2.43.0
> 
> 

^ permalink raw reply	[flat|nested] 28+ messages in thread

* Re: [PATCH v6 06/16] iio: core: create local __iio_chan_prefix_emit() for reuse
  2026-06-18 15:06   ` Nuno Sá
@ 2026-06-18 16:14     ` Rodrigo Alencar
  0 siblings, 0 replies; 28+ messages in thread
From: Rodrigo Alencar @ 2026-06-18 16:14 UTC (permalink / raw)
  To: Nuno Sá, rodrigo.alencar
  Cc: linux-iio, devicetree, linux-kernel, linux-doc, linux-hardening,
	Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
	David Lechner, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	Kees Cook, Gustavo A. R. Silva

On 18/06/26 16:06, Nuno Sá wrote:
> On Thu, Jun 18, 2026 at 02:27:22PM +0100, Rodrigo Alencar via B4 Relay wrote:
> > From: Rodrigo Alencar <rodrigo.alencar@analog.com>
> > 
> > Move logic to create a channel prefix for naming attribute files into a
> > separate __iio_chan_prefix_emit() function for reuse.

...

> > +static int __iio_chan_prefix_emit(const struct iio_chan_spec *chan,
> > +				  enum iio_shared_by shared_by,
> > +				  char *buf, size_t len)
> > +{
> > +	const char *dir = iio_direction[chan->output];
> > +	const char *type = iio_chan_type_name_spec[chan->type];
> > +	int n = 0;
> > +
> > +	switch (shared_by) {
> > +	case IIO_SHARED_BY_ALL:
> > +		buf[0] = '\0'; /* empty channel prefix */
> > +		break;
> > +	case IIO_SHARED_BY_DIR:
> > +		n = scnprintf(buf, len, "%s", dir);
> > +		break;
> > +	case IIO_SHARED_BY_TYPE:
> > +		n = scnprintf(buf, len, "%s_%s", dir, type);
> > +		if (chan->differential)
> > +			n += scnprintf(buf + n, len - n, "-%s", type);
> > +		break;
> > +	case IIO_SEPARATE:
> > +		if (chan->indexed) {
> > +			n = scnprintf(buf, len, "%s_%s%d", dir, type,
> > +				      chan->channel);
> > +			if (chan->differential)
> > +				n += scnprintf(buf + n, len - n, "-%s%d", type,
> > +					       chan->channel2);
> > +		} else {
> > +			if (chan->differential) {
> > +				WARN(1, "Differential channels must be indexed\n");
> > +				return -EINVAL;
> > +			}
> > +			n = scnprintf(buf, len, "%s_%s", dir, type);
> > +		}
> > +
> > +		if (chan->modified) {
> > +			if (chan->differential) {
> > +				WARN(1, "Differential channels can not have modifier\n");
> > +				return -EINVAL;
> 
> WARN() looks too much to me. dev_error() as we're treating it as such. I
> guess you don't want to pass struct device but not really an issue IMHO.

__iio_device_attr_init() also used WARN(), probably because it didnt have
access to a dev pointer. It would not be a problem to add an extra param.
 
> 
> > +			}
> > +			n += scnprintf(buf + n, len - n, "_%s",
> > +				       iio_modifier_names[chan->channel2]);
> > +		}
> > +
> > +		if (chan->extend_name)
> > +			n += scnprintf(buf + n, len - n, "_%s", chan->extend_name);
> > +		break;
> > +	}
> > +
> > +	if (n > 0 && n < len - 1) { /* prefix termination if not empty */
> > +		buf[n++] = '_';
> > +		buf[n] = '\0';
> > +	}
> > +
> 
> Can't we handle the above in the caller on kasprintf()? Then we could
> simplify and return in place.

I felt like doing this here would get a cleaner logic in the caller, which
would have to add the '_' conditionally.

> 
> > +	return n;
> > +}
> > +
> >  /**
> >   * iio_device_id() - query the unique ID for the device
> >   * @indio_dev:		Device structure whose ID is being queried
> > @@ -1100,106 +1159,19 @@ int __iio_device_attr_init(struct device_attribute *dev_attr,
> >  						size_t len),
> >  			   enum iio_shared_by shared_by)
> >  {
> > -	int ret = 0;
> > -	char *name = NULL;
> > -	char *full_postfix;
> > +	char prefix[NAME_MAX + 1];
> > +	int ret;
> >  
> >  	sysfs_attr_init(&dev_attr->attr);
> >  
> > -	/* Build up postfix of <extend_name>_<modifier>_postfix */
> > -	if (chan->modified && (shared_by == IIO_SEPARATE)) {
> > -		if (chan->extend_name)
> > -			full_postfix = kasprintf(GFP_KERNEL, "%s_%s_%s",
> > -						 iio_modifier_names[chan->channel2],
> > -						 chan->extend_name,
> > -						 postfix);
> > -		else
> > -			full_postfix = kasprintf(GFP_KERNEL, "%s_%s",
> > -						 iio_modifier_names[chan->channel2],
> > -						 postfix);
> > -	} else {
> > -		if (chan->extend_name == NULL || shared_by != IIO_SEPARATE)
> > -			full_postfix = kstrdup(postfix, GFP_KERNEL);
> > -		else
> > -			full_postfix = kasprintf(GFP_KERNEL,
> > -						 "%s_%s",
> > -						 chan->extend_name,
> > -						 postfix);
> > -	}
> > -	if (full_postfix == NULL)
> > +	ret = __iio_chan_prefix_emit(chan, shared_by, prefix, sizeof(prefix));
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	dev_attr->attr.name = kasprintf(GFP_KERNEL, "%s%s", prefix, postfix);
> > +	if (!dev_attr->attr.name)
> >  		return -ENOMEM;
> 
> I don't oppose the change. Looks like a nice cleanup. But bear in mind
> this very sensible as any subtle mistake means ABI breakage.

Yes! I tried to be careful... this is dangerous stuff!

-- 
Kind regards,

Rodrigo Alencar

^ permalink raw reply	[flat|nested] 28+ messages in thread

end of thread, other threads:[~2026-06-18 16:14 UTC | newest]

Thread overview: 28+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-18 13:27 [PATCH v6 00/16] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
2026-06-18 13:27 ` [PATCH v6 01/16] iio: ABI: add attributes for altcurrent channels Rodrigo Alencar via B4 Relay
2026-06-18 13:27 ` [PATCH v6 02/16] iio: ABI: scale and offset for frequency/phase channels Rodrigo Alencar via B4 Relay
2026-06-18 13:27 ` [PATCH v6 03/16] iio: ABI: add parent entry for iio channels Rodrigo Alencar via B4 Relay
2026-06-18 13:27 ` [PATCH v6 04/16] iio: add IIO_FREQUENCY channel type Rodrigo Alencar via B4 Relay
2026-06-18 13:27 ` [PATCH v6 05/16] iio: core: support 64-bit register through debugfs Rodrigo Alencar via B4 Relay
2026-06-18 14:45   ` Nuno Sá
2026-06-18 13:27 ` [PATCH v6 06/16] iio: core: create local __iio_chan_prefix_emit() for reuse Rodrigo Alencar via B4 Relay
2026-06-18 15:06   ` Nuno Sá
2026-06-18 16:14     ` Rodrigo Alencar
2026-06-18 13:27 ` [PATCH v6 07/16] iio: core: add hierarchical channel relationships Rodrigo Alencar via B4 Relay
2026-06-18 13:33   ` sashiko-bot
2026-06-18 13:27 ` [PATCH v6 08/16] dt-bindings: iio: frequency: add ad9910 Rodrigo Alencar via B4 Relay
2026-06-18 13:35   ` sashiko-bot
2026-06-18 13:27 ` [PATCH v6 09/16] iio: frequency: ad9910: initial driver implementation Rodrigo Alencar via B4 Relay
2026-06-18 13:37   ` sashiko-bot
2026-06-18 13:27 ` [PATCH v6 10/16] iio: frequency: ad9910: add basic parallel port support Rodrigo Alencar via B4 Relay
2026-06-18 13:41   ` sashiko-bot
2026-06-18 13:27 ` [PATCH v6 11/16] iio: frequency: ad9910: add digital ramp generator support Rodrigo Alencar via B4 Relay
2026-06-18 13:42   ` sashiko-bot
2026-06-18 13:27 ` [PATCH v6 12/16] iio: frequency: ad9910: add RAM mode support Rodrigo Alencar via B4 Relay
2026-06-18 13:43   ` sashiko-bot
2026-06-18 13:27 ` [PATCH v6 13/16] iio: frequency: ad9910: add output shift keying support Rodrigo Alencar via B4 Relay
2026-06-18 13:27 ` [PATCH v6 14/16] iio: frequency: ad9910: show channel priority in debugfs Rodrigo Alencar via B4 Relay
2026-06-18 13:45   ` sashiko-bot
2026-06-18 13:27 ` [PATCH v6 15/16] iio: ABI: add docs for ad9910 sysfs entries Rodrigo Alencar via B4 Relay
2026-06-18 13:44   ` sashiko-bot
2026-06-18 13:27 ` [PATCH v6 16/16] docs: iio: add documentation for ad9910 driver Rodrigo Alencar via B4 Relay

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox