* [PATCH RFC v4 01/10] dt-bindings: iio: frequency: add ad9910
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
@ 2026-05-08 17:00 ` Rodrigo Alencar via B4 Relay
2026-05-08 22:02 ` sashiko-bot
2026-05-08 17:00 ` [PATCH RFC v4 02/10] iio: core: support 64-bit register through debugfs Rodrigo Alencar via B4 Relay
` (9 subsequent siblings)
10 siblings, 1 reply; 26+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-05-08 17:00 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 | 198 +++++++++++++++++++++
MAINTAINERS | 7 +
2 files changed, 205 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..3b76871630c9
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
@@ -0,0 +1,198 @@
+# 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:
+ oneOf:
+ - items:
+ - const: ref_clk
+ - 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
+ items:
+ - description:
+ Signal that indicates that Digital Ramp Generator has reached a limit.
+ - description:
+ Signal that indicates the end of a 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.
+
+ adi,dac-output-current-microamp:
+ minimum: 8640
+ maximum: 31590
+ default: 20070
+ description:
+ DAC full-scale output current in microamps.
+
+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 d6c3c7d22403..27183c31f3ac 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1638,6 +1638,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] 26+ messages in thread* Re: [PATCH RFC v4 01/10] dt-bindings: iio: frequency: add ad9910
2026-05-08 17:00 ` [PATCH RFC v4 01/10] dt-bindings: iio: frequency: add ad9910 Rodrigo Alencar via B4 Relay
@ 2026-05-08 22:02 ` sashiko-bot
0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-05-08 22:02 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay; +Cc: conor+dt, krzk+dt, robh, devicetree
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The binding defines `#clock-cells` indicating clock provider capabilities but fails to include it in the `required` list and the `examples` block.
- [Medium] The `lock-detect-gpios` dependency is written with an unquoted comma (`[ adi,pll-enable ]`), causing it to evaluate to two non-existent properties.
--
commit 02fb206b49b457eba593a7f4a35e17f11545fbf5
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.
> 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..3b76871630c90
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
[ ... ]
> +dependencies:
> + adi,charge-pump-current-microamp: [ 'adi,pll-enable' ]
> + adi,refclk-out-drive-strength: [ 'adi,pll-enable' ]
> + lock-detect-gpios: [ adi,pll-enable ]
Does this dependency evaluate to two non-existent properties instead of
"adi,pll-enable"? In YAML flow collections, an unquoted comma acts as an
element separator, so this gets parsed as a list containing "adi" and
"pll-enable".
Since neither property exists, this could cause device trees using
lock-detect-gpios to fail schema validation.
Could we enclose the property name in quotes (e.g., [ 'adi,pll-enable' ])
like the other dependencies?
> + interrupts: [ interrupt-names ]
> + clocks: [ clock-names ]
> + '#clock-cells': [ clock-output-names ]
> +
> +required:
> + - compatible
> + - reg
> + - clocks
> + - dvdd-io33-supply
> + - avdd33-supply
> + - dvdd18-supply
> + - avdd18-supply
Since the binding defines #clock-cells, indicating clock provider
capabilities, shouldn't it be included in this required list?
According to the dt-bindings guidelines, when hardware has provider
capabilities, the corresponding standard properties must be explicitly
required to ensure schema validation catches incomplete device tree nodes
before runtime.
[ ... ]
> +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";
[ ... ]
> + adi,pll-enable;
> + adi,charge-pump-current-microamp = <387>;
> + adi,refclk-out-drive-strength = "disabled";
Should #clock-cells also be added to this example block?
The dt-bindings guidelines state that all required properties must be
included in the examples section to pass schema validation.
> + };
> + };
> +...
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com?part=1
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH RFC v4 02/10] iio: core: support 64-bit register through debugfs
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
2026-05-08 17:00 ` [PATCH RFC v4 01/10] dt-bindings: iio: frequency: add ad9910 Rodrigo Alencar via B4 Relay
@ 2026-05-08 17:00 ` Rodrigo Alencar via B4 Relay
2026-05-08 22:20 ` sashiko-bot
2026-05-10 10:07 ` Andy Shevchenko
2026-05-08 17:00 ` [PATCH RFC v4 03/10] iio: frequency: ad9910: initial driver implementation Rodrigo Alencar via B4 Relay
` (8 subsequent siblings)
10 siblings, 2 replies; 26+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-05-08 17:00 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.h | 4 ++++
2 files changed, 28 insertions(+), 9 deletions(-)
diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
index bd6f4f9f4533..cb4e2ade25c0 100644
--- a/drivers/iio/industrialio-core.c
+++ b/drivers/iio/industrialio-core.c
@@ -386,6 +386,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)
@@ -393,9 +394,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;
@@ -403,7 +412,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", val64);
return simple_read_from_buffer(userbuf, count, ppos,
iio_dev_opaque->read_buf,
@@ -415,8 +424,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))
@@ -429,7 +439,7 @@ static ssize_t iio_debugfs_write_reg(struct file *file,
buf[ret] = '\0';
- ret = sscanf(buf, "%i %i", ®, &val);
+ ret = sscanf(buf, "%i %lli", ®, &val64);
switch (ret) {
case 1:
@@ -437,8 +447,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__);
@@ -469,7 +483,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.h b/include/linux/iio/iio.h
index 96b05c86c325..86d17ee69e05 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] 26+ messages in thread* Re: [PATCH RFC v4 02/10] iio: core: support 64-bit register through debugfs
2026-05-08 17:00 ` [PATCH RFC v4 02/10] iio: core: support 64-bit register through debugfs Rodrigo Alencar via B4 Relay
@ 2026-05-08 22:20 ` sashiko-bot
2026-05-10 10:07 ` Andy Shevchenko
1 sibling, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-05-08 22:20 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay; +Cc: devicetree, robh, krzk+dt, conor+dt
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Uninitialized stack memory read when file offset is non-zero
- [Medium] Missing newline character in debugfs read output
- [Medium] Silent truncation of 64-bit values for 32-bit registers
- [Low] Format specifier type mismatch in sscanf
--
commit 2208ef2b9ad567bd166f4ac44a53f42c899c021b
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>
iio: core: support 64-bit register through debugfs
This patch adds a debugfs_reg64_access function pointer to iio_info to
support 64-bit registers. It also updates the read and write debugfs file
operations to use the 64-bit variant when available, falling back to 32-bit.
> diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
> index bd6f4f9f45330..cb4e2ade25c0e 100644
> --- a/drivers/iio/industrialio-core.c
> +++ b/drivers/iio/industrialio-core.c
[ ... ]
> @@ -403,7 +412,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", val64);
Does this inadvertently drop the newline character?
[ ... ]
> @@ -429,7 +439,7 @@ static ssize_t iio_debugfs_write_reg(struct file *file,
>
> buf[ret] = '\0';
Could buf be read uninitialized here if *ppos > 0?
fs/libfs.c:simple_write_to_buffer() writes user data to buf + *ppos and
returns the number of bytes written:
res = copy_from_user(to + pos, from, count);
if (res == count)
return -EFAULT;
count -= res;
*ppos = pos + count;
return count;
If *ppos > 0, the first *ppos bytes of buf remain uninitialized. The
subsequent sscanf() reads starting from buf[0], parsing up to count bytes
of uninitialized kernel stack memory.
>
> - ret = sscanf(buf, "%i %i", ®, &val);
> + ret = sscanf(buf, "%i %lli", ®, &val64);
Can this trigger compiler warnings? val64 is of type u64, but %lli is the
specifier for a signed long long pointer.
>
> switch (ret) {
> case 1:
> @@ -437,8 +447,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);
Does this silently truncate 64-bit values for 32-bit registers?
If a user provides a 64-bit value but the driver only implements the 32-bit
debugfs_reg_access callback, val64 is passed directly as the unsigned int
argument. This discards the upper 32 bits without returning an error.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com?part=2
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH RFC v4 02/10] iio: core: support 64-bit register through debugfs
2026-05-08 17:00 ` [PATCH RFC v4 02/10] iio: core: support 64-bit register through debugfs Rodrigo Alencar via B4 Relay
2026-05-08 22:20 ` sashiko-bot
@ 2026-05-10 10:07 ` Andy Shevchenko
1 sibling, 0 replies; 26+ messages in thread
From: Andy Shevchenko @ 2026-05-10 10:07 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 Fri, May 08, 2026 at 06:00:18PM +0100, Rodrigo Alencar via B4 Relay wrote:
> Add debugfs_reg64_access function pointer field into iio_info and modify
> file operation callbacks to favor 64-bit variant when it is available.
To write a value with bit 63 set it will require negative number, right?
Isn't this counter intuitive and may lead to rejection of the (correct)
values?
--
With Best Regards,
Andy Shevchenko
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH RFC v4 03/10] iio: frequency: ad9910: initial driver implementation
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
2026-05-08 17:00 ` [PATCH RFC v4 01/10] dt-bindings: iio: frequency: add ad9910 Rodrigo Alencar via B4 Relay
2026-05-08 17:00 ` [PATCH RFC v4 02/10] iio: core: support 64-bit register through debugfs Rodrigo Alencar via B4 Relay
@ 2026-05-08 17:00 ` Rodrigo Alencar via B4 Relay
2026-05-08 22:51 ` sashiko-bot
2026-05-08 17:00 ` [PATCH RFC v4 04/10] iio: frequency: ad9910: add basic parallel port support Rodrigo Alencar via B4 Relay
` (7 subsequent siblings)
10 siblings, 1 reply; 26+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-05-08 17:00 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 | 1058 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 1078 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 27183c31f3ac..6a53b202a84d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1644,6 +1644,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 583cbdf4e8cd..180e74f62d11 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 70d0e0b70e80..39271dd209ca 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_ADF4350) += adf4350.o
obj-$(CONFIG_ADF4371) += adf4371.o
obj-$(CONFIG_ADF4377) += adf4377.o
diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
new file mode 100644
index 000000000000..c75f2ef178c2
--- /dev/null
+++ b/drivers/iio/frequency/ad9910.c
@@ -0,0 +1,1058 @@
+// 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 0x08070000
+#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 GENMASK(13, 0)
+#define AD9910_POW_MAX GENMASK(15, 0)
+#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 (3200 * 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 (1000 * HZ_PER_MHZ)
+
+#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_REFCLK_OUT_DRV_DISABLED 0
+
+#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
+
+#define AD9910_REFDIV2_MIN_FREQ_HZ (120 * HZ_PER_MHZ)
+#define AD9910_REFDIV2_MAX_FREQ_HZ (1900 * HZ_PER_MHZ)
+
+#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 = 101,
+ AD9910_CHANNEL_PROFILE_1 = 102,
+ AD9910_CHANNEL_PROFILE_2 = 103,
+ AD9910_CHANNEL_PROFILE_3 = 104,
+ AD9910_CHANNEL_PROFILE_4 = 105,
+ AD9910_CHANNEL_PROFILE_5 = 106,
+ AD9910_CHANNEL_PROFILE_6 = 107,
+ AD9910_CHANNEL_PROFILE_7 = 108,
+};
+
+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 dac_output_current;
+
+ 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);
+ udelay(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, bool update)
+{
+ u32 fsc_code;
+
+ /* FSC = (86.4 / Rset) * (1 + CODE/256) where Rset = 10k ohms */
+ fsc_code = DIV_ROUND_CLOSEST(st->data.dac_output_current, 90) - 96;
+ fsc_code &= 0xFF;
+
+ return ad9910_reg32_write(st, AD9910_REG_AUX_DAC, fsc_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;
+
+ 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, AD9910_PLL_MIN_N, AD9910_PLL_MAX_N);
+ sysclk_freq_hz = refclk_freq_hz * tmp32;
+
+ if (sysclk_freq_hz < AD9910_PLL_OUT_MIN_FREQ_HZ ||
+ sysclk_freq_hz > AD9910_PLL_OUT_MAX_FREQ_HZ) {
+ dev_err(dev,
+ "PLL output frequency %u Hz is out of range\n",
+ sysclk_freq_hz);
+ return -ERANGE;
+ }
+
+ 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, 1, 2);
+ sysclk_freq_hz = refclk_freq_hz / tmp32;
+ tmp32 = FIELD_PREP(AD9910_CFR3_REFCLK_DIV_BYPASS_MSK, tmp32 % 2);
+ ret = ad9910_reg32_update(st, AD9910_REG_CFR3,
+ 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;
+}
+
+#define AD9910_EXT_INFO_TMPL(_name, _ident, _shared, _fn_desc) { \
+ .name = _name, \
+ .read = ad9910_ ## _fn_desc ## _read, \
+ .write = ad9910_ ## _fn_desc ## _write, \
+ .private = _ident, \
+ .shared = _shared, \
+}
+
+#define AD9910_EXT_INFO(_name, _ident, _shared) \
+ AD9910_EXT_INFO_TMPL(_name, _ident, _shared, ext_info)
+
+static const struct iio_chan_spec_ext_info ad9910_phy_ext_info[] = {
+ AD9910_EXT_INFO("powerdown", AD9910_POWERDOWN, IIO_SEPARATE),
+ { }
+};
+
+#define AD9910_PROFILE_CHAN(idx) { \
+ .type = IIO_ALTVOLTAGE, \
+ .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_SCALE), \
+}
+
+static const struct iio_chan_spec ad9910_channels[] = {
+ [AD9910_CHAN_IDX_PHY] = {
+ .type = IIO_ALTVOLTAGE,
+ .indexed = 1,
+ .output = 1,
+ .channel = AD9910_CHANNEL_PHY,
+ .address = AD9910_CHAN_IDX_PHY,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ .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);
+ tmp32 = (tmp64 * AD9910_MAX_PHASE_MICRORAD) >> 16;
+ *val = tmp32 / MICRO;
+ *val2 = tmp32 % MICRO;
+ return IIO_VAL_INT_PLUS_MICRO;
+ default:
+ return -EINVAL;
+ }
+ case IIO_CHAN_INFO_SCALE:
+ 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_ASF_MSK,
+ st->reg[AD9910_REG_PROFILE(tmp32)].val64);
+ *val = 0;
+ *val2 = tmp64 * MICRO >> 14;
+ return IIO_VAL_INT_PLUS_MICRO;
+ 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;
+ }
+ 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 (!in_range(val, 0, 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(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;
+
+ switch (chan->channel) {
+ case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+ tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+ tmp64 = (u64)val * MICRO + val2;
+ if (tmp64 >= AD9910_MAX_PHASE_MICRORAD)
+ return -EINVAL;
+
+ tmp64 <<= 16;
+ tmp64 = DIV_U64_ROUND_CLOSEST(tmp64, AD9910_MAX_PHASE_MICRORAD);
+ tmp64 = min(tmp64, AD9910_POW_MAX);
+ 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_SCALE:
+ if (val < 0 || val > 1 || (val == 1 && val2 > 0))
+ return -EINVAL;
+
+ switch (chan->channel) {
+ case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+ tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
+ tmp64 = ((u64)val * MICRO + val2) << 14;
+ tmp64 = DIV_U64_ROUND_CLOSEST(tmp64, MICRO);
+ tmp64 = min(tmp64, AD9910_ASF_MAX);
+ 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);
+ default:
+ return -EINVAL;
+ }
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ return ad9910_set_sysclk_freq(st, val, 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:
+ return IIO_VAL_INT_PLUS_MICRO;
+ case IIO_CHAN_INFO_PHASE:
+ case IIO_CHAN_INFO_SCALE:
+ switch (chan->channel) {
+ case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+ return IIO_VAL_INT_PLUS_MICRO;
+ default:
+ return -EINVAL;
+ }
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ return IIO_VAL_INT;
+ 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] = "profile[0]",
+ [AD9910_CHAN_IDX_PROFILE_1] = "profile[1]",
+ [AD9910_CHAN_IDX_PROFILE_2] = "profile[2]",
+ [AD9910_CHAN_IDX_PROFILE_3] = "profile[3]",
+ [AD9910_CHAN_IDX_PROFILE_4] = "profile[4]",
+ [AD9910_CHAN_IDX_PROFILE_5] = "profile[5]",
+ [AD9910_CHAN_IDX_PROFILE_6] = "profile[6]",
+ [AD9910_CHAN_IDX_PROFILE_7] = "profile[7]",
+};
+
+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 |= AD9910_CFR3_VCO_SEL_MSK |
+ 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_REFCLK_DIV_RESETB_MSK |
+ AD9910_CFR3_PFD_RESET_MSK;
+ }
+ st->reg[AD9910_REG_CFR3].val32 = cfr3;
+
+ return ad9910_set_sysclk_freq(st, AD9910_PLL_OUT_MAX_FREQ_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;
+ 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;
+ device_property_read_u32(dev, "adi,charge-pump-current-microamp", &tmp);
+ 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;
+
+ ret = device_property_match_property_string(dev,
+ "adi,refclk-out-drive-strength",
+ refclk_out_drv0,
+ ARRAY_SIZE(refclk_out_drv0));
+ if (ret < 0)
+ st->data.refclk_out_drv = AD9910_REFCLK_OUT_DRV_DISABLED;
+ else
+ st->data.refclk_out_drv = ret;
+ }
+
+ tmp = AD9910_DAC_IOUT_DEFAULT_uA;
+ device_property_read_u32(dev, "adi,dac-output-current-microamp", &tmp);
+ if (tmp < AD9910_DAC_IOUT_MIN_uA || tmp > AD9910_DAC_IOUT_MAX_uA)
+ return dev_err_probe(dev, -ERANGE,
+ "Invalid DAC output current %u uA\n", tmp);
+ st->data.dac_output_current = tmp;
+
+ 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_deassert(dev_rst);
+ if (ret)
+ return ret;
+
+ ret = ad9910_reg32_write(st, AD9910_REG_CFR1,
+ 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, 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;
+
+ 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");
+
+ ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(supplies), supplies);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to get regulators\n");
+
+ ret = devm_mutex_init(dev, &st->lock);
+ if (ret)
+ return ret;
+
+ 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);
+
+ 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");
+
+ 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 = 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[] = {
+ { "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] 26+ messages in thread* Re: [PATCH RFC v4 03/10] iio: frequency: ad9910: initial driver implementation
2026-05-08 17:00 ` [PATCH RFC v4 03/10] iio: frequency: ad9910: initial driver implementation Rodrigo Alencar via B4 Relay
@ 2026-05-08 22:51 ` sashiko-bot
0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-05-08 22:51 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay; +Cc: robh, conor+dt, devicetree, krzk+dt
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] Writing 0 to the `samp_freq` attribute causes a kernel panic due to division by zero.
- [High] Setting the profile frequency implicitly mutes the DAC output due to a 0-initialized register cache.
- [High] Negative fractional inputs for frequency and scale cause unsigned integer underflow, applying maximum settings.
- [High] Probe fails for standard reference clocks due to `DIV_ROUND_CLOSEST` overshooting the maximum PLL frequency.
- [High] The initialization of the CFR3 hardware register is silently skipped if the PLL is disabled and the divider is even.
- [Medium] The driver unconditionally hardcodes the AD9910 into 4-wire SPI mode, breaking 3-wire SPI configurations.
--
commit ca91efe2fa4d4b296d7ac7c8f6d66ef193e1a2a0
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>
iio: frequency: ad9910: initial driver implementation
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.
> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> new file mode 100644
> index 0000000000000..c75f2ef178c25
> --- /dev/null
> +++ b/drivers/iio/frequency/ad9910.c
[ ... ]
> +static int ad9910_set_sysclk_freq(struct ad9910_state *st, u32 freq_hz,
> + bool update)
> +{
[ ... ]
> + if (st->data.pll_enabled) {
[ ... ]
> + } else {
> + tmp32 = DIV_ROUND_CLOSEST(refclk_freq_hz, freq_hz);
> + tmp32 = clamp(tmp32, 1, 2);
If a user writes 0 to the samp_freq sysfs attribute, freq_hz will be 0.
Could this cause a division by zero panic here when the PLL is disabled?
[ ... ]
> +static int ad9910_write_raw(struct iio_dev *indio_dev,
> + struct iio_chan_spec const *chan,
> + int val, int val2, long info)
> +{
[ ... ]
> + case IIO_CHAN_INFO_FREQUENCY:
> + if (!in_range(val, 0, 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);
The IIO core parses inputs between 0 and -1 (e.g., -0.5) as val = 0 and
val2 = -500000.
Does this bypass the bounds check since val is 0, and cause an unsigned
integer underflow when adding the negative val2 to (u64)val * MICRO? This
could misconfigure the device with maximum physical values.
> + tmp64 = min(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);
Since the profile registers in the st->reg cache are zero-initialized
during probe, will updating only the frequency word here merge it with an
Amplitude Scale Factor (ASF) of 0, inadvertently muting the DAC output?
[ ... ]
> + case IIO_CHAN_INFO_SCALE:
> + if (val < 0 || val > 1 || (val == 1 && val2 > 0))
> + return -EINVAL;
> +
> + switch (chan->channel) {
> + case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
> + tmp32 = chan->channel - AD9910_CHANNEL_PROFILE_0;
> + tmp64 = ((u64)val * MICRO + val2) << 14;
Similar to the frequency attribute, could a negative fractional input like
-0.5 pass the val < 0 check here because val == 0, leading to unsigned
wrap-around when applying the negative val2?
[ ... ]
> +static int ad9910_cfg_sysclk(struct ad9910_state *st, bool update)
> +{
> + u32 cfr3 = AD9910_CFR3_OPEN_MSK;
> + u32 tmp32;
> +
> + cfr3 |= AD9910_CFR3_VCO_SEL_MSK |
> + FIELD_PREP(AD9910_CFR3_DRV0_MSK, st->data.refclk_out_drv);
> +
> + if (st->data.pll_enabled) {
[ ... ]
> + } else {
> + cfr3 |= 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_PLL_OUT_MAX_FREQ_HZ, update);
> +}
If the PLL is disabled, ad9910_set_sysclk_freq() is called with
update = false.
Inside that function, ad9910_reg32_update() checks if the new value matches
st->reg. Since cfr3 was just written to st->reg[AD9910_REG_CFR3].val32,
will this cause ad9910_reg32_update() to return 0 without executing the
SPI write, silently skipping the CFR3 hardware initialization?
Also, for standard reference clocks, is it possible that requesting
AD9910_PLL_OUT_MAX_FREQ_HZ (1000 MHz) causes a probe failure?
For example, with a 24 MHz clock, DIV_ROUND_CLOSEST(1000, 24) is 42,
which produces 1008 MHz. This exceeds the 1000 MHz maximum and causes
ad9910_set_sysclk_freq() to return -ERANGE.
[ ... ]
> +static int ad9910_setup(struct device *dev, struct ad9910_state *st,
> + struct reset_control *dev_rst)
> +{
> + int ret;
> +
> + ret = reset_control_deassert(dev_rst);
> + if (ret)
> + return ret;
> +
> + ret = ad9910_reg32_write(st, AD9910_REG_CFR1,
> + AD9910_CFR1_SDIO_INPUT_ONLY_MSK, false);
Does hardcoding AD9910_CFR1_SDIO_INPUT_ONLY_MSK unconditionally force the
device into 4-wire SPI mode? If the system relies on 3-wire SPI and
specifies spi-3wire in the device tree, could this break all subsequent
SPI reads?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com?part=3
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH RFC v4 04/10] iio: frequency: ad9910: add basic parallel port support
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
` (2 preceding siblings ...)
2026-05-08 17:00 ` [PATCH RFC v4 03/10] iio: frequency: ad9910: initial driver implementation Rodrigo Alencar via B4 Relay
@ 2026-05-08 17:00 ` Rodrigo Alencar via B4 Relay
2026-05-08 23:19 ` sashiko-bot
2026-05-08 17:00 ` [PATCH RFC v4 05/10] iio: frequency: ad9910: add digital ramp generator support Rodrigo Alencar via B4 Relay
` (6 subsequent siblings)
10 siblings, 1 reply; 26+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-05-08 17:00 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 channel with frequency scale, frequency offset, phase
offset, and amplitude offset extended attributes for configuring the
parallel data path. Enabling and disabling of parallel mode will be
implemented with buffer setup ops when an IIO backend integration is in
place.
Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
drivers/iio/frequency/ad9910.c | 153 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 153 insertions(+)
diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index c75f2ef178c2..b069b849e8d7 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 GENMASK(13, 0)
+#define AD9910_ASF_PP_LSB_MAX GENMASK(5, 0)
#define AD9910_POW_MAX GENMASK(15, 0)
+#define AD9910_POW_PP_LSB_MAX GENMASK(7, 0)
#define AD9910_NUM_PROFILES 8
/* PLL constants */
@@ -189,6 +195,7 @@
* @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_PORT: Parallel Port output channel
*/
enum ad9910_channel {
AD9910_CHANNEL_PHY = 100,
@@ -200,6 +207,7 @@ enum ad9910_channel {
AD9910_CHANNEL_PROFILE_5 = 106,
AD9910_CHANNEL_PROFILE_6 = 107,
AD9910_CHANNEL_PROFILE_7 = 108,
+ AD9910_CHANNEL_PARALLEL_PORT = 110,
};
enum {
@@ -212,10 +220,15 @@ enum {
AD9910_CHAN_IDX_PROFILE_5,
AD9910_CHAN_IDX_PROFILE_6,
AD9910_CHAN_IDX_PROFILE_7,
+ AD9910_CHAN_IDX_PARALLEL_PORT,
};
enum {
AD9910_POWERDOWN,
+ AD9910_PP_FREQ_SCALE,
+ AD9910_PP_FREQ_OFFSET,
+ AD9910_PP_PHASE_OFFSET,
+ AD9910_PP_AMP_OFFSET,
};
struct ad9910_data {
@@ -483,6 +496,10 @@ static ssize_t ad9910_ext_info_read(struct iio_dev *indio_dev,
case AD9910_POWERDOWN:
val = ad9910_sw_powerdown_get(st);
break;
+ case AD9910_PP_FREQ_SCALE:
+ val = BIT(FIELD_GET(AD9910_CFR2_FM_GAIN_MSK,
+ st->reg[AD9910_REG_CFR2].val32));
+ break;
default:
return -EINVAL;
}
@@ -511,6 +528,121 @@ static ssize_t ad9910_ext_info_write(struct iio_dev *indio_dev,
if (ret)
return ret;
break;
+ case AD9910_PP_FREQ_SCALE:
+ if (val32 > BIT(15) || !is_power_of_2(val32))
+ return -EINVAL;
+
+ val32 = FIELD_PREP(AD9910_CFR2_FM_GAIN_MSK, ilog2(val32));
+ ret = ad9910_reg32_update(st, AD9910_REG_CFR2,
+ AD9910_CFR2_FM_GAIN_MSK,
+ val32, true);
+ if (ret)
+ return ret;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return len;
+}
+
+static ssize_t ad9910_pp_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);
+ int vals[2];
+ u32 tmp32;
+ u64 tmp64;
+
+ guard(mutex)(&st->lock);
+
+ switch (private) {
+ case AD9910_PP_FREQ_OFFSET:
+ tmp64 = (u64)st->reg[AD9910_REG_FTW].val32 * st->data.sysclk_freq_hz;
+ vals[0] = tmp64 >> 32;
+ vals[1] = ((tmp64 & GENMASK_ULL(31, 0)) * MICRO) >> 32;
+ break;
+ case AD9910_PP_PHASE_OFFSET:
+ tmp32 = FIELD_GET(AD9910_POW_PP_LSB_MSK,
+ st->reg[AD9910_REG_POW].val16);
+ tmp32 = (tmp32 * AD9910_MAX_PHASE_MICRORAD) >> 16;
+ vals[0] = tmp32 / MICRO;
+ vals[1] = tmp32 % MICRO;
+ break;
+ case AD9910_PP_AMP_OFFSET:
+ tmp32 = FIELD_GET(AD9910_ASF_SCALE_FACTOR_PP_LSB_MSK,
+ st->reg[AD9910_REG_ASF].val32);
+ vals[0] = 0;
+ vals[1] = (u64)tmp32 * MICRO >> 14;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return iio_format_value(buf, IIO_VAL_INT_PLUS_MICRO, ARRAY_SIZE(vals), vals);
+}
+
+static ssize_t ad9910_pp_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);
+ int val, val2;
+ u32 tmp32;
+ int ret;
+
+ ret = iio_str_to_fixpoint(buf, MICRO / 10, &val, &val2);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&st->lock);
+
+ switch (private) {
+ case AD9910_PP_FREQ_OFFSET:
+ if (!in_range(val, 0, st->data.sysclk_freq_hz / 2))
+ return -EINVAL;
+
+ tmp32 = ad9910_rational_scale((u64)val * MICRO + val2, BIT_ULL(32),
+ (u64)MICRO * st->data.sysclk_freq_hz);
+ ret = ad9910_reg32_write(st, AD9910_REG_FTW, tmp32, true);
+ if (ret)
+ return ret;
+ break;
+ case AD9910_PP_PHASE_OFFSET:
+ if (val)
+ return -EINVAL;
+
+ if (!in_range(val2, 0, (AD9910_MAX_PHASE_MICRORAD >> 8)))
+ return -EINVAL;
+
+ tmp32 = DIV_ROUND_CLOSEST((u32)val2 << 16, AD9910_MAX_PHASE_MICRORAD);
+ tmp32 = min(tmp32, AD9910_POW_PP_LSB_MAX);
+ tmp32 = FIELD_PREP(AD9910_POW_PP_LSB_MSK, tmp32);
+ ret = ad9910_reg16_update(st, AD9910_REG_POW,
+ AD9910_POW_PP_LSB_MSK,
+ tmp32, true);
+ if (ret)
+ return ret;
+ break;
+ case AD9910_PP_AMP_OFFSET:
+ if (val)
+ return -EINVAL;
+
+ if (!in_range(val2, 0, (MICRO >> 8)))
+ return -EINVAL;
+
+ tmp32 = DIV_ROUND_CLOSEST((u32)val2 << 14, MICRO);
+ tmp32 = min(tmp32, AD9910_ASF_PP_LSB_MAX);
+ tmp32 = FIELD_PREP(AD9910_ASF_SCALE_FACTOR_PP_LSB_MSK, tmp32);
+ ret = ad9910_reg32_update(st, AD9910_REG_ASF,
+ AD9910_ASF_SCALE_FACTOR_PP_LSB_MSK,
+ tmp32, true);
+ if (ret)
+ return ret;
+ break;
default:
return -EINVAL;
}
@@ -529,11 +661,22 @@ static ssize_t ad9910_ext_info_write(struct iio_dev *indio_dev,
#define AD9910_EXT_INFO(_name, _ident, _shared) \
AD9910_EXT_INFO_TMPL(_name, _ident, _shared, ext_info)
+#define AD9910_PP_EXT_INFO(_name, _ident) \
+ AD9910_EXT_INFO_TMPL(_name, _ident, IIO_SEPARATE, pp_attrs)
+
static const struct iio_chan_spec_ext_info ad9910_phy_ext_info[] = {
AD9910_EXT_INFO("powerdown", AD9910_POWERDOWN, IIO_SEPARATE),
{ }
};
+static const struct iio_chan_spec_ext_info ad9910_pp_ext_info[] = {
+ AD9910_EXT_INFO("frequency_scale", AD9910_PP_FREQ_SCALE, IIO_SEPARATE),
+ AD9910_PP_EXT_INFO("frequency_offset", AD9910_PP_FREQ_OFFSET),
+ AD9910_PP_EXT_INFO("phase_offset", AD9910_PP_PHASE_OFFSET),
+ AD9910_PP_EXT_INFO("scale_offset", AD9910_PP_AMP_OFFSET),
+ { }
+};
+
#define AD9910_PROFILE_CHAN(idx) { \
.type = IIO_ALTVOLTAGE, \
.indexed = 1, \
@@ -564,6 +707,15 @@ 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_PORT] = {
+ .type = IIO_ALTVOLTAGE,
+ .indexed = 1,
+ .output = 1,
+ .channel = AD9910_CHANNEL_PARALLEL_PORT,
+ .address = AD9910_CHAN_IDX_PARALLEL_PORT,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE),
+ .ext_info = ad9910_pp_ext_info,
+ },
};
static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -816,6 +968,7 @@ static const char * const ad9910_channel_str[] = {
[AD9910_CHAN_IDX_PROFILE_5] = "profile[5]",
[AD9910_CHAN_IDX_PROFILE_6] = "profile[6]",
[AD9910_CHAN_IDX_PROFILE_7] = "profile[7]",
+ [AD9910_CHAN_IDX_PARALLEL_PORT] = "parallel_port",
};
static int ad9910_read_label(struct iio_dev *indio_dev,
--
2.43.0
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH RFC v4 04/10] iio: frequency: ad9910: add basic parallel port support
2026-05-08 17:00 ` [PATCH RFC v4 04/10] iio: frequency: ad9910: add basic parallel port support Rodrigo Alencar via B4 Relay
@ 2026-05-08 23:19 ` sashiko-bot
0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-05-08 23:19 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay; +Cc: conor+dt, robh, krzk+dt, devicetree
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] The AD9910_PP_FREQ_OFFSET write function fails to reject negative fractional values when the integer part is 0, causing a massive integer underflow when computing the frequency tuning word.
- [Medium] The parallel port channel exposes an `enable` sysfs attribute but lacks support in raw accessors, causing read/write operations to unconditionally return `-EINVAL`.
--
commit 6d64943a7cb666034a2bfe3e59647d035b7c3fb6
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>
iio: frequency: ad9910: add basic parallel port support
This commit adds a parallel port channel to the AD9910 driver with frequency
scale, frequency offset, phase offset, and amplitude offset extended
attributes. Enabling and disabling parallel mode will be implemented later
with buffer setup ops.
> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> index c75f2ef178c25..b069b849e8d72 100644
> --- a/drivers/iio/frequency/ad9910.c
> +++ b/drivers/iio/frequency/ad9910.c
[ ... ]
> @@ -511,6 +528,121 @@ static ssize_t ad9910_ext_info_write(struct iio_dev *indio_dev,
[ ... ]
> +static ssize_t ad9910_pp_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);
> + int val, val2;
> + u32 tmp32;
> + int ret;
> +
> + ret = iio_str_to_fixpoint(buf, MICRO / 10, &val, &val2);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&st->lock);
> +
> + switch (private) {
> + case AD9910_PP_FREQ_OFFSET:
> + if (!in_range(val, 0, st->data.sysclk_freq_hz / 2))
> + return -EINVAL;
> +
> + tmp32 = ad9910_rational_scale((u64)val * MICRO + val2, BIT_ULL(32),
> + (u64)MICRO * st->data.sysclk_freq_hz);
If a negative fraction between 0 and -1 is parsed (for example, "-0.5"),
won't iio_str_to_fixpoint() set val to 0 and val2 to a negative number like
-500000?
Since the bounds check only verifies the integer part (val), 0 passes. Could
the offset calculation (u64)val * MICRO + val2 then underflow to an enormous
unsigned value when adding the negative signed integer to 0ULL?
It looks like this would write a mathematically incorrect, out-of-bounds
frequency tuning word to the device hardware. Should there be an explicit
check for negative values here?
> + ret = ad9910_reg32_write(st, AD9910_REG_FTW, tmp32, true);
> + if (ret)
> + return ret;
> + break;
[ ... ]
> @@ -564,6 +707,15 @@ static const struct iio_chan_spec ad9910_channels[] = {
[ ... ]
> [AD9910_CHAN_IDX_PROFILE_7] = AD9910_PROFILE_CHAN(7),
> + [AD9910_CHAN_IDX_PARALLEL_PORT] = {
> + .type = IIO_ALTVOLTAGE,
> + .indexed = 1,
> + .output = 1,
> + .channel = AD9910_CHANNEL_PARALLEL_PORT,
> + .address = AD9910_CHAN_IDX_PARALLEL_PORT,
> + .info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE),
Does this expose an enable sysfs attribute that unconditionally returns
-EINVAL when accessed?
Looking at ad9910_read_raw(), the IIO_CHAN_INFO_ENABLE case only handles the
profile channels:
case IIO_CHAN_INFO_ENABLE:
switch (chan->channel) {
case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
[ ... ]
break;
default:
return -EINVAL;
}
If parallel mode enabling is planned for the future with buffer setup ops as
noted in the commit message, should this attribute bit be omitted for now to
avoid userspace enumeration tool failures?
> + .ext_info = ad9910_pp_ext_info,
> + },
> };
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com?part=4
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH RFC v4 05/10] iio: frequency: ad9910: add digital ramp generator support
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
` (3 preceding siblings ...)
2026-05-08 17:00 ` [PATCH RFC v4 04/10] iio: frequency: ad9910: add basic parallel port support Rodrigo Alencar via B4 Relay
@ 2026-05-08 17:00 ` Rodrigo Alencar via B4 Relay
2026-05-08 23:53 ` sashiko-bot
2026-05-08 17:00 ` [PATCH RFC v4 06/10] iio: frequency: ad9910: add RAM mode support Rodrigo Alencar via B4 Relay
` (5 subsequent siblings)
10 siblings, 1 reply; 26+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-05-08 17:00 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 | 511 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 509 insertions(+), 2 deletions(-)
diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index b069b849e8d7..d6c88ec51145 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)
@@ -145,6 +157,7 @@
#define AD9910_ASF_PP_LSB_MAX GENMASK(5, 0)
#define AD9910_POW_MAX GENMASK(15, 0)
#define AD9910_POW_PP_LSB_MAX GENMASK(7, 0)
+#define AD9910_STEP_RATE_MAX GENMASK(15, 0)
#define AD9910_NUM_PROFILES 8
/* PLL constants */
@@ -196,6 +209,9 @@
* @AD9910_CHANNEL_PROFILE_6: Profile 6 output channel
* @AD9910_CHANNEL_PROFILE_7: Profile 7 output channel
* @AD9910_CHANNEL_PARALLEL_PORT: Parallel Port 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,
@@ -208,6 +224,24 @@ enum ad9910_channel {
AD9910_CHANNEL_PROFILE_6 = 107,
AD9910_CHANNEL_PROFILE_7 = 108,
AD9910_CHANNEL_PARALLEL_PORT = 110,
+ AD9910_CHANNEL_DRG = 120,
+ AD9910_CHANNEL_DRG_RAMP_UP = 121,
+ AD9910_CHANNEL_DRG_RAMP_DOWN = 122,
+};
+
+/**
+ * enum ad9910_destination - AD9910 DDS core parameter destination
+ *
+ * @AD9910_DEST_FREQUENCY: Frequency destination
+ * @AD9910_DEST_PHASE: Phase destination
+ * @AD9910_DEST_AMPLITUDE: Amplitude destination
+ * @AD9910_DEST_POLAR: Polar destination
+ */
+enum ad9910_destination {
+ AD9910_DEST_FREQUENCY,
+ AD9910_DEST_PHASE,
+ AD9910_DEST_AMPLITUDE,
+ AD9910_DEST_POLAR,
};
enum {
@@ -221,6 +255,9 @@ enum {
AD9910_CHAN_IDX_PROFILE_6,
AD9910_CHAN_IDX_PROFILE_7,
AD9910_CHAN_IDX_PARALLEL_PORT,
+ AD9910_CHAN_IDX_DRG,
+ AD9910_CHAN_IDX_DRG_RAMP_UP,
+ AD9910_CHAN_IDX_DRG_RAMP_DOWN,
};
enum {
@@ -229,6 +266,10 @@ enum {
AD9910_PP_FREQ_OFFSET,
AD9910_PP_PHASE_OFFSET,
AD9910_PP_AMP_OFFSET,
+ AD9910_DRG_FREQ_ROC,
+ AD9910_DRG_PHASE_ROC,
+ AD9910_DRG_AMP_ROC,
+ AD9910_DRG_DWELL_EN,
};
struct ad9910_data {
@@ -482,6 +523,26 @@ static int ad9910_sw_powerdown_set(struct ad9910_state *st, bool enable)
true);
}
+static inline int ad9910_drg_destination_set(struct ad9910_state *st,
+ enum ad9910_destination dest,
+ bool update)
+{
+ return ad9910_reg32_update(st, AD9910_REG_CFR2,
+ AD9910_CFR2_DRG_DEST_MSK,
+ FIELD_PREP(AD9910_CFR2_DRG_DEST_MSK, dest),
+ update);
+}
+
+static inline int ad9910_drg_destination_assert(struct ad9910_state *st,
+ enum ad9910_destination dest)
+{
+ enum ad9910_destination drg_dest;
+
+ drg_dest = (enum ad9910_destination)FIELD_GET(AD9910_CFR2_DRG_DEST_MSK,
+ st->reg[AD9910_REG_CFR2].val32);
+ return drg_dest == dest ? 0 : -EBUSY;
+}
+
static ssize_t ad9910_ext_info_read(struct iio_dev *indio_dev,
uintptr_t private,
const struct iio_chan_spec *chan,
@@ -500,6 +561,14 @@ static ssize_t ad9910_ext_info_read(struct iio_dev *indio_dev,
val = BIT(FIELD_GET(AD9910_CFR2_FM_GAIN_MSK,
st->reg[AD9910_REG_CFR2].val32));
break;
+ case AD9910_DRG_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;
}
@@ -539,6 +608,23 @@ static ssize_t ad9910_ext_info_write(struct iio_dev *indio_dev,
if (ret)
return ret;
break;
+ case AD9910_DRG_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;
}
@@ -650,6 +736,179 @@ static ssize_t ad9910_pp_attrs_write(struct iio_dev *indio_dev,
return len;
}
+static ssize_t ad9910_drg_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);
+ unsigned int type;
+ int ret, vals[2];
+ 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;
+ rate *= 4;
+
+ switch (private) {
+ case AD9910_DRG_FREQ_ROC:
+ ret = ad9910_drg_destination_assert(st, AD9910_DEST_FREQUENCY);
+ if (ret)
+ return ret;
+
+ type = IIO_VAL_INT_64;
+ roc64 = ad9910_rational_scale(roc64, st->data.sysclk_freq_hz,
+ BIT_ULL(32) * rate);
+ vals[0] = (u32)roc64;
+ vals[1] = (u32)(roc64 >> 32);
+ break;
+ case AD9910_DRG_PHASE_ROC:
+ ret = ad9910_drg_destination_assert(st, AD9910_DEST_PHASE);
+ if (ret)
+ return ret;
+
+ type = IIO_VAL_INT_PLUS_NANO;
+ roc64 = ad9910_rational_scale(roc64, AD9910_PI_NANORAD,
+ BIT_ULL(31) * rate);
+ vals[0] = div_s64_rem(roc64, NANO, &vals[1]);
+ break;
+ case AD9910_DRG_AMP_ROC:
+ ret = ad9910_drg_destination_assert(st, AD9910_DEST_AMPLITUDE);
+ if (ret)
+ return ret;
+
+ type = IIO_VAL_INT_PLUS_NANO;
+ roc64 = ad9910_rational_scale(roc64, NANO, BIT_ULL(32) * rate);
+ vals[0] = div_s64_rem(roc64, NANO, &vals[1]);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return iio_format_value(buf, type, ARRAY_SIZE(vals), vals);
+}
+
+static ssize_t ad9910_drg_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);
+ enum ad9910_destination dest;
+ int val, val2;
+ u64 tmp64;
+ u32 rate;
+ int 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);
+ break;
+ case AD9910_CHANNEL_DRG_RAMP_DOWN:
+ rate = FIELD_GET(AD9910_DRG_RATE_DEC_MSK,
+ st->reg[AD9910_REG_DRG_RATE].val32);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (!rate)
+ return -ERANGE;
+
+ rate *= 4;
+
+ switch (private) {
+ case AD9910_DRG_FREQ_ROC:
+ ret = kstrtou64(buf, 10, &tmp64);
+ if (ret)
+ return ret;
+
+ tmp64 = ad9910_rational_scale(tmp64, BIT_ULL(32) * rate,
+ (u64)st->data.sysclk_freq_hz *
+ st->data.sysclk_freq_hz);
+ dest = AD9910_DEST_FREQUENCY;
+ break;
+ case AD9910_DRG_PHASE_ROC:
+ ret = iio_str_to_fixpoint(buf, NANO / 10, &val, &val2);
+ if (ret)
+ return ret;
+
+ if (val < 0 || val2 < 0)
+ return -EINVAL;
+
+ tmp64 = (u64)val * NANO + val2;
+ tmp64 = ad9910_rational_scale(tmp64, BIT_ULL(31) * rate,
+ (u64)AD9910_PI_NANORAD *
+ st->data.sysclk_freq_hz);
+ dest = AD9910_DEST_PHASE;
+ break;
+ case AD9910_DRG_AMP_ROC:
+ ret = iio_str_to_fixpoint(buf, NANO / 10, &val, &val2);
+ if (ret)
+ return ret;
+
+ if (val < 0 || val2 < 0)
+ return -EINVAL;
+
+ tmp64 = (u64)val * NANO + val2;
+ tmp64 = ad9910_rational_scale(tmp64, BIT_ULL(32) * rate,
+ (u64)NANO * st->data.sysclk_freq_hz);
+ dest = AD9910_DEST_AMPLITUDE;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ ret = ad9910_drg_destination_set(st, dest, false);
+ if (ret)
+ return ret;
+
+ tmp64 = min(tmp64, U32_MAX);
+
+ if (chan->channel == AD9910_CHANNEL_DRG_RAMP_UP) {
+ 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;
+ } else {
+ 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;
+ }
+
+ return len;
+}
+
#define AD9910_EXT_INFO_TMPL(_name, _ident, _shared, _fn_desc) { \
.name = _name, \
.read = ad9910_ ## _fn_desc ## _read, \
@@ -664,6 +923,9 @@ static ssize_t ad9910_pp_attrs_write(struct iio_dev *indio_dev,
#define AD9910_PP_EXT_INFO(_name, _ident) \
AD9910_EXT_INFO_TMPL(_name, _ident, IIO_SEPARATE, pp_attrs)
+#define AD9910_DRG_EXT_INFO(_name, _ident) \
+ AD9910_EXT_INFO_TMPL(_name, _ident, IIO_SEPARATE, drg_attrs)
+
static const struct iio_chan_spec_ext_info ad9910_phy_ext_info[] = {
AD9910_EXT_INFO("powerdown", AD9910_POWERDOWN, IIO_SEPARATE),
{ }
@@ -677,6 +939,14 @@ static const struct iio_chan_spec_ext_info ad9910_pp_ext_info[] = {
{ }
};
+static const struct iio_chan_spec_ext_info ad9910_drg_ramp_ext_info[] = {
+ AD9910_EXT_INFO("dwell_en", AD9910_DRG_DWELL_EN, IIO_SEPARATE),
+ AD9910_DRG_EXT_INFO("frequency_roc", AD9910_DRG_FREQ_ROC),
+ AD9910_DRG_EXT_INFO("phase_roc", AD9910_DRG_PHASE_ROC),
+ AD9910_DRG_EXT_INFO("scale_roc", AD9910_DRG_AMP_ROC),
+ { }
+};
+
#define AD9910_PROFILE_CHAN(idx) { \
.type = IIO_ALTVOLTAGE, \
.indexed = 1, \
@@ -716,6 +986,40 @@ static const struct iio_chan_spec ad9910_channels[] = {
.info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE),
.ext_info = ad9910_pp_ext_info,
},
+ [AD9910_CHAN_IDX_DRG] = {
+ .type = IIO_ALTVOLTAGE,
+ .indexed = 1,
+ .output = 1,
+ .channel = AD9910_CHANNEL_DRG,
+ .address = AD9910_CHAN_IDX_DRG,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE),
+ },
+ [AD9910_CHAN_IDX_DRG_RAMP_UP] = {
+ .type = IIO_ALTVOLTAGE,
+ .indexed = 1,
+ .output = 1,
+ .channel = AD9910_CHANNEL_DRG_RAMP_UP,
+ .address = AD9910_CHAN_IDX_DRG_RAMP_UP,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE) |
+ BIT(IIO_CHAN_INFO_FREQUENCY) |
+ BIT(IIO_CHAN_INFO_PHASE) |
+ BIT(IIO_CHAN_INFO_SCALE) |
+ BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ .ext_info = ad9910_drg_ramp_ext_info,
+ },
+ [AD9910_CHAN_IDX_DRG_RAMP_DOWN] = {
+ .type = IIO_ALTVOLTAGE,
+ .indexed = 1,
+ .output = 1,
+ .channel = AD9910_CHANNEL_DRG_RAMP_DOWN,
+ .address = AD9910_CHAN_IDX_DRG_RAMP_DOWN,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE) |
+ BIT(IIO_CHAN_INFO_FREQUENCY) |
+ BIT(IIO_CHAN_INFO_PHASE) |
+ BIT(IIO_CHAN_INFO_SCALE) |
+ BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ .ext_info = ad9910_drg_ramp_ext_info,
+ },
};
static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -725,6 +1029,7 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
struct ad9910_state *st = iio_priv(indio_dev);
u64 tmp64;
u32 tmp32;
+ int ret;
guard(mutex)(&st->lock);
@@ -739,6 +1044,10 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
*val = (tmp32 == st->profile);
}
break;
+ case AD9910_CHANNEL_DRG:
+ *val = FIELD_GET(AD9910_CFR2_DRG_ENABLE_MSK,
+ st->reg[AD9910_REG_CFR2].val32);
+ break;
default:
return -EINVAL;
}
@@ -750,6 +1059,22 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
tmp64 = FIELD_GET(AD9910_PROFILE_ST_FTW_MSK,
st->reg[AD9910_REG_PROFILE(tmp32)].val64);
break;
+ case AD9910_CHANNEL_DRG_RAMP_UP:
+ ret = ad9910_drg_destination_assert(st, AD9910_DEST_FREQUENCY);
+ if (ret)
+ return ret;
+
+ tmp64 = FIELD_GET(AD9910_DRG_LIMIT_UPPER_MSK,
+ st->reg[AD9910_REG_DRG_LIMIT].val64);
+ break;
+ case AD9910_CHANNEL_DRG_RAMP_DOWN:
+ ret = ad9910_drg_destination_assert(st, AD9910_DEST_FREQUENCY);
+ if (ret)
+ return ret;
+
+ tmp64 = FIELD_GET(AD9910_DRG_LIMIT_LOWER_MSK,
+ st->reg[AD9910_REG_DRG_LIMIT].val64);
+ break;
default:
return -EINVAL;
}
@@ -767,6 +1092,26 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
*val = tmp32 / MICRO;
*val2 = tmp32 % MICRO;
return IIO_VAL_INT_PLUS_MICRO;
+ case AD9910_CHANNEL_DRG_RAMP_UP:
+ ret = ad9910_drg_destination_assert(st, AD9910_DEST_PHASE);
+ if (ret)
+ return ret;
+
+ tmp64 = FIELD_GET(AD9910_DRG_LIMIT_UPPER_MSK,
+ st->reg[AD9910_REG_DRG_LIMIT].val64);
+ tmp64 = (tmp64 * AD9910_PI_NANORAD) >> 31;
+ *val = div_u64_rem(tmp64, NANO, val2);
+ return IIO_VAL_INT_PLUS_NANO;
+ case AD9910_CHANNEL_DRG_RAMP_DOWN:
+ ret = ad9910_drg_destination_assert(st, AD9910_DEST_PHASE);
+ if (ret)
+ return ret;
+
+ tmp64 = FIELD_GET(AD9910_DRG_LIMIT_LOWER_MSK,
+ st->reg[AD9910_REG_DRG_LIMIT].val64);
+ tmp64 = (tmp64 * AD9910_PI_NANORAD) >> 31;
+ *val = div_u64_rem(tmp64, NANO, val2);
+ return IIO_VAL_INT_PLUS_NANO;
default:
return -EINVAL;
}
@@ -779,6 +1124,26 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
*val = 0;
*val2 = tmp64 * MICRO >> 14;
return IIO_VAL_INT_PLUS_MICRO;
+ case AD9910_CHANNEL_DRG_RAMP_UP:
+ ret = ad9910_drg_destination_assert(st, AD9910_DEST_AMPLITUDE);
+ if (ret)
+ return ret;
+
+ tmp64 = FIELD_GET(AD9910_DRG_LIMIT_UPPER_MSK,
+ st->reg[AD9910_REG_DRG_LIMIT].val64);
+ *val = 0;
+ *val2 = tmp64 * NANO >> 32;
+ return IIO_VAL_INT_PLUS_NANO;
+ case AD9910_CHANNEL_DRG_RAMP_DOWN:
+ ret = ad9910_drg_destination_assert(st, AD9910_DEST_AMPLITUDE);
+ if (ret)
+ return ret;
+
+ tmp64 = FIELD_GET(AD9910_DRG_LIMIT_LOWER_MSK,
+ st->reg[AD9910_REG_DRG_LIMIT].val64);
+ *val = 0;
+ *val2 = tmp64 * NANO >> 32;
+ return IIO_VAL_INT_PLUS_NANO;
default:
return -EINVAL;
}
@@ -787,9 +1152,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;
default:
return -EINVAL;
}
@@ -823,6 +1202,11 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
return ret;
return ad9910_profile_set(st, tmp32);
+ case AD9910_CHANNEL_DRG:
+ tmp32 = FIELD_PREP(AD9910_CFR2_DRG_ENABLE_MSK, val);
+ return ad9910_reg32_update(st, AD9910_REG_CFR2,
+ AD9910_CFR2_DRG_ENABLE_MSK,
+ tmp32, true);
default:
return -EINVAL;
}
@@ -840,6 +1224,28 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
AD9910_PROFILE_ST_FTW_MSK,
tmp64, true);
+ case AD9910_CHANNEL_DRG_RAMP_UP:
+ ret = ad9910_drg_destination_set(st,
+ AD9910_DEST_FREQUENCY,
+ false);
+ if (ret)
+ return ret;
+
+ 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:
+ ret = ad9910_drg_destination_set(st,
+ AD9910_DEST_FREQUENCY,
+ false);
+ if (ret)
+ return ret;
+
+ 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;
}
@@ -861,6 +1267,40 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
AD9910_PROFILE_ST_POW_MSK,
tmp64, true);
+ case AD9910_CHANNEL_DRG_RAMP_UP:
+ tmp64 = (u64)val * NANO + val2;
+ if (tmp64 > 2ULL * AD9910_PI_NANORAD)
+ return -EINVAL;
+
+ ret = ad9910_drg_destination_set(st, AD9910_DEST_PHASE,
+ false);
+ if (ret)
+ return ret;
+
+ tmp64 <<= 31;
+ tmp64 = DIV_U64_ROUND_CLOSEST(tmp64, AD9910_PI_NANORAD);
+ tmp64 = min(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 = (u64)val * NANO + val2;
+ if (tmp64 > 2ULL * AD9910_PI_NANORAD)
+ return -EINVAL;
+
+ ret = ad9910_drg_destination_set(st, AD9910_DEST_PHASE,
+ false);
+ if (ret)
+ return ret;
+
+ tmp64 <<= 31;
+ tmp64 = DIV_U64_ROUND_CLOSEST(tmp64, AD9910_PI_NANORAD);
+ tmp64 = min(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;
}
@@ -878,11 +1318,62 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
return ad9910_reg64_update(st, AD9910_REG_PROFILE(tmp32),
AD9910_PROFILE_ST_ASF_MSK,
tmp64, true);
+ case AD9910_CHANNEL_DRG_RAMP_UP:
+ ret = ad9910_drg_destination_set(st,
+ AD9910_DEST_AMPLITUDE,
+ false);
+ if (ret)
+ return ret;
+
+ tmp64 = ((u64)val * NANO + val2) << 32;
+ tmp64 = DIV_U64_ROUND_CLOSEST(tmp64, NANO);
+ tmp64 = min(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:
+ ret = ad9910_drg_destination_set(st,
+ AD9910_DEST_AMPLITUDE,
+ false);
+ if (ret)
+ return ret;
+
+ tmp64 = ((u64)val * NANO + val2) << 32;
+ tmp64 = DIV_U64_ROUND_CLOSEST(tmp64, NANO);
+ tmp64 = min(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);
+
+ 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;
+ }
default:
return -EINVAL;
}
@@ -902,11 +1393,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_PLUS_MICRO;
+ case AD9910_CHANNEL_DRG_RAMP_UP:
+ case AD9910_CHANNEL_DRG_RAMP_DOWN:
+ return IIO_VAL_INT_PLUS_NANO;
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;
default:
return -EINVAL;
}
@@ -969,6 +1465,9 @@ static const char * const ad9910_channel_str[] = {
[AD9910_CHAN_IDX_PROFILE_6] = "profile[6]",
[AD9910_CHAN_IDX_PROFILE_7] = "profile[7]",
[AD9910_CHAN_IDX_PARALLEL_PORT] = "parallel_port",
+ [AD9910_CHAN_IDX_DRG] = "digital_ramp_generator",
+ [AD9910_CHAN_IDX_DRG_RAMP_UP] = "digital_ramp_up",
+ [AD9910_CHAN_IDX_DRG_RAMP_DOWN] = "digital_ramp_down",
};
static int ad9910_read_label(struct iio_dev *indio_dev,
@@ -1094,6 +1593,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] 26+ messages in thread* Re: [PATCH RFC v4 05/10] iio: frequency: ad9910: add digital ramp generator support
2026-05-08 17:00 ` [PATCH RFC v4 05/10] iio: frequency: ad9910: add digital ramp generator support Rodrigo Alencar via B4 Relay
@ 2026-05-08 23:53 ` sashiko-bot
0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-05-08 23:53 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay; +Cc: devicetree, robh, krzk+dt, conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Missing bounds check for negative fractional inputs causes integer underflow, silently configuring hardware to maximum limits.
- [Medium] The `IIO_CHAN_INFO_ENABLE` attribute is exposed for DRG ramp channels but unconditionally returns `-EINVAL`.
- [Low] Incompatible pointer type mismatch when passing `int *` to `div_u64_rem()`.
--
commit 81e59a90f90a26812172e27f2860e2027f281ffb
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>
Subject: iio: frequency: ad9910: add digital ramp generator support
This commit adds Digital Ramp Generator channels with destination selection
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.
> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> index b069b849e8d72..d6c88ec51145e 100644
> --- a/drivers/iio/frequency/ad9910.c
> +++ b/drivers/iio/frequency/ad9910.c
[ ... ]
> @@ -716,6 +986,40 @@ static const struct iio_chan_spec ad9910_channels[] = {
> .info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE),
> .ext_info = ad9910_pp_ext_info,
> },
> + [AD9910_CHAN_IDX_DRG] = {
> + .type = IIO_ALTVOLTAGE,
> + .indexed = 1,
> + .output = 1,
> + .channel = AD9910_CHANNEL_DRG,
> + .address = AD9910_CHAN_IDX_DRG,
> + .info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE),
> + },
> + [AD9910_CHAN_IDX_DRG_RAMP_UP] = {
> + .type = IIO_ALTVOLTAGE,
> + .indexed = 1,
> + .output = 1,
> + .channel = AD9910_CHANNEL_DRG_RAMP_UP,
> + .address = AD9910_CHAN_IDX_DRG_RAMP_UP,
> + .info_mask_separate = BIT(IIO_CHAN_INFO_ENABLE) |
The channel definition for AD9910_CHAN_IDX_DRG_RAMP_UP and
AD9910_CHAN_IDX_DRG_RAMP_DOWN includes BIT(IIO_CHAN_INFO_ENABLE) in the
info_mask_separate.
However, in ad9910_read_raw() and ad9910_write_raw(), only
AD9910_CHANNEL_DRG is handled for IIO_CHAN_INFO_ENABLE.
Will reads and writes to the ramp channels' enable attributes fall
through to the default case and unconditionally return -EINVAL? Should
the enable bit be removed from the ramp channels' info mask?
> + BIT(IIO_CHAN_INFO_FREQUENCY) |
> + BIT(IIO_CHAN_INFO_PHASE) |
> + BIT(IIO_CHAN_INFO_SCALE) |
> + BIT(IIO_CHAN_INFO_SAMP_FREQ),
> + .ext_info = ad9910_drg_ramp_ext_info,
> + },
[ ... ]
> @@ -767,6 +1092,26 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
> *val = tmp32 / MICRO;
> *val2 = tmp32 % MICRO;
> return IIO_VAL_INT_PLUS_MICRO;
> + case AD9910_CHANNEL_DRG_RAMP_UP:
> + ret = ad9910_drg_destination_assert(st, AD9910_DEST_PHASE);
> + if (ret)
> + return ret;
> +
> + tmp64 = FIELD_GET(AD9910_DRG_LIMIT_UPPER_MSK,
> + st->reg[AD9910_REG_DRG_LIMIT].val64);
> + tmp64 = (tmp64 * AD9910_PI_NANORAD) >> 31;
> + *val = div_u64_rem(tmp64, NANO, val2);
Here val2 is provided by the IIO core as an int *, but div_u64_rem()
expects its third argument to be a u32 *remainder.
Does passing a signed integer pointer to a function expecting an unsigned
integer pointer cause a compiler warning or strict aliasing violation?
Should a local u32 variable be used to capture the remainder before
assigning it to *val2?
> + return IIO_VAL_INT_PLUS_NANO;
[ ... ]
> 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);
> +
> + tmp64 = ((u64)val * MICRO + val2) * 4;
If a negative fractional string like "-0.5" is provided by userspace, the
IIO core sets val to 0 and val2 to a negative value (like -500000).
Since val2 is signed and (u64)val * MICRO is unsigned, won't adding them
together cause a sign-extended underflow, resulting in a very large tmp64
value that is then clamped to the hardware maximums?
Should there be a bounds check to ensure val2 >= 0, or an in_range()
check for these attributes before calculating tmp64?
> + if (!tmp64)
> + return -EINVAL;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com?part=5
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH RFC v4 06/10] iio: frequency: ad9910: add RAM mode support
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
` (4 preceding siblings ...)
2026-05-08 17:00 ` [PATCH RFC v4 05/10] iio: frequency: ad9910: add digital ramp generator support Rodrigo Alencar via B4 Relay
@ 2026-05-08 17:00 ` Rodrigo Alencar via B4 Relay
2026-05-09 0:33 ` sashiko-bot
2026-05-08 17:00 ` [PATCH RFC v4 07/10] iio: frequency: ad9910: add output shift keying support Rodrigo Alencar via B4 Relay
` (4 subsequent siblings)
10 siblings, 1 reply; 26+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-05-08 17:00 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 | 2 +
drivers/iio/frequency/ad9910.c | 329 ++++++++++++++++++++++++++++++++++++++++-
2 files changed, 325 insertions(+), 6 deletions(-)
diff --git a/drivers/iio/frequency/Kconfig b/drivers/iio/frequency/Kconfig
index 180e74f62d11..a5b2e5cb5269 100644
--- a/drivers/iio/frequency/Kconfig
+++ b/drivers/iio/frequency/Kconfig
@@ -29,6 +29,8 @@ config AD9910
tristate "Analog Devices AD9910 Direct Digital Synthesizer"
depends on SPI
depends on GPIOLIB
+ 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 d6c88ec51145..a153cd01e6f5 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -8,9 +8,11 @@
#include <linux/array_size.h>
#include <linux/bitfield.h>
#include <linux/clk.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 +149,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, 57)
+#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
@@ -160,6 +171,15 @@
#define AD9910_STEP_RATE_MAX GENMASK(15, 0)
#define AD9910_NUM_PROFILES 8
+#define AD9910_RAM_FW_MAGIC 0x00AD9910
+#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
@@ -191,7 +211,7 @@
#define AD9910_REFDIV2_MAX_FREQ_HZ (1900 * HZ_PER_MHZ)
#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)
@@ -212,6 +232,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,
@@ -227,6 +248,7 @@ enum ad9910_channel {
AD9910_CHANNEL_DRG = 120,
AD9910_CHANNEL_DRG_RAMP_UP = 121,
AD9910_CHANNEL_DRG_RAMP_DOWN = 122,
+ AD9910_CHANNEL_RAM = 130,
};
/**
@@ -244,6 +266,27 @@ enum ad9910_destination {
AD9910_DEST_POLAR,
};
+/**
+ * struct ad9910_ram_fw - AD9910 RAM firmware format
+ * @magic: Magic number for RAM firmware validation
+ * @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
+ * @reserved: Reserved field for future use, should be set to 0
+ * @wcount: Number of RAM words to be written
+ * @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;
+ __be32 cfr1;
+ __be64 profiles[AD9910_NUM_PROFILES];
+ __be32 reserved;
+ __be32 wcount;
+ __be32 words[] __counted_by_be(wcount);
+} __packed;
+
enum {
AD9910_CHAN_IDX_PHY,
AD9910_CHAN_IDX_PROFILE_0,
@@ -258,6 +301,7 @@ enum {
AD9910_CHAN_IDX_DRG,
AD9910_CHAN_IDX_DRG_RAMP_UP,
AD9910_CHAN_IDX_DRG_RAMP_DOWN,
+ AD9910_CHAN_IDX_RAM,
};
enum {
@@ -290,6 +334,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;
@@ -298,12 +343,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
@@ -327,6 +382,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) {
@@ -1020,6 +1091,17 @@ static const struct iio_chan_spec ad9910_channels[] = {
BIT(IIO_CHAN_INFO_SAMP_FREQ),
.ext_info = ad9910_drg_ramp_ext_info,
},
+ [AD9910_CHAN_IDX_RAM] = {
+ .type = IIO_ALTVOLTAGE,
+ .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),
+ },
};
static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -1048,6 +1130,10 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
*val = FIELD_GET(AD9910_CFR2_DRG_ENABLE_MSK,
st->reg[AD9910_REG_CFR2].val32);
break;
+ case AD9910_CHANNEL_RAM:
+ *val = FIELD_GET(AD9910_CFR1_RAM_ENABLE_MSK,
+ st->reg[AD9910_REG_CFR1].val32);
+ break;
default:
return -EINVAL;
}
@@ -1057,7 +1143,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;
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_DRG_RAMP_UP:
ret = ad9910_drg_destination_assert(st, AD9910_DEST_FREQUENCY);
@@ -1075,6 +1161,9 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
tmp64 = FIELD_GET(AD9910_DRG_LIMIT_LOWER_MSK,
st->reg[AD9910_REG_DRG_LIMIT].val64);
break;
+ case AD9910_CHANNEL_RAM:
+ tmp64 = st->reg[AD9910_REG_FTW].val32;
+ break;
default:
return -EINVAL;
}
@@ -1087,7 +1176,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;
tmp64 = FIELD_GET(AD9910_PROFILE_ST_POW_MSK,
- st->reg[AD9910_REG_PROFILE(tmp32)].val64);
+ ad9910_st_profile_val(st, tmp32));
tmp32 = (tmp64 * AD9910_MAX_PHASE_MICRORAD) >> 16;
*val = tmp32 / MICRO;
*val2 = tmp32 % MICRO;
@@ -1112,6 +1201,12 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
tmp64 = (tmp64 * AD9910_PI_NANORAD) >> 31;
*val = div_u64_rem(tmp64, NANO, val2);
return IIO_VAL_INT_PLUS_NANO;
+ case AD9910_CHANNEL_RAM:
+ tmp64 = st->reg[AD9910_REG_POW].val16;
+ tmp32 = (tmp64 * AD9910_MAX_PHASE_MICRORAD) >> 16;
+ *val = tmp32 / MICRO;
+ *val2 = tmp32 % MICRO;
+ return IIO_VAL_INT_PLUS_MICRO;
default:
return -EINVAL;
}
@@ -1120,7 +1215,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;
tmp64 = FIELD_GET(AD9910_PROFILE_ST_ASF_MSK,
- st->reg[AD9910_REG_PROFILE(tmp32)].val64);
+ ad9910_st_profile_val(st, tmp32));
*val = 0;
*val2 = tmp64 * MICRO >> 14;
return IIO_VAL_INT_PLUS_MICRO;
@@ -1160,6 +1255,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;
}
@@ -1181,7 +1280,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);
@@ -1207,6 +1306,26 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
return ad9910_reg32_update(st, AD9910_REG_CFR2,
AD9910_CFR2_DRG_ENABLE_MSK,
tmp32, true);
+ case AD9910_CHANNEL_RAM:
+ if (AD9910_RAM_ENABLED(st) == !!val)
+ return 0;
+
+ /* switch 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)
+ return ret;
+ st->reg_profile[i] = tmp64;
+ }
+
+ 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;
}
@@ -1220,6 +1339,11 @@ 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,
@@ -1246,6 +1370,8 @@ 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_RAM:
+ return ad9910_reg32_write(st, AD9910_REG_FTW, tmp64, true);
default:
return -EINVAL;
}
@@ -1263,6 +1389,13 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
tmp64 <<= 16;
tmp64 = DIV_U64_ROUND_CLOSEST(tmp64, AD9910_MAX_PHASE_MICRORAD);
tmp64 = min(tmp64, AD9910_POW_MAX);
+
+ 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,
@@ -1301,6 +1434,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_RAM:
+ tmp64 = (u64)val * MICRO + val2;
+ if (tmp64 >= AD9910_MAX_PHASE_MICRORAD)
+ return -EINVAL;
+
+ tmp64 <<= 16;
+ tmp64 = DIV_U64_ROUND_CLOSEST(tmp64, AD9910_MAX_PHASE_MICRORAD);
+ tmp64 = min(tmp64, AD9910_POW_MAX);
+ return ad9910_reg16_write(st, AD9910_REG_POW, tmp64, true);
default:
return -EINVAL;
}
@@ -1314,6 +1456,13 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
tmp64 = ((u64)val * MICRO + val2) << 14;
tmp64 = DIV_U64_ROUND_CLOSEST(tmp64, MICRO);
tmp64 = min(tmp64, 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,
@@ -1371,6 +1520,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;
}
@@ -1392,6 +1552,7 @@ static int ad9910_write_raw_get_fmt(struct iio_dev *indio_dev,
case IIO_CHAN_INFO_SCALE:
switch (chan->channel) {
case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
+ case AD9910_CHANNEL_RAM:
return IIO_VAL_INT_PLUS_MICRO;
case AD9910_CHANNEL_DRG_RAMP_UP:
case AD9910_CHANNEL_DRG_RAMP_DOWN:
@@ -1468,6 +1629,7 @@ static const char * const ad9910_channel_str[] = {
[AD9910_CHAN_IDX_DRG] = "digital_ramp_generator",
[AD9910_CHAN_IDX_DRG_RAMP_UP] = "digital_ramp_up",
[AD9910_CHAN_IDX_DRG_RAMP_DOWN] = "digital_ramp_down",
+ [AD9910_CHAN_IDX_RAM] = "ram_control",
};
static int ad9910_read_label(struct iio_dev *indio_dev,
@@ -1477,6 +1639,123 @@ 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;
+ u32 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;
+
+ wcount = get_unaligned_be32(&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;
+
+ 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;
+
+ /* copy ram profiles */
+ for (idx = 0; idx < AD9910_NUM_PROFILES; idx++)
+ st->reg_profile[idx] = get_unaligned_be64(&fw_data->profiles[idx]) |
+ AD9910_PROFILE_RAM_OPEN_MSK;
+
+ /* update CFR1 */
+ 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_be32(&fw_data->wcount);
+ if (!wcount) {
+ *written = size;
+ return FW_UPLOAD_ERR_NONE; /* nothing else to write */
+ }
+
+ /* ensure profile is selected */
+ 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;
+
+ /* populate words into tx_buf[1:] */
+ 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,
@@ -1601,9 +1880,33 @@ static int ad9910_setup(struct device *dev, struct ad9910_state *st,
if (ret)
return ret;
+ for (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[] = {
@@ -1688,7 +1991,21 @@ 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);
+ ret = devm_iio_device_register(dev, indio_dev);
+ if (ret)
+ return ret;
+
+ 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");
+
+ ad9910_debugfs_init(st, indio_dev);
+
+ return devm_add_action_or_reset(dev, ad9910_ram_fwu_unregister, st->ram_fwu);
}
static const struct spi_device_id ad9910_id[] = {
--
2.43.0
^ permalink raw reply related [flat|nested] 26+ messages in thread* Re: [PATCH RFC v4 06/10] iio: frequency: ad9910: add RAM mode support
2026-05-08 17:00 ` [PATCH RFC v4 06/10] iio: frequency: ad9910: add RAM mode support Rodrigo Alencar via B4 Relay
@ 2026-05-09 0:33 ` sashiko-bot
0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-05-09 0:33 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay; +Cc: devicetree, krzk+dt, conor+dt, robh
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] State Corruption on Hardware Write Failure during RAM mode toggle
- [High] Integer Underflow from negative fraction bypasses bounds checks
- [Medium] Missing IIO_CHAN_INFO_SCALE interface for RAM channel prevents single-tone amplitude configuration
--
commit faac9fb534e90f61a8a63acf8162a1789a0da104
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>
iio: frequency: ad9910: add RAM mode support
This commit adds RAM control channel support to the AD9910 driver. It
introduces RAM data loading via firmware upload, profile configuration,
and profile parameter caching when RAM mode is active.
> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> index d6c88ec51145e..a153cd01e6f58 100644
> --- a/drivers/iio/frequency/ad9910.c
> +++ b/drivers/iio/frequency/ad9910.c
[ ... ]
> @@ -1020,6 +1091,17 @@ static const struct iio_chan_spec ad9910_channels[] = {
> BIT(IIO_CHAN_INFO_SAMP_FREQ),
> .ext_info = ad9910_drg_ramp_ext_info,
> },
> + [AD9910_CHAN_IDX_RAM] = {
> + .type = IIO_ALTVOLTAGE,
> + .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),
> + },
> };
Is BIT(IIO_CHAN_INFO_SCALE) missing from this mask?
ad9910_write_raw_get_fmt() expects AD9910_CHANNEL_RAM to support
IIO_CHAN_INFO_SCALE for single-tone amplitude configuration. Without the bit
set here, and without handling it in ad9910_read_raw() and ad9910_write_raw(),
users cannot adjust the single-tone amplitude while RAM mode is enabled.
[ ... ]
> @@ -1207,6 +1306,26 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
> return ad9910_reg32_update(st, AD9910_REG_CFR2,
> AD9910_CFR2_DRG_ENABLE_MSK,
> tmp32, true);
> + case AD9910_CHANNEL_RAM:
> + if (AD9910_RAM_ENABLED(st) == !!val)
> + return 0;
> +
> + /* switch 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)
> + return ret;
Can this loop corrupt the software state if an SPI write fails partway through?
If ad9910_reg64_write() returns an error, the function exits immediately without
rolling back the profiles that were already swapped. A subsequent retry would
then iterate over the profiles again, swapping the previously swapped profiles
back and moving the remaining ones forward. This leaves the hardware and
software caches permanently desynchronized.
> + st->reg_profile[i] = tmp64;
> + }
> +
> + 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;
> }
[ ... ]
> @@ -1246,6 +1370,8 @@ 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_RAM:
> + return ad9910_reg32_write(st, AD9910_REG_FTW, tmp64, true);
Does the IIO_CHAN_INFO_FREQUENCY block handle negative fractional inputs
correctly when the integer part is zero?
If a user writes a value like -0.1, IIO parses it as val = 0 and
val2 = -100000. Earlier in the IIO_CHAN_INFO_FREQUENCY block, the bounds
check only inspects val:
case IIO_CHAN_INFO_FREQUENCY:
if (!in_range(val, 0, st->data.sysclk_freq_hz / 2))
return -EINVAL;
tmp64 = ad9910_rational_scale((u64)val * MICRO + val2, ...);
Since val is 0, the check passes. The mathematical operation then adds a
negative integer to an unsigned 64-bit integer, underflowing to a massive
value. This bypasses the bounds check and silently configures a huge RF
frequency instead of safely returning -EINVAL.
This same underflow pattern appears to affect IIO_CHAN_INFO_SCALE and
IIO_CHAN_INFO_SAMP_FREQ.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com?part=6
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH RFC v4 07/10] iio: frequency: ad9910: add output shift keying support
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
` (5 preceding siblings ...)
2026-05-08 17:00 ` [PATCH RFC v4 06/10] iio: frequency: ad9910: add RAM mode support Rodrigo Alencar via B4 Relay
@ 2026-05-08 17:00 ` Rodrigo Alencar via B4 Relay
2026-05-09 1:08 ` sashiko-bot
2026-05-08 17:00 ` [PATCH RFC v4 08/10] iio: frequency: ad9910: show channel priority in debugfs Rodrigo Alencar via B4 Relay
` (3 subsequent siblings)
10 siblings, 1 reply; 26+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-05-08 17:00 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 readback via IIO_CHAN_INFO_SCALE (ASF register);
- Automatic OSK step size configurable througth the scale_roc extended
attribute, which allows for selectable step sizes in nano-units:
- 0: no step, means manual mode (NOT pin controlled)
- 61035: 1/2^14 step, automatic mode (pin controlled)
- 122070: 2/2^14 step, automatic mode (pin controlled)
- 244141: 4/2^14 step, automatic mode (pin controlled)
- 488281: 8/2^14 step, automatic mode (pin controlled)
- 1000000000: 1.0 step, 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 | 192 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 192 insertions(+)
diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index a153cd01e6f5..1fdbaba356d7 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -233,6 +233,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,
@@ -249,6 +250,7 @@ enum ad9910_channel {
AD9910_CHANNEL_DRG_RAMP_UP = 121,
AD9910_CHANNEL_DRG_RAMP_DOWN = 122,
AD9910_CHANNEL_RAM = 130,
+ AD9910_CHANNEL_OSK = 140,
};
/**
@@ -302,6 +304,7 @@ enum {
AD9910_CHAN_IDX_DRG_RAMP_UP,
AD9910_CHAN_IDX_DRG_RAMP_DOWN,
AD9910_CHAN_IDX_RAM,
+ AD9910_CHAN_IDX_OSK,
};
enum {
@@ -314,6 +317,8 @@ enum {
AD9910_DRG_PHASE_ROC,
AD9910_DRG_AMP_ROC,
AD9910_DRG_DWELL_EN,
+ AD9910_OSK_AUTO_ROC,
+ AD9910_OSK_AUTO_ROC_AVAIL,
};
struct ad9910_data {
@@ -980,6 +985,134 @@ static ssize_t ad9910_drg_attrs_write(struct iio_dev *indio_dev,
return len;
}
+static const u32 ad9910_osk_nstep[] = {
+ 0, /* no step: manual mode (NOT pin controlled) */
+ 61035, /* 1/2^14 step: automatic mode (pin controlled) */
+ 122070, /* 2/2^14 step: automatic mode (pin controlled) */
+ 244141, /* 4/2^14 step: automatic mode (pin controlled) */
+ 488281, /* 8/2^14 step: automatic mode (pin controlled) */
+ NANO, /* full scale 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;
+ int vals[2];
+ u64 roc64;
+
+ guard(mutex)(&st->lock);
+
+ rate = 4 * FIELD_GET(AD9910_ASF_RAMP_RATE_MSK,
+ st->reg[AD9910_REG_ASF].val32);
+ if (!rate)
+ return -ERANGE;
+
+ switch (private) {
+ case AD9910_OSK_AUTO_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_nstep[step + 1];
+ } else if (pinctrl_en) {
+ step = ad9910_osk_nstep[ARRAY_SIZE(ad9910_osk_nstep) - 1];
+ } else {
+ step = ad9910_osk_nstep[0];
+ }
+
+ roc64 = div_u64((u64)step * st->data.sysclk_freq_hz, rate);
+ vals[0] = div_s64_rem(roc64, NANO, &vals[1]);
+ return iio_format_value(buf, IIO_VAL_INT_PLUS_NANO,
+ ARRAY_SIZE(vals), vals);
+ case AD9910_OSK_AUTO_ROC_AVAIL: {
+ ssize_t len = 0;
+
+ for (unsigned int i = 0; i < ARRAY_SIZE(ad9910_osk_nstep); i++) {
+ roc64 = div_u64((u64)ad9910_osk_nstep[i] *
+ st->data.sysclk_freq_hz, rate);
+ vals[0] = div_s64_rem(roc64, NANO, &vals[1]);
+ if (!vals[1])
+ len += sysfs_emit_at(buf, len, "%d ", vals[0]);
+ else
+ len += sysfs_emit_at(buf, len, "%d.%09d ",
+ vals[0], vals[1]);
+ }
+
+ 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);
+ int val, val2, ret;
+ u32 idx, cfg, rate;
+ u64 nstep;
+
+ ret = iio_str_to_fixpoint(buf, NANO / 10, &val, &val2);
+ if (ret)
+ return ret;
+
+ if (val < 0 || val2 < 0)
+ return -EINVAL;
+
+ guard(mutex)(&st->lock);
+
+ rate = 4 * FIELD_GET(AD9910_ASF_RAMP_RATE_MSK,
+ st->reg[AD9910_REG_ASF].val32);
+ if (!rate)
+ return -ERANGE;
+
+ switch (private) {
+ case AD9910_OSK_AUTO_ROC:
+ nstep = ad9910_rational_scale((u64)val * NANO + val2, rate,
+ st->data.sysclk_freq_hz);
+ idx = find_closest(nstep, ad9910_osk_nstep,
+ ARRAY_SIZE(ad9910_osk_nstep));
+ if (idx == ARRAY_SIZE(ad9910_osk_nstep) - 1) {
+ cfg = AD9910_CFR1_OSK_MANUAL_EXT_CTL_MSK;
+ } else if (idx == 0) {
+ cfg = 0;
+ } else {
+ cfg = FIELD_PREP(AD9910_ASF_STEP_SIZE_MSK, idx - 1);
+ ret = ad9910_reg32_update(st, AD9910_REG_ASF,
+ AD9910_ASF_STEP_SIZE_MSK,
+ cfg, false);
+ if (ret)
+ return ret;
+
+ cfg = 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,
+ cfg, true);
+ if (ret)
+ return ret;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return len;
+}
+
#define AD9910_EXT_INFO_TMPL(_name, _ident, _shared, _fn_desc) { \
.name = _name, \
.read = ad9910_ ## _fn_desc ## _read, \
@@ -997,6 +1130,9 @@ static ssize_t ad9910_drg_attrs_write(struct iio_dev *indio_dev,
#define AD9910_DRG_EXT_INFO(_name, _ident) \
AD9910_EXT_INFO_TMPL(_name, _ident, IIO_SEPARATE, drg_attrs)
+#define AD9910_OSK_EXT_INFO(_name, _ident) \
+ AD9910_EXT_INFO_TMPL(_name, _ident, IIO_SEPARATE, osk_attrs)
+
static const struct iio_chan_spec_ext_info ad9910_phy_ext_info[] = {
AD9910_EXT_INFO("powerdown", AD9910_POWERDOWN, IIO_SEPARATE),
{ }
@@ -1018,6 +1154,12 @@ 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[] = {
+ AD9910_OSK_EXT_INFO("scale_roc", AD9910_OSK_AUTO_ROC),
+ AD9910_OSK_EXT_INFO("scale_roc_available", AD9910_OSK_AUTO_ROC_AVAIL),
+ { }
+};
+
#define AD9910_PROFILE_CHAN(idx) { \
.type = IIO_ALTVOLTAGE, \
.indexed = 1, \
@@ -1102,6 +1244,17 @@ static const struct iio_chan_spec ad9910_channels[] = {
BIT(IIO_CHAN_INFO_PHASE) |
BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
+ [AD9910_CHAN_IDX_OSK] = {
+ .type = IIO_ALTVOLTAGE,
+ .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_SCALE) |
+ BIT(IIO_CHAN_INFO_SAMP_FREQ),
+ .ext_info = ad9910_osk_ext_info,
+ },
};
static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -1134,6 +1287,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;
}
@@ -1239,6 +1396,12 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
*val = 0;
*val2 = tmp64 * NANO >> 32;
return IIO_VAL_INT_PLUS_NANO;
+ case AD9910_CHANNEL_OSK:
+ tmp64 = FIELD_GET(AD9910_ASF_SCALE_FACTOR_MSK,
+ st->reg[AD9910_REG_ASF].val32);
+ *val = 0;
+ *val2 = tmp64 * MICRO >> 14;
+ return IIO_VAL_INT_PLUS_MICRO;
default:
return -EINVAL;
}
@@ -1259,6 +1422,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;
}
@@ -1326,6 +1493,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;
}
@@ -1495,6 +1667,14 @@ 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:
+ tmp64 = ((u64)val * MICRO + val2) << 14;
+ tmp64 = DIV_U64_ROUND_CLOSEST(tmp64, MICRO);
+ tmp32 = min(tmp64, 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;
}
@@ -1531,6 +1711,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;
}
@@ -1630,6 +1815,7 @@ static const char * const ad9910_channel_str[] = {
[AD9910_CHAN_IDX_DRG_RAMP_UP] = "digital_ramp_up",
[AD9910_CHAN_IDX_DRG_RAMP_DOWN] = "digital_ramp_down",
[AD9910_CHAN_IDX_RAM] = "ram_control",
+ [AD9910_CHAN_IDX_OSK] = "output_shift_keying",
};
static int ad9910_read_label(struct iio_dev *indio_dev,
@@ -1873,6 +2059,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] 26+ messages in thread* Re: [PATCH RFC v4 07/10] iio: frequency: ad9910: add output shift keying support
2026-05-08 17:00 ` [PATCH RFC v4 07/10] iio: frequency: ad9910: add output shift keying support Rodrigo Alencar via B4 Relay
@ 2026-05-09 1:08 ` sashiko-bot
0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-05-09 1:08 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay; +Cc: robh, conor+dt, krzk+dt, devicetree
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] The OSK channel cannot be scaled because ad9910_write_raw_get_fmt() does not support AD9910_CHANNEL_OSK.
- [Medium] Integer wrap-around on 32-bit platforms leads to incorrect scale_roc configuration for out-of-bounds user inputs.
- [Medium] Writing non-standard truthy values to the OSK enable attribute turns the channel off instead of on.
- [Low] The scale_roc_available sysfs attribute is incorrectly created with write permissions.
--
commit d92799d64693d2c01054913b1a546994813e0150
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>
Subject: iio: frequency: ad9910: add output shift keying support
Add OSK channel with amplitude envelope control capabilities.
> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> index a153cd01e6f58..1fdbaba356d73 100644
> --- a/drivers/iio/frequency/ad9910.c
> +++ b/drivers/iio/frequency/ad9910.c
> @@ -980,6 +985,134 @@ static ssize_t ad9910_drg_attrs_write(struct iio_dev *indio_dev,
> return len;
> }
>
[ ... ]
> +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)
> +{
[ ... ]
> + switch (private) {
> + case AD9910_OSK_AUTO_ROC:
> + nstep = ad9910_rational_scale((u64)val * NANO + val2, rate,
> + st->data.sysclk_freq_hz);
> + idx = find_closest(nstep, ad9910_osk_nstep,
> + ARRAY_SIZE(ad9910_osk_nstep));
Is there a chance that a very large nstep value could cause issues on 32-bit
platforms here?
The find_closest() macro internally assigns the input to a local long variable.
If nstep is a u64 that exceeds LONG_MAX, it could wrap around to a negative
number, matching the 0th index instead of the maximum step size.
[ ... ]
> @@ -997,6 +1130,9 @@ static ssize_t ad9910_drg_attrs_write(struct iio_dev *indio_dev,
> #define AD9910_DRG_EXT_INFO(_name, _ident) \
> AD9910_EXT_INFO_TMPL(_name, _ident, IIO_SEPARATE, drg_attrs)
>
> +#define AD9910_OSK_EXT_INFO(_name, _ident) \
> + AD9910_EXT_INFO_TMPL(_name, _ident, IIO_SEPARATE, osk_attrs)
> +
> static const struct iio_chan_spec_ext_info ad9910_phy_ext_info[] = {
> AD9910_EXT_INFO("powerdown", AD9910_POWERDOWN, IIO_SEPARATE),
> { }
> @@ -1018,6 +1154,12 @@ 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[] = {
> + AD9910_OSK_EXT_INFO("scale_roc", AD9910_OSK_AUTO_ROC),
> + AD9910_OSK_EXT_INFO("scale_roc_available", AD9910_OSK_AUTO_ROC_AVAIL),
> + { }
> +};
> +
Does scale_roc_available end up with write permissions here?
The AD9910_OSK_EXT_INFO macro uses AD9910_EXT_INFO_TMPL, which assigns both
the .read and .write callbacks. This will cause the IIO core to create
the scale_roc_available sysfs attribute with 0644 permissions, but available
attributes are typically read-only.
[ ... ]
> @@ -1102,6 +1244,17 @@ static const struct iio_chan_spec ad9910_channels[] = {
> BIT(IIO_CHAN_INFO_PHASE) |
> BIT(IIO_CHAN_INFO_SAMP_FREQ),
> },
> + [AD9910_CHAN_IDX_OSK] = {
> + .type = IIO_ALTVOLTAGE,
> + .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_SCALE) |
> + BIT(IIO_CHAN_INFO_SAMP_FREQ),
> + .ext_info = ad9910_osk_ext_info,
> + },
> };
Since AD9910_CHANNEL_OSK is added with IIO_CHAN_INFO_SCALE here, does
ad9910_write_raw_get_fmt() also need to be updated to support this channel type?
If ad9910_write_raw_get_fmt() falls through to the default case and returns
-EINVAL, it looks like the IIO core might abort write operations for the scale
attribute before ad9910_write_raw() is even called.
[ ... ]
> @@ -1326,6 +1493,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;
> }
If val is a non-standard truthy value (like 2), wouldn't FIELD_PREP() mask it
to 0 and inadvertently disable the channel instead of enabling it?
Should this use !!val to sanitize the input, similar to how the RAM channel
does it?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com?part=7
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH RFC v4 08/10] iio: frequency: ad9910: show channel priority in debugfs
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
` (6 preceding siblings ...)
2026-05-08 17:00 ` [PATCH RFC v4 07/10] iio: frequency: ad9910: add output shift keying support Rodrigo Alencar via B4 Relay
@ 2026-05-08 17:00 ` Rodrigo Alencar via B4 Relay
2026-05-09 1:49 ` sashiko-bot
2026-05-08 17:00 ` [PATCH RFC v4 09/10] Documentation: ABI: testing: add docs for ad9910 sysfs entries Rodrigo Alencar via B4 Relay
` (2 subsequent siblings)
10 siblings, 1 reply; 26+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-05-08 17:00 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 the 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 | 171 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 171 insertions(+)
diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index 1fdbaba356d7..d8fe88259f22 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -2082,6 +2082,170 @@ 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];
+
+ /* Parallel data port enabled and data destination is frequency */
+ mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
+ st->reg[AD9910_REG_CFR1].val32);
+ if (mode_en) /* TODO: get destination from backend once it is supported */
+ return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_PORT];
+
+ /* 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 file *file, char __user *userbuf,
+ size_t count, loff_t *ppos)
+{
+ struct iio_dev *indio_dev = file->private_data;
+ const char *src = ad9910_frequency_source_get(indio_dev);
+
+ return simple_read_from_buffer(userbuf, count, ppos, src, strlen(src));
+}
+
+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];
+
+ /* Parallel data port enabled and data destination is phase */
+ mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
+ st->reg[AD9910_REG_CFR1].val32);
+ if (mode_en) /* TODO: get destination from backend once it is supported */
+ return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_PORT];
+
+ /* POW: RAM enabled and data destination is frequency, 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_phase_source_show(struct file *file, char __user *userbuf,
+ size_t count, loff_t *ppos)
+{
+ struct iio_dev *indio_dev = file->private_data;
+ const char *src = ad9910_phase_source_get(indio_dev);
+
+ return simple_read_from_buffer(userbuf, count, ppos, src, strlen(src));
+}
+
+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_CFR2].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];
+
+ /* Parallel data port enabled and data destination is phase */
+ mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
+ st->reg[AD9910_REG_CFR1].val32);
+ if (mode_en) /* TODO: get destination from backend once it is supported */
+ return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_PORT];
+
+ /* 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 file *file, char __user *userbuf,
+ size_t count, loff_t *ppos)
+{
+ struct iio_dev *indio_dev = file->private_data;
+ const char *src = ad9910_amplitude_source_get(indio_dev);
+
+ return simple_read_from_buffer(userbuf, count, ppos, src, strlen(src));
+}
+
+static const struct file_operations ad9910_frequency_source_fops = {
+ .owner = THIS_MODULE,
+ .read = ad9910_frequency_source_show,
+};
+
+static const struct file_operations ad9910_phase_source_fops = {
+ .owner = THIS_MODULE,
+ .read = ad9910_phase_source_show,
+};
+
+static const struct file_operations ad9910_amplitude_source_fops = {
+ .owner = THIS_MODULE,
+ .read = ad9910_amplitude_source_show,
+};
+
static inline void ad9910_debugfs_init(struct ad9910_state *st,
struct iio_dev *indio_dev)
{
@@ -2097,6 +2261,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] 26+ messages in thread* Re: [PATCH RFC v4 08/10] iio: frequency: ad9910: show channel priority in debugfs
2026-05-08 17:00 ` [PATCH RFC v4 08/10] iio: frequency: ad9910: show channel priority in debugfs Rodrigo Alencar via B4 Relay
@ 2026-05-09 1:49 ` sashiko-bot
0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-05-09 1:49 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay; +Cc: krzk+dt, devicetree, conor+dt, robh
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] Missing `.open` callback in debugfs `file_operations` leads to a guaranteed NULL pointer dereference.
- [High] Function prototype mismatch in `file_operations` `.read` callbacks causes build failures and CFI panics.
- [Medium] Register bitmasks are applied to the wrong control registers, leading to corrupted state evaluation.
- [Low] Using `simple_read_from_buffer` with dynamically evaluated string pointers creates a TOCTOU race condition.
- [Low] The strings exposed through debugfs lack a trailing newline.
--
commit f3ad86f1b54a4f67b1e1ded4fa3bd327c17e7ba9
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 to indicate which channel the specific DDS parameter is being sourced
from.
> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> index 1fdbaba356d73..d8fe88259f22d 100644
> --- a/drivers/iio/frequency/ad9910.c
> +++ b/drivers/iio/frequency/ad9910.c
> @@ -2082,6 +2082,170 @@ static int ad9910_setup(struct device *dev, struct ad9910_state *st,
[ ... ]
> + /* Parallel data port enabled and data destination is frequency */
> + mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
> + st->reg[AD9910_REG_CFR1].val32);
Does this apply a CFR2 mask to a CFR1 register? It seems
AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK should be evaluated against
st->reg[AD9910_REG_CFR2].
> + if (mode_en) /* TODO: get destination from backend once it is supported */
> + return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_PORT];
[ ... ]
> +static int ad9910_frequency_source_show(struct file *file, char __user *userbuf,
> + size_t count, loff_t *ppos)
Does the .read callback in struct file_operations require a return type of
ssize_t instead of int? Could this mismatch cause build failures or
Control Flow Integrity (CFI) kernel panics?
> +{
> + struct iio_dev *indio_dev = file->private_data;
Without an .open handler like simple_open(), the VFS and debugfs proxy
layers leave file->private_data initialized to NULL. Does this lead to a NULL
pointer dereference when iio_priv() extracts the state from indio_dev?
> + const char *src = ad9910_frequency_source_get(indio_dev);
Do the static strings returned here lack a trailing newline (\n)? Reading
this file might overwrite or corrupt the user's shell prompt.
> +
> + return simple_read_from_buffer(userbuf, count, ppos, src, strlen(src));
If userspace performs a partial read and the hardware state changes before
the next read(), could ad9910_frequency_source_get() return a different,
shorter string?
If this occurs, simple_read_from_buffer() would apply the old *ppos to the
new string, potentially returning garbage. Would using seq_file
via DEFINE_SHOW_ATTRIBUTE() safely cache the output buffer upon open()?
> +}
> +
> +static inline const char *ad9910_phase_source_get(struct iio_dev *indio_dev)
> +{
[ ... ]
> + /* Parallel data port enabled and data destination is phase */
> + mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
> + st->reg[AD9910_REG_CFR1].val32);
Is this another case where a CFR2 mask is applied to a CFR1 register?
> + if (mode_en) /* TODO: get destination from backend once it is supported */
> + return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_PORT];
[ ... ]
> +static int ad9910_phase_source_show(struct file *file, char __user *userbuf,
> + size_t count, loff_t *ppos)
Could this return type also be ssize_t instead of int to match the .read
callback signature?
> +{
> + struct iio_dev *indio_dev = file->private_data;
Does this have the same NULL pointer dereference issue as the frequency show
function due to missing simple_open()?
> + const char *src = ad9910_phase_source_get(indio_dev);
> +
> + return simple_read_from_buffer(userbuf, count, ppos, src, strlen(src));
> +}
> +
> +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_CFR2].val32);
Does this apply a CFR1 mask to a CFR2 register?
> + if (mode_en)
> + return ad9910_channel_str[AD9910_CHAN_IDX_OSK];
> +
[ ... ]
> + /* Parallel data port enabled and data destination is phase */
> + mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
> + st->reg[AD9910_REG_CFR1].val32);
Is this a third instance of applying a CFR2 mask to a CFR1 register?
> + if (mode_en) /* TODO: get destination from backend once it is supported */
> + return ad9910_channel_str[AD9910_CHAN_IDX_PARALLEL_PORT];
[ ... ]
> +static int ad9910_amplitude_source_show(struct file *file, char __user *userbuf,
> + size_t count, loff_t *ppos)
Could this return type also be ssize_t instead of int?
> +{
> + struct iio_dev *indio_dev = file->private_data;
Does this have the same NULL pointer dereference issue as the other show
functions?
> + const char *src = ad9910_amplitude_source_get(indio_dev);
> +
> + return simple_read_from_buffer(userbuf, count, ppos, src, strlen(src));
> +}
> +
> +static const struct file_operations ad9910_frequency_source_fops = {
> + .owner = THIS_MODULE,
> + .read = ad9910_frequency_source_show,
> +};
Should this struct include an .open callback, such as simple_open(), to ensure
that file->private_data is initialized correctly before .read is called?
> +
> +static const struct file_operations ad9910_phase_source_fops = {
> + .owner = THIS_MODULE,
> + .read = ad9910_phase_source_show,
> +};
> +
> +static const struct file_operations ad9910_amplitude_source_fops = {
> + .owner = THIS_MODULE,
> + .read = ad9910_amplitude_source_show,
> +};
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com?part=8
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH RFC v4 09/10] Documentation: ABI: testing: add docs for ad9910 sysfs entries
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
` (7 preceding siblings ...)
2026-05-08 17:00 ` [PATCH RFC v4 08/10] iio: frequency: ad9910: show channel priority in debugfs Rodrigo Alencar via B4 Relay
@ 2026-05-08 17:00 ` Rodrigo Alencar via B4 Relay
2026-05-09 1:24 ` sashiko-bot
2026-05-08 17:00 ` [PATCH RFC v4 10/10] docs: iio: add documentation for ad9910 driver Rodrigo Alencar via B4 Relay
2026-05-09 22:31 ` [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer David Lechner
10 siblings, 1 reply; 26+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-05-08 17:00 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 Parallel Port, Digital Ramp Generator and OSK parameters.
Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
.../ABI/testing/sysfs-bus-iio-frequency-ad9910 | 73 ++++++++++++++++++++++
MAINTAINERS | 1 +
2 files changed, 74 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..eb0ff96a6e10
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
@@ -0,0 +1,73 @@
+What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_frequency_offset
+KernelVersion:
+Contact: linux-iio@vger.kernel.org
+Description:
+ For a channel that allows frequency control through buffers, this
+ represents the base frequency value in Hz. The actual output frequency
+ is a result with the sum of this value.
+
+What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_frequency_scale
+KernelVersion:
+Contact: linux-iio@vger.kernel.org
+Description:
+ For a channel that allows frequency control through buffers, this
+ represents the frequency modulation gain. This value multiplies the
+ buffer input sample value before it is added to a frequency offset.
+
+What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_phase_offset
+KernelVersion:
+Contact: linux-iio@vger.kernel.org
+Description:
+ For a channel that allows phase control through buffers, this
+ represents the base phase value in radians. The actual output phase
+ is a result with the sum of this value.
+
+What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_scale_offset
+KernelVersion:
+Contact: linux-iio@vger.kernel.org
+Description:
+ For a channel that allows amplitude control through buffers, this
+ represents the value for a base amplitude scale. The actual output
+ amplitude scale is a result with the sum of this value.
+
+What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_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 limits. It enables dwell mode at a
+ sweep limit when set to 1. Otherwise, the sweep may stop at the initial
+ position or restart from that initial position or continue by reversing
+ its direction.
+
+What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_frequency_roc
+KernelVersion:
+Contact: linux-iio@vger.kernel.org
+Description:
+ Frequency rate of change in Hz/s for channels that produce linear
+ frequency sweeps. This value may be influenced by the channel's
+ sampling_frequency setting.
+
+What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_phase_roc
+KernelVersion:
+Contact: linux-iio@vger.kernel.org
+Description:
+ Phase rate of change in rad/s for channels that produce linear
+ phase sweeps. This value may be influenced by the channel's
+ sampling_frequency setting.
+
+What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_scale_roc
+KernelVersion:
+Contact: linux-iio@vger.kernel.org
+Description:
+ Amplitude scale rate of change in 1/s for channels that ramp
+ amplitude. This value may be influenced by the channel's
+ sampling_frequency setting.
+
+What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_scale_roc_available
+KernelVersion:
+Contact: linux-iio@vger.kernel.org
+Description:
+ Lists the available scale_roc values for the channel based on
+ the current sampling_frequency. Values are space-separated in
+ ascending order.
diff --git a/MAINTAINERS b/MAINTAINERS
index 6a53b202a84d..b52c0aae96b7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1643,6 +1643,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] 26+ messages in thread* Re: [PATCH RFC v4 09/10] Documentation: ABI: testing: add docs for ad9910 sysfs entries
2026-05-08 17:00 ` [PATCH RFC v4 09/10] Documentation: ABI: testing: add docs for ad9910 sysfs entries Rodrigo Alencar via B4 Relay
@ 2026-05-09 1:24 ` sashiko-bot
0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-05-09 1:24 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay; +Cc: robh, devicetree, krzk+dt, conor+dt
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] The newly added ABI documentation file omits the `Date:` field and leaves the `KernelVersion:` field empty for all entries.
- [Low] Awkward and confusing phrasing in the descriptions of multiple ABI attributes: 'is a result with the sum of this value'.
--
commit a42d44a5e6db95765e7fc42b4509b02bd033fda8
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>
Documentation: ABI: testing: add docs for ad9910 sysfs entries
This commit adds a custom ABI documentation file for the DDS AD9910 with
sysfs entries to control Parallel Port, Digital Ramp Generator and OSK
parameters.
> diff --git a/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910 b/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
> new file mode 100644
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
> @@ -0,0 +1,73 @@
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_frequency_offset
> +KernelVersion:
This isn't a bug, but the Date field is missing and the KernelVersion field
is empty for this and all subsequent entries in the file. Should these be
populated with the creation date and the expected kernel release?
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + For a channel that allows frequency control through buffers, this
> + represents the base frequency value in Hz. The actual output frequency
> + is a result with the sum of this value.
This isn't a bug, but the phrasing "is a result with the sum of this value"
is slightly confusing. Would it be clearer to phrase this as "is the sum of
this value and the input value"?
This same phrasing is also used below in the descriptions for
out_altvoltageY_phase_offset and out_altvoltageY_scale_offset.
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com?part=9
^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH RFC v4 10/10] docs: iio: add documentation for ad9910 driver
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
` (8 preceding siblings ...)
2026-05-08 17:00 ` [PATCH RFC v4 09/10] Documentation: ABI: testing: add docs for ad9910 sysfs entries Rodrigo Alencar via B4 Relay
@ 2026-05-08 17:00 ` Rodrigo Alencar via B4 Relay
2026-05-09 1:21 ` sashiko-bot
2026-05-09 23:42 ` David Lechner
2026-05-09 22:31 ` [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer David Lechner
10 siblings, 2 replies; 26+ messages in thread
From: Rodrigo Alencar via B4 Relay @ 2026-05-08 17:00 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 | 607 +++++++++++++++++++++++++++++++++++++++++++
Documentation/iio/index.rst | 1 +
MAINTAINERS | 1 +
3 files changed, 609 insertions(+)
diff --git a/Documentation/iio/ad9910.rst b/Documentation/iio/ad9910.rst
new file mode 100644
index 000000000000..7c5dad054d5f
--- /dev/null
+++ b/Documentation/iio/ad9910.rst
@@ -0,0 +1,607 @@
+.. 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 the IIO ``altvoltage`` channel type and supports
+five DDS operating modes: single tone, parallel port modulation, digital ramp
+generation (DRG), RAM playback and output shift keying (OSK). The device has
+8 hardware profiles, each capable of storing independent single tone and RAM
+playback parameters.
+
+
+Channel hierarchy
+=================
+
+The driver exposes the following IIO output channels, each identified by a
+unique channel number and a human-readable label:
+
+* ``out_altvoltage100``: ``phy``: Physical output: system clock and profile control
+
+ * ``out_altvoltage101``: ``profile[0]``: Single tone control for profile 0:
+ frequency, phase, amplitude
+
+ * ``out_altvoltage102``: ``profile[1]``: Single tone control for profile 1:
+ frequency, phase, amplitude
+
+ * ``out_altvoltage103``: ``profile[2]``: Single tone control for profile 2:
+ frequency, phase, amplitude
+
+ * ``out_altvoltage104``: ``profile[3]``: Single tone control for profile 3:
+ frequency, phase, amplitude
+
+ * ``out_altvoltage105``: ``profile[4]``: Single tone control for profile 4:
+ frequency, phase, amplitude
+
+ * ``out_altvoltage106``: ``profile[5]``: Single tone control for profile 5:
+ frequency, phase, amplitude
+
+ * ``out_altvoltage107``: ``profile[6]``: Single tone control for profile 6:
+ frequency, phase, amplitude
+
+ * ``out_altvoltage108``: ``profile[7]``: Single tone control for profile 7:
+ frequency, phase, amplitude
+
+ * ``out_altvoltage110``: ``parallel_port``: Parallel port modulation channel
+
+ * ``out_altvoltage120``: ``digital_ramp_generator``: DRG control: enable
+
+ * ``out_altvoltage121``: ``digital_ramp_up``: DRG ramp-up parameters:
+ dwell enable, limits, rate of change, ramp rate
+ * ``out_altvoltage122``: ``digital_ramp_down``: DRG ramp-down parameters:
+ dwell enable, limits, rate of change, ramp rate
+
+ * ``out_altvoltage130``: ``ram_control``: RAM playback: enable, frequency,
+ phase and sampling frequency for active profile. Other configurations are
+ provided through a firmware upload interface.
+
+ * ``out_altvoltage150``: ``output_shift_keying``: OSK: enable, amplitude
+ scale, ramp rate, rate of change control
+
+The ``phy`` channel is the root of the hierarchy. Changing its
+``sampling_frequency`` reconfigures the system clock (SYSCLK) which affects all
+other channels.
+
+Most of the mode-specific channels (single-tone, DRG, RAM, OSK) have an
+``enable`` attribute that turns the mode on/off.
+
+DDS modes
+=========
+
+The AD9910 supports multiple modes of operation that can be configured
+independently or in combination. Such modes and their corresponding IIO channels
+are described in this section. Each DDS core parameter (frequency, phase and
+amplitude) value 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 summarizes 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 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 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
+ - 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 ``profile[Y]`` channels
+provides 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. Then enabling a profile 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).
+
+ * - ``scale``
+ - fractional
+ - Amplitude scale factor. Range :math:`[0, 1]`. Stored in the profile's
+ amplitude scale factor (ASF).
+
+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 to 2 and set it as the active profile:
+
+.. code-block:: bash
+
+ echo 100000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage103_frequency
+ echo 0.5 > /sys/bus/iio/devices/iio:device0/out_altvoltage103_scale
+ echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage103_phase
+
+ # Activate profile 2
+ echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage103_en
+
+Read back the current single tone frequency:
+
+.. code-block:: bash
+
+ cat /sys/bus/iio/devices/iio:device0/out_altvoltage103_frequency
+
+Parallel Port mode
+------------------
+
+The parallel port allows real-time modulation of DDS parameters through a
+16-bit external data bus.
+
+.. flat-table::
+ :header-rows: 1
+
+ * - Attribute
+ - Unit
+ - Description
+
+ * - ``frequency_scale``
+ - power-of-2
+ - FM gain multiplier applied to 16-bit parallel input. Range :math:`[1, 32768]`,
+ must be a power of 2.
+
+ * - ``frequency_offset``
+ - Hz
+ - Base FTW to which scaled parallel data is added. Range :math:`[0, f_{SYSCLK}/2)`.
+
+ * - ``phase_offset``
+ - rad
+ - Base phase for polar modulation. Lower 8 bits of POW register.
+ Range :math:`[0, 2\pi/256)`.
+
+ * - ``scale_offset``
+ - fractional
+ - Base amplitude for polar modulation. Lower 6 bits of ASF register.
+ Range :math:`[0, 1/256)`.
+
+Usage examples
+^^^^^^^^^^^^^^
+
+Set parallel port frequency modulation with a scale of 16 and a 50 MHz
+offset:
+
+.. code-block:: bash
+
+ echo 16 > /sys/bus/iio/devices/iio:device0/out_altvoltage113_frequency_scale
+ echo 50000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage113_frequency_offset
+
+Digital ramp generator (DRG)
+----------------------------
+
+The DRG produces linear frequency, phase or amplitude sweeps using dedicated
+hardware. It is controlled through three channels: a parent control channel
+(``digital_ramp_generator``) and two child ramp channels
+(``digital_ramp_up``, ``digital_ramp_down``). DRG destination is set when
+ramp attributes are written, i.e. writing to ``frequency`` or ``frequency_roc``
+sets the destination to frequency.
+
+Control channel attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. flat-table::
+ :header-rows: 1
+
+ * - Attribute
+ - Unit
+ - Description
+
+ * - ``en``
+ - boolean
+ - Enable/disable the DRG.
+
+Ramp channel attributes
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``digital_ramp_up`` and ``digital_ramp_down`` channels share the same
+attribute set but configure ascending and descending ramp parameters
+independently:
+
+.. 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 configurations
+ create a single-shot ramp at the transition of the DRCTL pin: ramp-up
+ only, ramp-down only or bidirectional with dwell at the limits.
+
+ * - ``frequency``
+ - Hz
+ - Frequency ramp limit. Range: :math:`[0, f_{SYSCLK}/2)`. Writing a value
+ sets the ramp destination to frequency. Reading back returns the
+ currently active frequency limit or -EBUSY if other destination is
+ active (phase or amplitude).
+
+ * - ``phase``
+ - rad
+ - Phase ramp limit. Range: :math:`[0, 2\pi)`. Writing a value sets the
+ ramp destination to phase. Reading back returns the currently active
+ phase limit or -EBUSY if other destination is active (frequency or
+ amplitude).
+
+ * - ``scale``
+ - fractional
+ - Amplitude scale ramp limit. Range: :math:`[0, 1)`. Writing a value sets
+ the ramp destination to amplitude. Reading back returns the currently
+ active scale limit or -EBUSY if other destination is active (frequency
+ or phase).
+
+ * - ``sampling_frequency``
+ - Hz
+ - Ramp clock rate. It is controlled by an integer divider so the requested
+ value will adjust to nearest supported value.
+
+ * - ``frequency_roc``
+ - Hz/s
+ - Frequency rate of change. Sets the per-tick frequency increment/decrement
+ based on the current ramp clock rate.
+
+ * - ``phase_roc``
+ - rad/s
+ - Phase rate of change. Sets the per-tick phase increment/decrement based
+ on the current ramp clock rate.
+
+ * - ``scale_roc``
+ - 1/s
+ - Amplitude scale rate of change. Sets the per-tick amplitude scale
+ increment/decrement based on the current ramp clock rate.
+
+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_altvoltage121_dwell_en
+ echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_dwell_en
+
+ # Set ramp limits
+ echo 60000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage121_frequency
+ echo 40000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_frequency
+
+ # Set ramp rate
+ echo 25000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage121_sampling_frequency
+ echo 25000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_sampling_frequency
+
+ # Set frequency rate of change (Hz/s)
+ echo 25000000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage121_frequency_roc
+ echo 25000000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_frequency_roc
+
+ # Enable the DRG
+ echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage120_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. It is controlled by an integer divider so the
+ requested value will adjust to nearest supported value.
+
+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 FW data follows
+a simple binary format:
+
+- 80-byte header:
+
+ - 4-byte big-endian magic word: 0x00AD9910;
+ - 4-byte big-endian CFR1 value: configuration for the CFR1 register. Only
+ bits relevant to RAM mode (data destination and internal profile control)
+ are considered. Other bits are ignored and have no effect:
+
+ - Bits [30:29]: RAM data destination:
+
+ - 00: frequency;
+ - 01: phase;
+ - 10: amplitude;
+ - 11: polar;
+
+ - Bits [20:17]: Internal profile control (see Table 14 of the datasheet);
+
+ - 8 sets of 8-byte big-endian profile data for profiles 0-7. Each set contains:
+
+ - Bits [55:40]: Address step rate value;
+ - Bits [39:30]: End address for the profile;
+ - Bits [23:14]: Start address for the profile;
+ - Bit [5]: no-dwell high for ramp-up mode;
+ - Bit [3]: zero-crossing for direct-switch mode;
+ - Bits [2:0]: operating mode:
+
+ - 000: direct switch;
+ - 001: ramp-up;
+ - 010: bidirectional;
+ - 011: bidirectional continuous;
+ - 100: ramp-up continuous;
+
+ - 4-byte big-endian reserved word: set to 0;
+ - 4-byte big-endian word count: number of 32-bit words to be loaded (0-1024);
+
+- Followed by the specified number of 32-bit big-endian data words.
+
+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_altvoltage130_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.
+
+ * - ``scale``
+ - fractional
+ - Target amplitude for the OSK ramp. 14-bit ASF field. Range: :math:`[0, 1)`.
+
+ * - ``sampling_frequency``
+ - Hz
+ - OSK ramp rate. It is controlled by an integer divider so the requested
+ value will adjust to nearest supported value.
+
+ * - ``scale_roc``
+ - 1/s
+ - Amplitude scale rate of change. Writing a non-zero value enables
+ automatic OSK and selects the closest hardware step size. Writing ``0``
+ disables automatic ramping (manual control of the ASK register using
+ ``scale``). Writing the maximum available value enables pin-controlled
+ immediate transition with no ramping.
+
+ * - ``scale_roc_available``
+ - 1/s
+ - Lists the available ``scale_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.
+
+Usage examples
+^^^^^^^^^^^^^^
+
+Enable OSK with automatic ramping:
+
+.. code-block:: bash
+
+ # Set ramp rate
+ echo 1000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_sampling_frequency
+
+ # Check available rate of change values
+ cat /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale_roc_available
+
+ # Enable automatic OSK with a rate of change
+ echo 61.035000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale_roc
+
+ # Enable OSK
+ echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_en
+
+Enable pin-controlled immediate OSK:
+
+.. code-block:: bash
+
+ # Read the last (highest) available value for pin-controlled mode
+ cat /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale_roc_available
+
+ # Enable OSK in manual mode (no rate of change)
+ echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale_roc
+ echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_en
+
+ # Set target amplitude to full scale
+ echo 1.0 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale
+
+Physical channel
+================
+
+The ``phy`` channel provides device-level control:
+
+.. flat-table::
+ :header-rows: 1
+
+ * - Attribute
+ - Unit
+ - Description
+
+ * - ``sampling_frequency``
+ - Hz
+ - System clock (SYSCLK) frequency. With PLL enabled, configures the PLL
+ multiplier (range 420-1000 MHz). Without PLL, ref clock can only be
+ divided by 2.
+
+ * - ``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_altvoltage100_sampling_frequency
+
+Read current system clock frequency:
+
+.. code-block:: bash
+
+ cat /sys/bus/iio/devices/iio:device0/out_altvoltage100_sampling_frequency
+
+Power down the device:
+
+.. code-block:: bash
+
+ echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage100_powerdown
diff --git a/Documentation/iio/index.rst b/Documentation/iio/index.rst
index 007e0a1fcc5a..1ada7b460066 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
adis16475
adis16480
diff --git a/MAINTAINERS b/MAINTAINERS
index b52c0aae96b7..57bff3d169d5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1645,6 +1645,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] 26+ messages in thread* Re: [PATCH RFC v4 10/10] docs: iio: add documentation for ad9910 driver
2026-05-08 17:00 ` [PATCH RFC v4 10/10] docs: iio: add documentation for ad9910 driver Rodrigo Alencar via B4 Relay
@ 2026-05-09 1:21 ` sashiko-bot
2026-05-09 23:42 ` David Lechner
1 sibling, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-05-09 1:21 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay; +Cc: devicetree, krzk+dt, conor+dt, robh
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The parallel port channel ID used in the usage example does not match the documented channel hierarchy.
--
commit 734cfe5a204649de75b2933d43b7a6c5df484c59
Author: Rodrigo Alencar <rodrigo.alencar@analog.com>
docs: iio: add documentation for ad9910 driver
This commit adds documentation for the AD9910 DDS IIO driver. It describes
the channels, DDS operating modes, attributes, and provides ABI usage
examples.
> diff --git a/Documentation/iio/ad9910.rst b/Documentation/iio/ad9910.rst
> new file mode 100644
> index 0000000000000..7c5dad054d5fa
> --- /dev/null
> +++ b/Documentation/iio/ad9910.rst
[ ... ]
> + * ``out_altvoltage108``: ``profile[7]``: Single tone control for profile 7:
> + frequency, phase, amplitude
> +
> + * ``out_altvoltage110``: ``parallel_port``: Parallel port modulation channel
> +
> + * ``out_altvoltage120``: ``digital_ramp_generator``: DRG control: enable
[ ... ]
> +Usage examples
> +^^^^^^^^^^^^^^
> +
> +Set parallel port frequency modulation with a scale of 16 and a 50 MHz
> +offset:
> +
> +.. code-block:: bash
> +
> + echo 16 > /sys/bus/iio/devices/iio:device0/out_altvoltage113_frequency_scale
> + echo 50000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage113_frequency_offset
This isn't a bug, but the parallel port channel ID used in the usage example
does not match the earlier documented channel hierarchy. The example uses
out_altvoltage113, while the hierarchy section documents the parallel port
channel as out_altvoltage110.
Could the example be updated to use out_altvoltage110 to keep the
documentation consistent?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260508-ad9910-iio-driver-v4-0-d26bfd20ee3d@analog.com?part=10
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH RFC v4 10/10] docs: iio: add documentation for ad9910 driver
2026-05-08 17:00 ` [PATCH RFC v4 10/10] docs: iio: add documentation for ad9910 driver Rodrigo Alencar via B4 Relay
2026-05-09 1:21 ` sashiko-bot
@ 2026-05-09 23:42 ` David Lechner
2026-05-10 9:30 ` Rodrigo Alencar
1 sibling, 1 reply; 26+ messages in thread
From: David Lechner @ 2026-05-09 23:42 UTC (permalink / raw)
To: rodrigo.alencar, linux-iio, devicetree, linux-kernel, linux-doc,
linux-hardening
Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Philipp Zabel, Jonathan Corbet, Shuah Khan, Kees Cook,
Gustavo A. R. Silva
On 5/8/26 12:00 PM, Rodrigo Alencar via B4 Relay wrote:
> 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 | 607 +++++++++++++++++++++++++++++++++++++++++++
> Documentation/iio/index.rst | 1 +
> MAINTAINERS | 1 +
> 3 files changed, 609 insertions(+)
>
> diff --git a/Documentation/iio/ad9910.rst b/Documentation/iio/ad9910.rst
> new file mode 100644
> index 000000000000..7c5dad054d5f
> --- /dev/null
> +++ b/Documentation/iio/ad9910.rst
> @@ -0,0 +1,607 @@
> +.. 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 the IIO ``altvoltage`` channel type and supports
> +five DDS operating modes: single tone, parallel port modulation, digital ramp
> +generation (DRG), RAM playback and output shift keying (OSK). The device has
> +8 hardware profiles, each capable of storing independent single tone and RAM
> +playback parameters.
> +
> +
> +Channel hierarchy
> +=================
> +
> +The driver exposes the following IIO output channels, each identified by a
> +unique channel number and a human-readable label:
> +
Can we format this as a table with a header to make it clear what each item is?
I'm guessing that the second `` is the label?
And perhaps provide a link to the sections below that describe the common attributes
of each channel type?
> +* ``out_altvoltage100``: ``phy``: Physical output: system clock and profile control
Any attributes on this one?
> +
> + * ``out_altvoltage101``: ``profile[0]``: Single tone control for profile 0:
Why not just ``profile0``?
Also, why not ``out_altvoltage110`` so that the last digit matches the profile
index? It looks like we are skipping by 10s later anyway.
> + frequency, phase, amplitude
> +
> + * ``out_altvoltage102``: ``profile[1]``: Single tone control for profile 1:
> + frequency, phase, amplitude
> +
> + * ``out_altvoltage103``: ``profile[2]``: Single tone control for profile 2:
> + frequency, phase, amplitude
> +
> + * ``out_altvoltage104``: ``profile[3]``: Single tone control for profile 3:
> + frequency, phase, amplitude
> +
> + * ``out_altvoltage105``: ``profile[4]``: Single tone control for profile 4:
> + frequency, phase, amplitude
> +
> + * ``out_altvoltage106``: ``profile[5]``: Single tone control for profile 5:
> + frequency, phase, amplitude
> +
> + * ``out_altvoltage107``: ``profile[6]``: Single tone control for profile 6:
> + frequency, phase, amplitude
> +
> + * ``out_altvoltage108``: ``profile[7]``: Single tone control for profile 7:
> + frequency, phase, amplitude
> +
> + * ``out_altvoltage110``: ``parallel_port``: Parallel port modulation channel
I guess no attributes on this one yet since implementation is deferred?
> +
> + * ``out_altvoltage120``: ``digital_ramp_generator``: DRG control: enable
> +
> + * ``out_altvoltage121``: ``digital_ramp_up``: DRG ramp-up parameters:
> + dwell enable, limits, rate of change, ramp rate
> + * ``out_altvoltage122``: ``digital_ramp_down``: DRG ramp-down parameters:
> + dwell enable, limits, rate of change, ramp rate
Oh, I guess these are just the general "control knob" name, not the actual
sysfs attribute name.
> +
> + * ``out_altvoltage130``: ``ram_control``: RAM playback: enable, frequency,
> + phase and sampling frequency for active profile. Other configurations are
> + provided through a firmware upload interface.
> +
> + * ``out_altvoltage150``: ``output_shift_keying``: OSK: enable, amplitude
> + scale, ramp rate, rate of change control
> +
> +The ``phy`` channel is the root of the hierarchy. Changing its
> +``sampling_frequency`` reconfigures the system clock (SYSCLK) which affects all
> +other channels.
> +
> +Most of the mode-specific channels (single-tone, DRG, RAM, OSK) have an
> +``enable`` attribute that turns the mode on/off.
> +
> +DDS modes
> +=========
> +
> +The AD9910 supports multiple modes of operation that can be configured
> +independently or in combination. Such modes and their corresponding IIO channels
> +are described in this section. Each DDS core parameter (frequency, phase and
> +amplitude) value 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 summarizes 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 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 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
> + - 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 ``profile[Y]`` channels
> +provides 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. Then enabling a profile 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).
> +
> + * - ``scale``
> + - fractional
> + - Amplitude scale factor. Range :math:`[0, 1]`. Stored in the profile's
> + amplitude scale factor (ASF).
> +
> +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 to 2 and set it as the active profile:
> +
> +.. code-block:: bash
> +
> + echo 100000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage103_frequency
> + echo 0.5 > /sys/bus/iio/devices/iio:device0/out_altvoltage103_scale
> + echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage103_phase
> +
> + # Activate profile 2
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage103_en
> +
> +Read back the current single tone frequency:
> +
> +.. code-block:: bash
> +
> + cat /sys/bus/iio/devices/iio:device0/out_altvoltage103_frequency
> +
> +Parallel Port mode
> +------------------
> +
> +The parallel port allows real-time modulation of DDS parameters through a
> +16-bit external data bus.
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Attribute
> + - Unit
> + - Description
> +
> + * - ``frequency_scale``
> + - power-of-2
> + - FM gain multiplier applied to 16-bit parallel input. Range :math:`[1, 32768]`,
General comment for the whole doc. Can you spell out the acronyms the
first time they are used for us noobs.
> + must be a power of 2.
> +
> + * - ``frequency_offset``
> + - Hz
> + - Base FTW to which scaled parallel data is added. Range :math:`[0, f_{SYSCLK}/2)`.
> +
> + * - ``phase_offset``
> + - rad
> + - Base phase for polar modulation. Lower 8 bits of POW register.
> + Range :math:`[0, 2\pi/256)`.
> +
> + * - ``scale_offset``
> + - fractional
> + - Base amplitude for polar modulation. Lower 6 bits of ASF register.
> + Range :math:`[0, 1/256)`.
> +
I guess there was some discussion on these attributes. I see some of these in the
ad9832 driver in staging, but I'm guessing they are new ABI. It isn't clear to
me from the documentation here what they actually do though. I guess they are
just basic transformations on the input signal?
And a practical note, they should be "frequencyscale". I don't like that it is
harder to read, but it is easier for a machine to parse.
> +Usage examples
> +^^^^^^^^^^^^^^
> +
> +Set parallel port frequency modulation with a scale of 16 and a 50 MHz
> +offset:
> +
> +.. code-block:: bash
> +
> + echo 16 > /sys/bus/iio/devices/iio:device0/out_altvoltage113_frequency_scale
> + echo 50000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage113_frequency_offset
> +
> +Digital ramp generator (DRG)
> +----------------------------
> +
> +The DRG produces linear frequency, phase or amplitude sweeps using dedicated
> +hardware. It is controlled through three channels: a parent control channel
> +(``digital_ramp_generator``) and two child ramp channels
> +(``digital_ramp_up``, ``digital_ramp_down``). DRG destination is set when
> +ramp attributes are written, i.e. writing to ``frequency`` or ``frequency_roc``
> +sets the destination to frequency.
Would it be better to say that the destination is set when the the
value is non-zero? Otherwise, how would one change the destination
once set?
> +
> +Control channel attributes
> +^^^^^^^^^^^^^^^^^^^^^^^^^^
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Attribute
> + - Unit
> + - Description
> +
> + * - ``en``
> + - boolean
> + - Enable/disable the DRG.
> +
> +Ramp channel attributes
> +^^^^^^^^^^^^^^^^^^^^^^^^
> +
> +The ``digital_ramp_up`` and ``digital_ramp_down`` channels share the same
> +attribute set but configure ascending and descending ramp parameters
> +independently:
> +
> +.. 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 configurations
> + create a single-shot ramp at the transition of the DRCTL pin: ramp-up
> + only, ramp-down only or bidirectional with dwell at the limits.
> +
> + * - ``frequency``
> + - Hz
> + - Frequency ramp limit. Range: :math:`[0, f_{SYSCLK}/2)`. Writing a value
> + sets the ramp destination to frequency. Reading back returns the
> + currently active frequency limit or -EBUSY if other destination is
> + active (phase or amplitude).
> +
> + * - ``phase``
> + - rad
> + - Phase ramp limit. Range: :math:`[0, 2\pi)`. Writing a value sets the
> + ramp destination to phase. Reading back returns the currently active
> + phase limit or -EBUSY if other destination is active (frequency or
> + amplitude).
> +
> + * - ``scale``
> + - fractional
> + - Amplitude scale ramp limit. Range: :math:`[0, 1)`. Writing a value sets
> + the ramp destination to amplitude. Reading back returns the currently
> + active scale limit or -EBUSY if other destination is active (frequency
> + or phase).
> +
> + * - ``sampling_frequency``
> + - Hz
> + - Ramp clock rate. It is controlled by an integer divider so the requested
> + value will adjust to nearest supported value.
> +
> + * - ``frequency_roc``
> + - Hz/s
> + - Frequency rate of change. Sets the per-tick frequency increment/decrement
> + based on the current ramp clock rate.
> +
> + * - ``phase_roc``
> + - rad/s
> + - Phase rate of change. Sets the per-tick phase increment/decrement based
> + on the current ramp clock rate.
> +
> + * - ``scale_roc``
> + - 1/s
> + - Amplitude scale rate of change. Sets the per-tick amplitude scale
> + increment/decrement based on the current ramp clock rate.
> +
> +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_altvoltage121_dwell_en
> + echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_dwell_en
> +
> + # Set ramp limits
> + echo 60000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage121_frequency
> + echo 40000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_frequency
> +
> + # Set ramp rate
> + echo 25000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage121_sampling_frequency
> + echo 25000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_sampling_frequency
> +
> + # Set frequency rate of change (Hz/s)
> + echo 25000000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage121_frequency_roc
> + echo 25000000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_frequency_roc
> +
> + # Enable the DRG
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage120_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. It is controlled by an integer divider so the
> + requested value will adjust to nearest supported value.
> +
> +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 FW data follows
> +a simple binary format:
> +
> +- 80-byte header:
> +
> + - 4-byte big-endian magic word: 0x00AD9910;
> + - 4-byte big-endian CFR1 value: configuration for the CFR1 register. Only
> + bits relevant to RAM mode (data destination and internal profile control)
> + are considered. Other bits are ignored and have no effect:
> +
> + - Bits [30:29]: RAM data destination:
> +
> + - 00: frequency;
> + - 01: phase;
> + - 10: amplitude;
> + - 11: polar;
> +
> + - Bits [20:17]: Internal profile control (see Table 14 of the datasheet);
> +
> + - 8 sets of 8-byte big-endian profile data for profiles 0-7. Each set contains:
> +
> + - Bits [55:40]: Address step rate value;
> + - Bits [39:30]: End address for the profile;
> + - Bits [23:14]: Start address for the profile;
> + - Bit [5]: no-dwell high for ramp-up mode;
> + - Bit [3]: zero-crossing for direct-switch mode;
> + - Bits [2:0]: operating mode:
> +
> + - 000: direct switch;
> + - 001: ramp-up;
> + - 010: bidirectional;
> + - 011: bidirectional continuous;
> + - 100: ramp-up continuous;
> +
> + - 4-byte big-endian reserved word: set to 0;
Will it be enough? :-)
Another option could be to include a file format version field.
> + - 4-byte big-endian word count: number of 32-bit words to be loaded (0-1024);
> +
> +- Followed by the specified number of 32-bit big-endian data words.
> +
> +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_altvoltage130_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.
> +
> + * - ``scale``
> + - fractional
> + - Target amplitude for the OSK ramp. 14-bit ASF field. Range: :math:`[0, 1)`.
> +
> + * - ``sampling_frequency``
> + - Hz
> + - OSK ramp rate. It is controlled by an integer divider so the requested
> + value will adjust to nearest supported value.
> +
Would be more logical to to keep scale attributes together.
> + * - ``scale_roc``
> + - 1/s
> + - Amplitude scale rate of change. Writing a non-zero value enables
> + automatic OSK and selects the closest hardware step size. Writing ``0``
> + disables automatic ramping (manual control of the ASK register using
> + ``scale``). Writing the maximum available value enables pin-controlled
> + immediate transition with no ramping.
> +
> + * - ``scale_roc_available``
> + - 1/s
> + - Lists the available ``scale_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.
> +
> +Usage examples
> +^^^^^^^^^^^^^^
> +
> +Enable OSK with automatic ramping:
> +
> +.. code-block:: bash
> +
> + # Set ramp rate
> + echo 1000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_sampling_frequency
> +
> + # Check available rate of change values
> + cat /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale_roc_available
> +
> + # Enable automatic OSK with a rate of change
> + echo 61.035000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale_roc
> +
> + # Enable OSK
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_en
> +
> +Enable pin-controlled immediate OSK:
> +
> +.. code-block:: bash
> +
> + # Read the last (highest) available value for pin-controlled mode
> + cat /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale_roc_available
> +
> + # Enable OSK in manual mode (no rate of change)
> + echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale_roc
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_en
> +
> + # Set target amplitude to full scale
> + echo 1.0 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale
> +
> +Physical channel
> +================
> +
> +The ``phy`` channel provides device-level control:
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Attribute
> + - Unit
> + - Description
> +
> + * - ``sampling_frequency``
> + - Hz
> + - System clock (SYSCLK) frequency. With PLL enabled, configures the PLL
> + multiplier (range 420-1000 MHz). Without PLL, ref clock can only be
> + divided by 2.
What controls the PLL?
> +
> + * - ``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_altvoltage100_sampling_frequency
> +
> +Read current system clock frequency:
> +
> +.. code-block:: bash
> +
> + cat /sys/bus/iio/devices/iio:device0/out_altvoltage100_sampling_frequency
> +
> +Power down the device:
> +
> +.. code-block:: bash
> +
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage100_powerdown
> diff --git a/Documentation/iio/index.rst b/Documentation/iio/index.rst
> index 007e0a1fcc5a..1ada7b460066 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
> adis16475
> adis16480
> diff --git a/MAINTAINERS b/MAINTAINERS
> index b52c0aae96b7..57bff3d169d5 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -1645,6 +1645,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
>
I like the direction this is going. Looks sensible to me.
I didn't have time to read the code, so just going off of the docs for now.
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH RFC v4 10/10] docs: iio: add documentation for ad9910 driver
2026-05-09 23:42 ` David Lechner
@ 2026-05-10 9:30 ` Rodrigo Alencar
0 siblings, 0 replies; 26+ messages in thread
From: Rodrigo Alencar @ 2026-05-10 9:30 UTC (permalink / raw)
To: David Lechner, rodrigo.alencar, linux-iio, devicetree,
linux-kernel, linux-doc, linux-hardening
Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Philipp Zabel, Jonathan Corbet, Shuah Khan, Kees Cook,
Gustavo A. R. Silva
On 26/05/09 06:42PM, David Lechner wrote:
> On 5/8/26 12:00 PM, Rodrigo Alencar via B4 Relay wrote:
> > 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.
...
> > +Channel hierarchy
> > +=================
> > +
> > +The driver exposes the following IIO output channels, each identified by a
> > +unique channel number and a human-readable label:
> > +
>
> Can we format this as a table with a header to make it clear what each item is?
> I'm guessing that the second `` is the label?
>
> And perhaps provide a link to the sections below that describe the common attributes
> of each channel type?
Yes, that is a label. A table is better indeed.
> > +* ``out_altvoltage100``: ``phy``: Physical output: system clock and profile control
>
> Any attributes on this one?
>
> > +
> > + * ``out_altvoltage101``: ``profile[0]``: Single tone control for profile 0:
>
> Why not just ``profile0``?
>
> Also, why not ``out_altvoltage110`` so that the last digit matches the profile
> index? It looks like we are skipping by 10s later anyway.
Yeah, that can be done. I thought of out_altvoltage110 being a channel to hold common
things between profiles, but it ended up empty so I left the spot as a placeholder.
> > + frequency, phase, amplitude
> > +
> > + * ``out_altvoltage102``: ``profile[1]``: Single tone control for profile 1:
> > + frequency, phase, amplitude
> > +
> > + * ``out_altvoltage103``: ``profile[2]``: Single tone control for profile 2:
> > + frequency, phase, amplitude
> > +
> > + * ``out_altvoltage104``: ``profile[3]``: Single tone control for profile 3:
> > + frequency, phase, amplitude
> > +
> > + * ``out_altvoltage105``: ``profile[4]``: Single tone control for profile 4:
> > + frequency, phase, amplitude
> > +
> > + * ``out_altvoltage106``: ``profile[5]``: Single tone control for profile 5:
> > + frequency, phase, amplitude
> > +
> > + * ``out_altvoltage107``: ``profile[6]``: Single tone control for profile 6:
> > + frequency, phase, amplitude
> > +
> > + * ``out_altvoltage108``: ``profile[7]``: Single tone control for profile 7:
> > + frequency, phase, amplitude
> > +
> > + * ``out_altvoltage110``: ``parallel_port``: Parallel port modulation channel
>
> I guess no attributes on this one yet since implementation is deferred?
Only basic knobs will be exposed, proper implementation will come with a later backend
support.
> > +
> > + * ``out_altvoltage120``: ``digital_ramp_generator``: DRG control: enable
> > +
> > + * ``out_altvoltage121``: ``digital_ramp_up``: DRG ramp-up parameters:
> > + dwell enable, limits, rate of change, ramp rate
> > + * ``out_altvoltage122``: ``digital_ramp_down``: DRG ramp-down parameters:
> > + dwell enable, limits, rate of change, ramp rate
>
> Oh, I guess these are just the general "control knob" name, not the actual
> sysfs attribute name.
Correct, just a description.
>
> > +
> > + * ``out_altvoltage130``: ``ram_control``: RAM playback: enable, frequency,
> > + phase and sampling frequency for active profile. Other configurations are
> > + provided through a firmware upload interface.
> > +
> > + * ``out_altvoltage150``: ``output_shift_keying``: OSK: enable, amplitude
> > + scale, ramp rate, rate of change control
> > +
> > +The ``phy`` channel is the root of the hierarchy. Changing its
> > +``sampling_frequency`` reconfigures the system clock (SYSCLK) which affects all
> > +other channels.
> > +
> > +Most of the mode-specific channels (single-tone, DRG, RAM, OSK) have an
> > +``enable`` attribute that turns the mode on/off.
> > +
...
> > +Parallel Port mode
> > +------------------
> > +
> > +The parallel port allows real-time modulation of DDS parameters through a
> > +16-bit external data bus.
> > +
> > +.. flat-table::
> > + :header-rows: 1
> > +
> > + * - Attribute
> > + - Unit
> > + - Description
> > +
> > + * - ``frequency_scale``
> > + - power-of-2
> > + - FM gain multiplier applied to 16-bit parallel input. Range :math:`[1, 32768]`,
>
> General comment for the whole doc. Can you spell out the acronyms the
> first time they are used for us noobs.
>
> > + must be a power of 2.
> > +
> > + * - ``frequency_offset``
> > + - Hz
> > + - Base FTW to which scaled parallel data is added. Range :math:`[0, f_{SYSCLK}/2)`.
> > +
> > + * - ``phase_offset``
> > + - rad
> > + - Base phase for polar modulation. Lower 8 bits of POW register.
> > + Range :math:`[0, 2\pi/256)`.
> > +
> > + * - ``scale_offset``
> > + - fractional
> > + - Base amplitude for polar modulation. Lower 6 bits of ASF register.
> > + Range :math:`[0, 1/256)`.
> > +
>
> I guess there was some discussion on these attributes. I see some of these in the
> ad9832 driver in staging, but I'm guessing they are new ABI. It isn't clear to
> me from the documentation here what they actually do though. I guess they are
> just basic transformations on the input signal?
Not sure how the ABI is not clear:
For a channel that allows amplitude control through buffers, this
represents the value for a base amplitude scale. The actual output
amplitude scale is a result with the sum of this value.
So yes, it is a basic transformation.
>
> And a practical note, they should be "frequencyscale". I don't like that it is
> harder to read, but it is easier for a machine to parse.
Parsers like the ones in libiio is not having problems with that.
> > +Usage examples
> > +^^^^^^^^^^^^^^
> > +
> > +Set parallel port frequency modulation with a scale of 16 and a 50 MHz
> > +offset:
> > +
> > +.. code-block:: bash
> > +
> > + echo 16 > /sys/bus/iio/devices/iio:device0/out_altvoltage113_frequency_scale
> > + echo 50000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage113_frequency_offset
> > +
> > +Digital ramp generator (DRG)
> > +----------------------------
> > +
> > +The DRG produces linear frequency, phase or amplitude sweeps using dedicated
> > +hardware. It is controlled through three channels: a parent control channel
> > +(``digital_ramp_generator``) and two child ramp channels
> > +(``digital_ramp_up``, ``digital_ramp_down``). DRG destination is set when
> > +ramp attributes are written, i.e. writing to ``frequency`` or ``frequency_roc``
> > +sets the destination to frequency.
>
> Would it be better to say that the destination is set when the the
> value is non-zero? Otherwise, how would one change the destination
> once set?
Destination is only one, so you just need to write phase or phase_roc, if you want
to target phase then. Does that not sound intuitive?
Zero is a valid value to be written.
>
> > +
> > +Control channel attributes
> > +^^^^^^^^^^^^^^^^^^^^^^^^^^
> > +
> > +.. flat-table::
> > + :header-rows: 1
> > +
> > + * - Attribute
> > + - Unit
> > + - Description
> > +
> > + * - ``en``
> > + - boolean
> > + - Enable/disable the DRG.
> > +
> > +Ramp channel attributes
> > +^^^^^^^^^^^^^^^^^^^^^^^^
> > +
> > +The ``digital_ramp_up`` and ``digital_ramp_down`` channels share the same
> > +attribute set but configure ascending and descending ramp parameters
> > +independently:
> > +
> > +.. 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 configurations
> > + create a single-shot ramp at the transition of the DRCTL pin: ramp-up
> > + only, ramp-down only or bidirectional with dwell at the limits.
> > +
> > + * - ``frequency``
> > + - Hz
> > + - Frequency ramp limit. Range: :math:`[0, f_{SYSCLK}/2)`. Writing a value
> > + sets the ramp destination to frequency. Reading back returns the
> > + currently active frequency limit or -EBUSY if other destination is
> > + active (phase or amplitude).
> > +
> > + * - ``phase``
> > + - rad
> > + - Phase ramp limit. Range: :math:`[0, 2\pi)`. Writing a value sets the
> > + ramp destination to phase. Reading back returns the currently active
> > + phase limit or -EBUSY if other destination is active (frequency or
> > + amplitude).
> > +
> > + * - ``scale``
> > + - fractional
> > + - Amplitude scale ramp limit. Range: :math:`[0, 1)`. Writing a value sets
> > + the ramp destination to amplitude. Reading back returns the currently
> > + active scale limit or -EBUSY if other destination is active (frequency
> > + or phase).
> > +
> > + * - ``sampling_frequency``
> > + - Hz
> > + - Ramp clock rate. It is controlled by an integer divider so the requested
> > + value will adjust to nearest supported value.
> > +
> > + * - ``frequency_roc``
> > + - Hz/s
> > + - Frequency rate of change. Sets the per-tick frequency increment/decrement
> > + based on the current ramp clock rate.
> > +
> > + * - ``phase_roc``
> > + - rad/s
> > + - Phase rate of change. Sets the per-tick phase increment/decrement based
> > + on the current ramp clock rate.
> > +
> > + * - ``scale_roc``
> > + - 1/s
> > + - Amplitude scale rate of change. Sets the per-tick amplitude scale
> > + increment/decrement based on the current ramp clock rate.
> > +
> > +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_altvoltage121_dwell_en
> > + echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_dwell_en
> > +
> > + # Set ramp limits
> > + echo 60000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage121_frequency
> > + echo 40000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_frequency
> > +
> > + # Set ramp rate
> > + echo 25000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage121_sampling_frequency
> > + echo 25000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_sampling_frequency
> > +
> > + # Set frequency rate of change (Hz/s)
> > + echo 25000000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage121_frequency_roc
> > + echo 25000000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage122_frequency_roc
> > +
> > + # Enable the DRG
> > + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage120_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. It is controlled by an integer divider so the
> > + requested value will adjust to nearest supported value.
> > +
> > +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 FW data follows
> > +a simple binary format:
> > +
> > +- 80-byte header:
> > +
> > + - 4-byte big-endian magic word: 0x00AD9910;
> > + - 4-byte big-endian CFR1 value: configuration for the CFR1 register. Only
> > + bits relevant to RAM mode (data destination and internal profile control)
> > + are considered. Other bits are ignored and have no effect:
> > +
> > + - Bits [30:29]: RAM data destination:
> > +
> > + - 00: frequency;
> > + - 01: phase;
> > + - 10: amplitude;
> > + - 11: polar;
> > +
> > + - Bits [20:17]: Internal profile control (see Table 14 of the datasheet);
> > +
> > + - 8 sets of 8-byte big-endian profile data for profiles 0-7. Each set contains:
> > +
> > + - Bits [55:40]: Address step rate value;
> > + - Bits [39:30]: End address for the profile;
> > + - Bits [23:14]: Start address for the profile;
> > + - Bit [5]: no-dwell high for ramp-up mode;
> > + - Bit [3]: zero-crossing for direct-switch mode;
> > + - Bits [2:0]: operating mode:
> > +
> > + - 000: direct switch;
> > + - 001: ramp-up;
> > + - 010: bidirectional;
> > + - 011: bidirectional continuous;
> > + - 100: ramp-up continuous;
> > +
> > + - 4-byte big-endian reserved word: set to 0;
>
> Will it be enough? :-)
>
> Another option could be to include a file format version field.
Yeah, maybe a CRC and a version as you pointed out. In terms of RAM functionality,
that would be all. Maybe a table for this one too...
> > + - 4-byte big-endian word count: number of 32-bit words to be loaded (0-1024);
> > +
> > +- Followed by the specified number of 32-bit big-endian data words.
> > +
> > +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_altvoltage130_en
> > +
...
> > +Physical channel
> > +================
> > +
> > +The ``phy`` channel provides device-level control:
> > +
> > +.. flat-table::
> > + :header-rows: 1
> > +
> > + * - Attribute
> > + - Unit
> > + - Description
> > +
> > + * - ``sampling_frequency``
> > + - Hz
> > + - System clock (SYSCLK) frequency. With PLL enabled, configures the PLL
> > + multiplier (range 420-1000 MHz). Without PLL, ref clock can only be
> > + divided by 2.
>
> What controls the PLL?
It gets enabled in the device-tree. One would want that when feeding a lower clock rate
source as a reference clock. It would also need a loop-filter connected to the device.
This property can be used to configure the desired sysclk frequency, the PLL divider/multiplier
and VCO configs will be derived from that.
> > +
> > + * - ``powerdown``
> > + - boolean (0 or 1)
> > + - Software power-down. Writing 1 powers down the digital core, DAC,
> > + reference clock input and auxiliary DAC simultaneously.
> > +
...
>
> I like the direction this is going. Looks sensible to me.
>
> I didn't have time to read the code, so just going off of the docs for now.
Thanks for the review. The code would need some cleanup after the ABI is mature.
Also, sashiko is pointing out a lot of issues already.. so those I suppose I can
handle on my own for now.
--
Kind regards,
Rodrigo Alencar
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer
2026-05-08 17:00 [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer Rodrigo Alencar via B4 Relay
` (9 preceding siblings ...)
2026-05-08 17:00 ` [PATCH RFC v4 10/10] docs: iio: add documentation for ad9910 driver Rodrigo Alencar via B4 Relay
@ 2026-05-09 22:31 ` David Lechner
2026-05-10 8:50 ` Rodrigo Alencar
10 siblings, 1 reply; 26+ messages in thread
From: David Lechner @ 2026-05-09 22:31 UTC (permalink / raw)
To: rodrigo.alencar, linux-iio, devicetree, linux-kernel, linux-doc,
linux-hardening
Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Philipp Zabel, Jonathan Corbet, Shuah Khan, Kees Cook,
Gustavo A. R. Silva
On 5/8/26 12:00 PM, Rodrigo Alencar via B4 Relay wrote:
> This patch series adds support for the Analog Devices AD9910 DDS.
> This is a RFC so that we can agree/discuss on the design that follows:
>
> This is a follow-up of the V3 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, there was
> further discussion on the ABI and on mode priority debug.
>
What happened with the idea of adding a new attribute to show the
relationship of the sub-channels to the actual physical output
channels?
^ permalink raw reply [flat|nested] 26+ messages in thread* Re: [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer
2026-05-09 22:31 ` [PATCH RFC v4 00/10] AD9910 Direct Digital Synthesizer David Lechner
@ 2026-05-10 8:50 ` Rodrigo Alencar
0 siblings, 0 replies; 26+ messages in thread
From: Rodrigo Alencar @ 2026-05-10 8:50 UTC (permalink / raw)
To: David Lechner, rodrigo.alencar, linux-iio, devicetree,
linux-kernel, linux-doc, linux-hardening
Cc: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Philipp Zabel, Jonathan Corbet, Shuah Khan, Kees Cook,
Gustavo A. R. Silva
On 26/05/09 05:31PM, David Lechner wrote:
> On 5/8/26 12:00 PM, Rodrigo Alencar via B4 Relay wrote:
> > This patch series adds support for the Analog Devices AD9910 DDS.
> > This is a RFC so that we can agree/discuss on the design that follows:
> >
> > This is a follow-up of the V3 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, there was
> > further discussion on the ABI and on mode priority debug.
> >
> What happened with the idea of adding a new attribute to show the
> relationship of the sub-channels to the actual physical output
> channels?
That's still to be done in iio core. I was still to think on how to do that,
and I am trying to get a mature ABI first.
I am not sure about the use case where a sub-channel is shared between
multiple channels, but I thought of a iio_chan_spec pointer to a parent
iio_chan_spec in the same struct. Similar to a device-tree, we have the
primary tree structure and then phandles can be used separately to create
more complex dependencies between channels. So a "channel ref" attribute
could be separate concept.
Then iio core would create the read-only attribute "subcomponent_of" or
"parent" when that is not NULL. The read function would just output the parent
channel label. Then labels would be important to create this logical dependency
between channels, and maybe that is bad, but in this context, I suppose labels
are going to be needed anyways.
--
Kind regards,
Rodrigo Alencar
^ permalink raw reply [flat|nested] 26+ messages in thread