Linux Documentation
 help / color / mirror / Atom feed
* Re: [RFC PATCH v3 1/3] scripts: add kconfirm
From: Demi Marie Obenour @ 2026-05-17 20:25 UTC (permalink / raw)
  To: Miguel Ojeda
  Cc: Julian Braha, nathan, nsc, jani.nikula, akpm, gary, ljs, arnd,
	gregkh, masahiroy, ojeda, corbet, qingfang.deng, yann.prono, ej,
	linux-kernel, rust-for-linux, linux-doc, linux-kbuild
In-Reply-To: <CANiq72k_tXGSCd1BEg8XmTr+acZHfdRbcFOVD7=O6yAbmv-nHw@mail.gmail.com>


[-- Attachment #1.1.1: Type: text/plain, Size: 1229 bytes --]

On 5/17/26 05:58, Miguel Ojeda wrote:
> On Sun, May 17, 2026 at 8:10 AM Demi Marie Obenour
> <demiobenour@gmail.com> wrote:
>>
>> I think it is simpler to just inline all of this code into its
>> single call-site.  The safety of the code is obvious in context,
>> and you can avoid checking for impossible errors.  For instance,
>> since all of the options have required arguments, it really is safe
>> to dereference optarg without any null check.
> 
> If we are going to have unsafe code, then let's please build safe
> abstractions wherever possible, just like we do elsewhere. We should
> also write `// SAFETY` comments and enable the lints that catch that
> etc., just like elsewhere too.
> 
> (This is not to say we should use `getopt` instead of something like
> `clap` -- as soon as we start using `cargo vendor`, then it makes
> sense to at least consider having a set of vetted, well-known crates
> to write Rust tools in-tree, as I mentioned in v1.)

I was hoping for Linux to avoid the Rust trend of downloading tons
of third-party crates, with all the supply-chain risks that entails.
Hence the idea of using getopt and system C libraries.
-- 
Sincerely,
Demi Marie Obenour (she/her/hers)

[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]

[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

^ permalink raw reply

* [PATCH v5 2/2] docs: spi: add documentation for userspace device instantiation
From: Vishwaroop A @ 2026-05-17 20:16 UTC (permalink / raw)
  To: broonie, linux-spi
  Cc: smangipudi, jonathanh, thierry.reding, corbet, linux-doc, va
In-Reply-To: <20260517201602.498135-1-va@nvidia.com>

Document the new_device and delete_device sysfs attributes on SPI
controllers:

  - Documentation/spi/instantiating-devices.rst: describes when and
    why this interface is needed, accepted parameters, usage examples,
    and limitations.
  - Documentation/ABI/testing/sysfs-class-spi-master: formal ABI
    entry for both attributes.

Signed-off-by: Vishwaroop A <va@nvidia.com>
---
 .../ABI/testing/sysfs-class-spi-master        | 34 +++++++
 Documentation/spi/index.rst                   |  1 +
 Documentation/spi/instantiating-devices.rst   | 88 +++++++++++++++++++
 3 files changed, 123 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-class-spi-master
 create mode 100644 Documentation/spi/instantiating-devices.rst

diff --git a/Documentation/ABI/testing/sysfs-class-spi-master b/Documentation/ABI/testing/sysfs-class-spi-master
new file mode 100644
index 000000000000..b498be128bad
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-class-spi-master
@@ -0,0 +1,34 @@
+What:		/sys/class/spi_master/spiB/new_device
+Date:		April 2026
+KernelVersion:	7.2
+Contact:	linux-spi@vger.kernel.org
+Description:	(WO) Instantiate a new SPI device on bus B, where B
+		is the bus number (0, 1, 2, ...). Takes parameters
+		in the format:
+
+		<modalias> <chip_select> [<max_speed_hz> [<mode>]]
+
+		where modalias is the driver name, chip_select is the
+		CS line number, and max_speed_hz and mode are optional.
+
+		The device can later be removed with delete_device.
+
+		Only devices created via this interface can be removed
+		with delete_device; platform and DT devices are not
+		affected.
+
+		Example:
+		# echo spidev 0 > /sys/class/spi_master/spi0/new_device
+		# echo spidev 0 10000000 > /sys/class/spi_master/spi0/new_device
+		# echo spidev 0 10000000 3 > /sys/class/spi_master/spi0/new_device
+
+What:		/sys/class/spi_master/spiB/delete_device
+Date:		April 2026
+KernelVersion:	7.2
+Contact:	linux-spi@vger.kernel.org
+Description:	(WO) Remove a SPI device previously created via
+		new_device. Takes a single parameter: the chip select
+		number of the device to remove.
+
+		Example:
+		# echo 0 > /sys/class/spi_master/spi0/delete_device
diff --git a/Documentation/spi/index.rst b/Documentation/spi/index.rst
index ac0c2233ce48..3f723e2c07da 100644
--- a/Documentation/spi/index.rst
+++ b/Documentation/spi/index.rst
@@ -8,6 +8,7 @@ Serial Peripheral Interface (SPI)
    :maxdepth: 1
 
    spi-summary
+   instantiating-devices
    spidev
    multiple-data-lanes
    butterfly
diff --git a/Documentation/spi/instantiating-devices.rst b/Documentation/spi/instantiating-devices.rst
new file mode 100644
index 000000000000..9ed08d94ae01
--- /dev/null
+++ b/Documentation/spi/instantiating-devices.rst
@@ -0,0 +1,88 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+==============================
+How to instantiate SPI devices
+==============================
+
+SPI devices are normally declared statically via device-tree, ACPI, or
+board files. When the SPI controller is registered, these devices are
+instantiated automatically by the SPI core. This is the preferred method
+for any device with a proper kernel driver.
+
+Instantiate from user-space
+---------------------------
+
+In certain cases a SPI device cannot be declared statically:
+
+* The ``spidev`` driver, which provides raw userspace access to SPI
+  buses, explicitly rejects the bare ``"spidev"`` compatible string in
+  device-tree because spidev is a Linux implementation detail, not a
+  hardware description. Vendor-specific compatible strings for spidev
+  (e.g. ``"vendor,board-spidev"``) are also generally not accepted
+  upstream. Device-tree overlays do not help here either, since the
+  spidev driver performs the same compatible check regardless of how
+  the DT node was loaded.
+
+* You are developing or testing a SPI device on a development board
+  where the SPI bus is exposed on expansion headers, and the connected
+  device may change frequently.
+
+For these cases, a sysfs interface is provided on each SPI controller
+(similar to the I2C ``new_device``/``delete_device`` interface described
+in Documentation/i2c/instantiating-devices.rst). Two write-only
+attribute files are created in every SPI controller directory:
+``new_device`` and ``delete_device``.
+
+File ``new_device`` takes 2 to 4 parameters: the name of the SPI
+device (a string), the chip select number, and optionally
+``max_speed_hz`` and ``mode``::
+
+  <modalias> <chip_select> [<max_speed_hz> [<mode>]]
+
+The modalias is set both as the device's ``modalias`` field and as its
+``driver_override``. This ensures that the device binds to the named
+driver directly, bypassing the normal bus matching logic (OF, ACPI,
+and ``id_table``). This is necessary because drivers like ``spidev``
+deliberately exclude generic names from their ``id_table``.
+
+If ``max_speed_hz`` is omitted or 0, ``spi_setup()`` clamps it to
+the controller's maximum speed. If ``mode`` is omitted, SPI mode 0
+(CPOL=0, CPHA=0) is used.
+
+File ``delete_device`` takes a single parameter: the chip select
+number. As no two devices can share a chip select on a given SPI bus,
+the chip select is sufficient to uniquely identify the device.
+
+Examples::
+
+  # Create a spidev device on SPI bus 0, chip select 0
+  echo spidev 0 > /sys/class/spi_master/spi0/new_device
+
+  # Create with explicit clock rate and SPI mode
+  echo spidev 0 10000000 3 > /sys/class/spi_master/spi0/new_device
+
+  # Remove the device
+  echo 0 > /sys/class/spi_master/spi0/delete_device
+
+On systems that need spidev access at boot, a systemd service or
+udev rule can write to ``new_device`` after the SPI controller is
+available.
+
+Limitations
+^^^^^^^^^^^
+
+Devices created through this interface have the following limitations
+compared to devices declared via device-tree:
+
+* No interrupt (IRQ) support.
+* No additional properties such as ``spi-max-frequency`` DT bindings
+  or controller-specific configuration.
+* No platform data or software nodes.
+
+For ``spidev`` usage these limitations are not relevant, since spidev
+provides a raw byte-level interface that does not require any of these
+features.
+
+Only devices created via ``new_device`` can be removed through
+``delete_device``. Devices declared via device-tree, ACPI, or board
+files are not affected by this interface.
-- 
2.17.1


^ permalink raw reply related

* [PATCH v5 1/2] spi: add new_device/delete_device sysfs interface
From: Vishwaroop A @ 2026-05-17 20:16 UTC (permalink / raw)
  To: broonie, linux-spi
  Cc: smangipudi, jonathanh, thierry.reding, corbet, linux-doc, va
In-Reply-To: <20260517201602.498135-1-va@nvidia.com>

Development boards such as the Jetson AGX Orin expose SPI buses
on expansion headers (e.g. the 40-pin header) so that users can
connect and interact with SPI peripherals from userspace. The
standard way to get /dev/spidevB.C character device nodes for
this purpose is to register spi_device instances backed by the
spidev driver.

Today there is no viable way to do this on upstream kernels:

  - The spidev driver rejects the bare "spidev" compatible
    string in DT, since spidev is a Linux software interface
    and not a description of real hardware.
  - Vendor-specific compatible strings (e.g. "nvidia,tegra-spidev")
    have been rejected by DT maintainers for the same reason.

The I2C subsystem solved an analogous problem by exposing
new_device/delete_device sysfs attributes on each adapter. Add
the same interface to SPI host controllers, so that userspace
(e.g. a systemd unit at boot) can instantiate SPI devices at
runtime without needing anything in device-tree.

The new_device file accepts:

  <modalias> <chip_select> [<max_speed_hz> [<mode>]]

where chip_select is required, while max_speed_hz and mode are
optional and default to 0 if omitted. max_speed_hz == 0 is
clamped to the controller's maximum by spi_setup(); mode == 0
selects SPI mode 0 (CPOL=0, CPHA=0).

The modalias is used both as the device identifier and as a
driver_override, so that the device binds to the named driver
directly. This is necessary because some drivers like spidev
deliberately exclude generic names from their id_table.

Devices created this way are limited compared to those declared
via DT or board files:

  - No IRQ is assigned (the device gets IRQ 0 / no interrupt).
  - No platform_data or device properties are attached.
  - No OF node is associated with the device.

These limitations are acceptable for spidev, which only needs a
registered spi_device to expose a character device to userspace.

Only devices created via new_device can be removed through
delete_device; DT and platform devices are unaffected.

The sysfs attributes are gated behind CONFIG_SPI_DYNAMIC since
this feature adds a new way of dynamically instantiating and
removing SPI devices, and the add_lock locking in
spi_unregister_controller() is already conditional on
CONFIG_SPI_DYNAMIC.

A 'dead' flag on spi_controller prevents new device registration
and list insertion after spi_unregister_controller() begins
tearing down the controller. This avoids:

  1. An ABBA deadlock between add_lock and the kernfs active
     reference held by sysfs store callbacks. add_lock is
     released before device_del() so that in-flight sysfs
     operations can drain.

  2. Orphaned devices that could slip through after the
     userspace_clients cleanup but before device_del().

  3. Use-after-free if __unregister frees a device that
     new_device_store() still references. An extra get_device()
     before spi_add_device() holds the device alive.

Link: https://lore.kernel.org/linux-tegra/909f0c92-d110-4253-903e-5c81e21e12c9@nvidia.com/

Signed-off-by: Vishwaroop A <va@nvidia.com>
---
 drivers/spi/spi.c       | 217 ++++++++++++++++++++++++++++++++++++++--
 include/linux/spi/spi.h |  13 +++
 2 files changed, 224 insertions(+), 6 deletions(-)

diff --git a/drivers/spi/spi.c b/drivers/spi/spi.c
index 7001f5dce8bd..9042c19746b8 100644
--- a/drivers/spi/spi.c
+++ b/drivers/spi/spi.c
@@ -296,11 +296,188 @@ static const struct attribute_group spi_controller_statistics_group = {
 	.attrs  = spi_controller_statistics_attrs,
 };
 
+#if IS_ENABLED(CONFIG_SPI_DYNAMIC)
+
+/*
+ * new_device_store - instantiate a new SPI device from userspace
+ *
+ * Takes parameters: <modalias> <chip_select> [<max_speed_hz> [<mode>]]
+ *
+ * Examples:
+ *   echo spidev 0 > new_device
+ *   echo spidev 0 10000000 > new_device
+ *   echo spidev 0 10000000 3 > new_device
+ */
+static ssize_t
+new_device_store(struct device *dev, struct device_attribute *attr,
+		 const char *buf, size_t count)
+{
+	struct spi_controller *ctlr = container_of(dev, struct spi_controller,
+						   dev);
+	struct spi_device *spi;
+	char modalias[SPI_NAME_SIZE];
+	u16 chip_select;
+	u32 max_speed_hz = 0;
+	u32 mode = 0;
+	char *blank;
+	int n, res, status;
+
+	blank = strchr(buf, ' ');
+	if (!blank) {
+		dev_err(dev, "%s: Missing parameters\n", "new_device");
+		return -EINVAL;
+	}
+
+	if (blank - buf > SPI_NAME_SIZE - 1) {
+		dev_err(dev, "%s: Invalid device name\n", "new_device");
+		return -EINVAL;
+	}
+
+	memset(modalias, 0, sizeof(modalias));
+	memcpy(modalias, buf, blank - buf);
+
+	/*
+	 * sscanf fills only the fields it matches; unmatched optional
+	 * fields (max_speed_hz, mode) stay zero from initialisation above.
+	 * max_speed_hz == 0 is clamped to the controller max by spi_setup().
+	 * mode == 0 selects SPI mode 0 (CPOL=0, CPHA=0).
+	 */
+	res = sscanf(++blank, "%hu %u %u%n",
+		     &chip_select, &max_speed_hz, &mode, &n);
+	if (res < 1) {
+		dev_err(dev, "%s: Can't parse chip select\n", "new_device");
+		return -EINVAL;
+	}
+
+	if (chip_select >= ctlr->num_chipselect) {
+		dev_err(dev, "%s: Chip select %u >= max %u\n", "new_device",
+			chip_select, ctlr->num_chipselect);
+		return -EINVAL;
+	}
+
+	spi = spi_alloc_device(ctlr);
+	if (!spi)
+		return -ENOMEM;
+
+	spi_set_chipselect(spi, 0, chip_select);
+	spi->max_speed_hz = max_speed_hz;
+	spi->mode = mode;
+	spi->cs_index_mask = BIT(0);
+	strscpy(spi->modalias, modalias, sizeof(spi->modalias));
+
+	/*
+	 * Set driver_override so that the device binds to the driver
+	 * named by modalias regardless of whether that driver's
+	 * id_table contains a matching entry.  This is needed because
+	 * some drivers (e.g. spidev) deliberately omit generic names
+	 * from their id_table.
+	 */
+	status = device_set_driver_override(&spi->dev, modalias);
+	if (status) {
+		spi_dev_put(spi);
+		return status;
+	}
+
+	/* Extra ref so concurrent __unregister cannot free the device */
+	get_device(&spi->dev);
+
+	status = spi_add_device(spi);
+	if (status) {
+		put_device(&spi->dev);
+		spi_dev_put(spi);
+		return status;
+	}
+
+	mutex_lock(&ctlr->userspace_clients_lock);
+	if (!ctlr->dead) {
+		list_add_tail(&spi->userspace_node, &ctlr->userspace_clients);
+		mutex_unlock(&ctlr->userspace_clients_lock);
+		put_device(&spi->dev);
+		dev_info(dev, "%s: Instantiated device %s at CS%u\n",
+			 "new_device", modalias, chip_select);
+		return count;
+	}
+	mutex_unlock(&ctlr->userspace_clients_lock);
+
+	/*
+	 * Controller is dying; __unregister will clean up the device.
+	 * Drop our extra ref and bail.
+	 */
+	put_device(&spi->dev);
+	return -ENODEV;
+}
+static DEVICE_ATTR_WO(new_device);
+
+static ssize_t
+delete_device_store(struct device *dev, struct device_attribute *attr,
+		    const char *buf, size_t count)
+{
+	struct spi_controller *ctlr = container_of(dev, struct spi_controller,
+						   dev);
+	struct spi_device *spi, *next;
+	unsigned short cs;
+	char end;
+	int res;
+
+	res = sscanf(buf, "%hu%c", &cs, &end);
+	if (res < 1) {
+		dev_err(dev, "%s: Can't parse chip select\n", "delete_device");
+		return -EINVAL;
+	}
+	if (res > 1 && end != '\n') {
+		dev_err(dev, "%s: Extra parameters\n", "delete_device");
+		return -EINVAL;
+	}
+
+	res = -ENOENT;
+	mutex_lock(&ctlr->userspace_clients_lock);
+	list_for_each_entry_safe(spi, next, &ctlr->userspace_clients,
+				 userspace_node) {
+		if (spi_get_chipselect(spi, 0) == cs) {
+			dev_info(dev, "%s: Deleting device %s at CS%u\n",
+				 "delete_device", spi->modalias, cs);
+
+			list_del(&spi->userspace_node);
+			spi_unregister_device(spi);
+			res = count;
+			break;
+		}
+	}
+	mutex_unlock(&ctlr->userspace_clients_lock);
+
+	if (res < 0)
+		dev_err(dev, "%s: Can't find device in list\n",
+			"delete_device");
+	return res;
+}
+static DEVICE_ATTR_IGNORE_LOCKDEP(delete_device, 0200, NULL,
+				   delete_device_store);
+
+static struct attribute *spi_controller_userspace_attrs[] = {
+	&dev_attr_new_device.attr,
+	&dev_attr_delete_device.attr,
+	NULL,
+};
+
+static const struct attribute_group spi_controller_userspace_group = {
+	.attrs = spi_controller_userspace_attrs,
+};
+
 static const struct attribute_group *spi_controller_groups[] = {
 	&spi_controller_statistics_group,
+	&spi_controller_userspace_group,
 	NULL,
 };
 
+#else /* !CONFIG_SPI_DYNAMIC */
+
+static const struct attribute_group *spi_controller_groups[] = {
+	&spi_controller_statistics_group,
+	NULL,
+};
+
+#endif /* CONFIG_SPI_DYNAMIC */
+
 static void spi_statistics_add_transfer_stats(struct spi_statistics __percpu *pcpu_stats,
 					      struct spi_transfer *xfer,
 					      struct spi_message *msg)
@@ -724,10 +901,10 @@ static int __spi_add_device(struct spi_device *spi, struct spi_device *parent)
 		return status;
 
 	/* Controller may unregister concurrently */
-	if (IS_ENABLED(CONFIG_SPI_DYNAMIC) &&
-	    !device_is_registered(&ctlr->dev)) {
+#if IS_ENABLED(CONFIG_SPI_DYNAMIC)
+	if (ctlr->dead)
 		return -ENODEV;
-	}
+#endif
 
 	if (ctlr->cs_gpiods) {
 		u8 cs;
@@ -3256,6 +3433,10 @@ struct spi_controller *__spi_alloc_controller(struct device *dev,
 	mutex_init(&ctlr->bus_lock_mutex);
 	mutex_init(&ctlr->io_mutex);
 	mutex_init(&ctlr->add_lock);
+#if IS_ENABLED(CONFIG_SPI_DYNAMIC)
+	mutex_init(&ctlr->userspace_clients_lock);
+	INIT_LIST_HEAD(&ctlr->userspace_clients);
+#endif
 	ctlr->bus_num = -1;
 	ctlr->num_chipselect = 1;
 	ctlr->num_data_lanes = 1;
@@ -3633,8 +3814,35 @@ void spi_unregister_controller(struct spi_controller *ctlr)
 	if (IS_ENABLED(CONFIG_SPI_DYNAMIC))
 		mutex_lock(&ctlr->add_lock);
 
+	/*
+	 * Mark dead and drain userspace_clients before __unregister,
+	 * since spi_unregister_device() doesn't do list_del() itself.
+	 */
+#if IS_ENABLED(CONFIG_SPI_DYNAMIC)
+	mutex_lock(&ctlr->userspace_clients_lock);
+	ctlr->dead = true;
+	while (!list_empty(&ctlr->userspace_clients)) {
+		struct spi_device *spi;
+
+		spi = list_first_entry(&ctlr->userspace_clients,
+				       struct spi_device,
+				       userspace_node);
+		list_del(&spi->userspace_node);
+		spi_unregister_device(spi);
+	}
+	mutex_unlock(&ctlr->userspace_clients_lock);
+#endif
+
 	device_for_each_child(&ctlr->dev, NULL, __unregister);
 
+	/*
+	 * Release add_lock before device_del(): holding it would
+	 * deadlock against kernfs_drain waiting for in-flight sysfs
+	 * stores.  ctlr->dead prevents new device registration.
+	 */
+	if (IS_ENABLED(CONFIG_SPI_DYNAMIC))
+		mutex_unlock(&ctlr->add_lock);
+
 	/* First make sure that this controller was ever added */
 	mutex_lock(&board_lock);
 	found = idr_find(&spi_controller_idr, id);
@@ -3655,9 +3863,6 @@ void spi_unregister_controller(struct spi_controller *ctlr)
 		idr_remove(&spi_controller_idr, id);
 	mutex_unlock(&board_lock);
 
-	if (IS_ENABLED(CONFIG_SPI_DYNAMIC))
-		mutex_unlock(&ctlr->add_lock);
-
 	/*
 	 * Release the last reference on the controller if its driver
 	 * has not yet been converted to devm_spi_alloc_host/target().
diff --git a/include/linux/spi/spi.h b/include/linux/spi/spi.h
index 7587b1c5d7ec..7a86749d2701 100644
--- a/include/linux/spi/spi.h
+++ b/include/linux/spi/spi.h
@@ -250,6 +250,10 @@ struct spi_device {
 	u8			rx_lane_map[SPI_DEVICE_DATA_LANE_CNT_MAX];
 	u8			num_rx_lanes;
 
+#if IS_ENABLED(CONFIG_SPI_DYNAMIC)
+	struct list_head	userspace_node;
+#endif
+
 	/*
 	 * Likely need more hooks for more protocol options affecting how
 	 * the controller talks to each chip, like:
@@ -554,6 +558,9 @@ extern struct spi_device *devm_spi_new_ancillary_device(struct spi_device *spi,
  * @defer_optimize_message: set to true if controller cannot pre-optimize messages
  *	and needs to defer the optimization step until the message is actually
  *	being transferred
+ * @userspace_clients: list of SPI devices instantiated from userspace via
+ *	the sysfs new_device interface
+ * @userspace_clients_lock: mutex protecting @userspace_clients
  *
  * Each SPI controller can communicate with one or more @spi_device
  * children.  These make a small bus, sharing MOSI, MISO and SCK signals
@@ -809,6 +816,12 @@ struct spi_controller {
 	bool			queue_empty;
 	bool			must_async;
 	bool			defer_optimize_message;
+
+#if IS_ENABLED(CONFIG_SPI_DYNAMIC)
+	struct list_head	userspace_clients;
+	struct mutex		userspace_clients_lock;
+	bool			dead;
+#endif
 };
 
 static inline void *spi_controller_get_devdata(struct spi_controller *ctlr)
-- 
2.17.1


^ permalink raw reply related

* [PATCH v5 0/2] spi: add new_device/delete_device sysfs interface
From: Vishwaroop A @ 2026-05-17 20:16 UTC (permalink / raw)
  To: broonie, linux-spi
  Cc: smangipudi, jonathanh, thierry.reding, corbet, linux-doc, va

Add I2C-style new_device/delete_device sysfs attributes to SPI host
controllers, allowing userspace to instantiate and remove SPI devices
at runtime without device-tree changes.

Changes since v4:
  - Removed spi_unregister_device() call from new_device_store()'s
    ctlr->dead teardown path.  That call raced with
    device_for_each_child(__unregister) in spi_unregister_controller(),
    causing a double-free.  The extra get_device() ref keeps the struct
    alive; __unregister handles the actual cleanup.

Changes since v3:
  - Replaced holding add_lock across __spi_add_device() + list
    insertion (which caused an ABBA deadlock between add_lock and the
    kernfs active reference during concurrent unbind) with:
    * A 'dead' flag on spi_controller, set in
      spi_unregister_controller() under both add_lock and
      userspace_clients_lock.
    * __spi_add_device() checks ctlr->dead under add_lock to reject
      new devices after teardown begins.
    * new_device_store() checks ctlr->dead under userspace_clients_lock
      before list insertion, falling back to cleanup + ENODEV.
    * add_lock is released before device_del() so in-flight sysfs
      stores can drain without deadlocking.
    * get_device() taken before spi_add_device() prevents
      use-after-free if __unregister runs concurrently.
  - Used #if IS_ENABLED() preprocessor guard (not runtime IS_ENABLED())
    for the ctlr->dead check in __spi_add_device(), since the dead
    field is conditionally compiled.

Changes since v2:
  - Gated sysfs attributes and locking on CONFIG_SPI_DYNAMIC.

Changes since v1:
  - Added locking to prevent races between new_device_store() and
    concurrent spi_unregister_controller().

Link: https://lore.kernel.org/linux-tegra/909f0c92-d110-4253-903e-5c81e21e12c9@nvidia.com/

Vishwaroop A (2):
  spi: add new_device/delete_device sysfs interface
  docs: spi: add documentation for userspace device instantiation

 .../ABI/testing/sysfs-class-spi-master        |  34 +++
 Documentation/spi/index.rst                   |   1 +
 Documentation/spi/instantiating-devices.rst   |  88 +++++++
 drivers/spi/spi.c                             | 217 +++++++++++++++++-
 include/linux/spi/spi.h                       |  13 ++
 5 files changed, 347 insertions(+), 6 deletions(-)
 create mode 100644 Documentation/ABI/testing/sysfs-class-spi-master
 create mode 100644 Documentation/spi/instantiating-devices.rst

-- 
2.17.1


^ permalink raw reply

* Re: [PATCH v3] killswitch: add per-function short-circuit mitigation primitive
From: Brandon Taylor @ 2026-05-17 19:19 UTC (permalink / raw)
  To: Sasha Levin, linux-kernel
  Cc: linux-doc, linux-kselftest, bpf, live-patching,
	Greg Kroah-Hartman, Andrew Morton, Jonathan Corbet,
	Mathieu Desnoyers, Joshua Peisach, Florian Weimer, Breno Leitao,
	Anthony Iliopoulos, Michal Hocko, Jiri Olsa
In-Reply-To: <20260517134858.146569-1-sashal@kernel.org>

Have we learned NOTHING from just over 9 and a half years ago?!

I do not pretend to be a prophet of Linus, but I cannot for the life of 
me help but get flashbacks from kernel version 4.8 when Linus himself 
did not explain, but EXPLODED, in saying "there is NO F*CKING EXCUSE to 
knowingly kill the kernel."

So for me to hear about THIS from a YouTube video, the fact that we are 
still--STILL!--coming up with new ways to do something which we ought to 
KNOW to be ABSOLUTELY UNACCEPTABLE and DOWNRIGHT INTOLERABLE, BOILS MY 
BLOOD TO NO END.

You ought to consider yourself lucky that it's ME writing this and not 
Linus, because he'd be saying the exact same thing, and making it God 
knows how many times worse. He would break his foot off in somebody's 
BEHIND over this "killswitch" idiocy, and NEVER MIND that it was 
supposedly "designed" to prevent exploits like Fragnesia, Copy Fail, and 
Dirty Frag from creating havoc in Linux distributions, ESPECIALLY his 
go-to in Fedora!

Forgive me (especially you, Master Linus) for blowing my stack over 
this, but we all ought to take a lesson from the past:

Killing the Linux kernel is NOT an acceptable method to mitigate exploits.

I don't care HOW long it takes, but we HAVE TO PATCH THOSE 
VULNERABILITIES, and we HAVE to do it the RIGHT WAY, NOT just introduce 
some kernel-killing "failsafe" just because somebody doesn't know how to 
plug those holes.

I don't care--and neither will Linus--about the so-called "simplest 
mitigation," and neither should you. We should all care that we get the 
code RIGHT.

Brandon

On 5/17/2026 8:48 AM, Sasha Levin wrote:
> When a kernel (security) issue goes public, fleets stay exposed until a patched
> kernel is built, distributed, and rebooted into.
>
> For many such issues the simplest mitigation is to stop calling the buggy
> function. Killswitch provides that. An admin writes:
>
>      echo "engage af_alg_sendmsg -1" \
>          > /sys/kernel/security/killswitch/control
>
> After this, af_alg_sendmsg() returns -EPERM on every call without
> running its body. The mitigation takes effect immediately, and is dropped on
> the next reboot -- by which point a patched kernel is hopefully in place.
>
> A lot of recent kernel issues sit in code paths most installs only have enabled
> to support a relative minority of users: AF_ALG, ksmbd, nf_tables, vsock, ax25,
> and friends.
>
> For most users, the cost of "this socket family stops working for the day" is
> much smaller than the cost of running a known vulnerable kernel until the fix
> lands.
>
> Why not an existing facility:
>
> * livepatch needs a built, signed, per-kernel-version module per CVE.
>    Under Secure Boot the operator can't sign their own, so they wait
>    for the vendor, and only a minority of vendors actually ship
>    livepatches. Killswitch covers the days before that module shows
>    up.
>
> * fail_function (CONFIG_FUNCTION_ERROR_INJECTION) is disabled in
>    most production kernels. Even where enabled, it only works on
>    functions pre-annotated with ALLOW_ERROR_INJECTION() in source -
>    no help for a freshly-disclosed CVE. The debugfs UI is blocked by
>    lockdown=integrity and the override is probabilistic.
>
> * BPF override (bpf_override_return) honors the same
>    ALLOW_ERROR_INJECTION() whitelist, and BPF itself is off in many
>    production kernels. Even where on, the operator interface is
>    "load a verified BPF program," not a one-line write.
>
> * Module blacklist only helps when the bug is in a loadable module.
>
> Killswitch fills the gap: write a symbol to securityfs, function
> returns the chosen value until disengage or reboot.
>
> Assisted-by: Claude:claude-opus-4-7
> Signed-off-by: Sasha Levin <sashal@kernel.org>
> ---
>
> Changes since v2:
> - Fix LLVM=1 build: gate __noipa__ on __has_attribute() (Breno)
> - Admin guide: do-not-engage list, pre-soak workflow, relation to
>    livepatch/fail_function/BPF (Michal, Mathieu, Joshua)
> - Add CVE-2026-43284 (esp_input) worked example + netns selftest
> - Drop unused [reason] token from Kconfig help and cmdline comment
> - Commit message: spell out why livepatch / fail_function / BPF
>    override / module-blacklist don't cover this window.
>
>   Documentation/admin-guide/index.rst           |   1 +
>   Documentation/admin-guide/killswitch.rst      | 229 +++++
>   Documentation/admin-guide/tainted-kernels.rst |   8 +
>   MAINTAINERS                                   |  11 +
>   include/linux/killswitch.h                    |  19 +
>   include/linux/panic.h                         |   3 +-
>   include/linux/security.h                      |   1 +
>   init/Kconfig                                  |   2 +
>   kernel/Kconfig.killswitch                     |  31 +
>   kernel/Makefile                               |   1 +
>   kernel/killswitch.c                           | 863 ++++++++++++++++++
>   kernel/panic.c                                |   1 +
>   lib/Kconfig.debug                             |  13 +
>   lib/Makefile                                  |   1 +
>   lib/test_killswitch.c                         |  85 ++
>   security/security.c                           |   1 +
>   tools/testing/selftests/Makefile              |   1 +
>   tools/testing/selftests/killswitch/.gitignore |   1 +
>   tools/testing/selftests/killswitch/Makefile   |   8 +
>   .../selftests/killswitch/cve_31431_test.c     | 162 ++++
>   .../selftests/killswitch/cve_43284_test.c     |  88 ++
>   .../selftests/killswitch/killswitch_test.sh   | 254 ++++++
>   22 files changed, 1783 insertions(+), 1 deletion(-)
>   create mode 100644 Documentation/admin-guide/killswitch.rst
>   create mode 100644 include/linux/killswitch.h
>   create mode 100644 kernel/Kconfig.killswitch
>   create mode 100644 kernel/killswitch.c
>   create mode 100644 lib/test_killswitch.c
>   create mode 100644 tools/testing/selftests/killswitch/.gitignore
>   create mode 100644 tools/testing/selftests/killswitch/Makefile
>   create mode 100644 tools/testing/selftests/killswitch/cve_31431_test.c
>   create mode 100644 tools/testing/selftests/killswitch/cve_43284_test.c
>   create mode 100755 tools/testing/selftests/killswitch/killswitch_test.sh
>
> diff --git a/Documentation/admin-guide/index.rst b/Documentation/admin-guide/index.rst
> index cd28dfe91b060..ca37dd70f108d 100644
> --- a/Documentation/admin-guide/index.rst
> +++ b/Documentation/admin-guide/index.rst
> @@ -70,6 +70,7 @@ problems and bugs in particular.
>      bug-hunting
>      bug-bisect
>      tainted-kernels
> +   killswitch
>      ramoops
>      dynamic-debug-howto
>      init
> diff --git a/Documentation/admin-guide/killswitch.rst b/Documentation/admin-guide/killswitch.rst
> new file mode 100644
> index 0000000000000..a524cc9ee23ca
> --- /dev/null
> +++ b/Documentation/admin-guide/killswitch.rst
> @@ -0,0 +1,229 @@
> +.. SPDX-License-Identifier: GPL-2.0
> +..
> +.. Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
> +
> +============
> +Killswitch
> +============
> +
> +Killswitch lets a privileged operator make a chosen kernel function
> +return a fixed value without executing its body, as a temporary
> +mitigation for a security bug while a real fix is being prepared.
> +
> +The function returns the operator-supplied value and nothing else
> +runs in its place. There is no allowlist, no return-type check; if
> +the kprobe layer accepts the symbol, killswitch engages it. Once
> +engaged, the change is in effect on every CPU until ``disengage`` is
> +written or the system reboots.
> +
> +Configuration
> +=============
> +
> +``CONFIG_KILLSWITCH``
> +  Enables the feature. Depends on ``SECURITYFS``, ``KPROBES`` (with
> +  ftrace support), and ``FUNCTION_ERROR_INJECTION``.
> +
> +The interface
> +=============
> +
> +::
> +
> +    /sys/kernel/security/killswitch/
> +        engaged                 RO  currently-engaged functions
> +        control                 WO  command sink
> +        taint                   RO  0 or 1
> +        fn/<name>/              per-function directory, created on engage
> +            retval              RW  return value
> +            hits                RO  per-cpu summed call count
> +
> +Three commands are accepted by ``control``::
> +
> +    engage <symbol> <retval>
> +    disengage <symbol>
> +    disengage_all
> +
> +Each engage and disengage emits a single ``KERN_WARNING`` line to
> +dmesg with the symbol, retval, hit count (on disengage), and the
> +operator's identity (uid/auid/sessionid/comm, or ``source=cmdline``).
> +
> +Engagement is rejected when:
> +
> +* the symbol is unknown, in a non-traceable section, on the kprobe
> +  blacklist, or otherwise refused by ``register_kprobe`` (the error
> +  from the kprobe layer is logged and returned to userspace);
> +* the symbol is already engaged (``-EBUSY``);
> +* the operator does not hold ``CAP_SYS_ADMIN``.
> +
> +Whatever value the operator writes is what the function returns.
> +Writing the wrong type or wrong value lands in the caller as-is.
> +
> +Boot parameter
> +==============
> +
> +``killswitch=fn1=<val>,fn2=<val>,...``
> +
> +Parsed early; engagements are applied at the end of kernel init
> +once the kprobe subsystem is up. Parse failures emit a warning and
> +skip the offending entry; they never panic.
> +
> +Useful for fleet rollout: when an issue drops, ship the mitigation
> +in the bootloader / PXE config and roll the fleet through reboots
> +while the real fix is being prepared.
> +
> +Tainting
> +========
> +
> +The first successful engagement (runtime or boot-time) sets
> +``TAINT_KILLSWITCH`` (bit 20, char ``H``). The taint persists across
> +``disengage`` until reboot, so an oops on a killswitch-modified
> +kernel is identifiable from the banner: ``Tainted: ... H`` tells a
> +maintainer to consult ``engaged`` before further triage.
> +
> +Module unload
> +=============
> +
> +If a module containing an engaged target is unloaded, killswitch
> +auto-disengages the entry and emits a ``KERN_WARNING`` so the loss
> +of mitigation is visible. Reloading the module does not silently
> +re-arm the killswitch; the operator re-engages explicitly.
> +
> +Choosing the right target
> +=========================
> +
> +A function that *looks* skippable may be relied on by callers for a
> +side effect (a lock the caller releases, a refcount the caller
> +drops, a scatterlist the caller consumes). The rule of thumb:
> +
> +  Pick the **highest-level** entry point that contains the bug.
> +
> +That gives callers no chance to dereference half-initialised state
> +from a function whose body was skipped. Two illustrative examples
> +from ``crypto/af_alg.c``:
> +
> +Anti-pattern: ``af_alg_count_tsgl``
> +-----------------------------------
> +
> +``af_alg_count_tsgl()`` returns ``unsigned int`` (the number of TX
> +SG entries). Engaging it with retval ``0`` causes the caller in
> +``algif_aead.c`` to allocate a 1-entry scatterlist (its
> +``if (!entries) entries = 1`` guard) and then walk the *real* TX
> +SGL into that undersized destination via ``af_alg_pull_tsgl``,
> +producing out-of-bounds writes. **Killswitching here introduces a
> +worse bug than the one being mitigated.**
> +
> +Anti-pattern: ``af_alg_pull_tsgl``
> +----------------------------------
> +
> +``af_alg_pull_tsgl()`` returns ``void``, so any retval is accepted.
> +But its caller depends on the per-request SGL being filled in.
> +Skipping the body leaves the per-request SGL with NULL pages; the
> +next-stage ``memcpy_sglist`` dereferences them and the kernel
> +oopses.
> +
> +Correct pattern: ``af_alg_sendmsg``
> +-----------------------------------
> +
> +``af_alg_sendmsg()`` is the highest-level entry into the AF_ALG
> +send path. Engaging it with retval ``-EPERM`` causes every send
> +attempt to return -EPERM to userspace; no caller ever sees
> +half-initialised state, and any AF_ALG-reachable bug downstream of
> +``sendmsg`` is unreachable until the killswitch is disengaged.
> +
> +The canonical pattern: pick a syscall-handler-shaped function whose
> +return value already encodes "this operation didn't happen", and
> +let userspace handle the error as it would any other failed
> +syscall.
> +
> +Correct pattern: ``esp_input`` (CVE-2026-43284)
> +-----------------------------------------------
> +
> +The IPsec ESP receive-path bug fixed by ``xfrm: esp: avoid in-place
> +decrypt on shared skb frags`` is reachable through ``esp_input()``
> +in ``net/ipv4/esp4.c`` (and ``esp6_input()`` for IPv6). Engage these
> +with retval ``-EINVAL``:
> +
> +::
> +
> +    echo "engage esp_input -22"  > /sys/kernel/security/killswitch/control
> +    echo "engage esp6_input -22" > /sys/kernel/security/killswitch/control
> +
> +Inbound ESP packets are then dropped before decapsulation, neutering
> +any bug downstream of the ESP receive path. IPsec tunnels stop
> +working; other networking is unaffected.
> +
> +Do not engage
> +=============
> +
> +Do not killswitch:
> +
> +* process or memory primitives the rest of the kernel needs to
> +  function: ``fork``, ``do_exit``, ``__alloc_pages``, ``kmalloc``,
> +  ``schedule``, anything in ``mm/`` reached by every allocation.
> +* hot paths in the scheduler, timekeeping, RCU, or interrupt entry.
> +* functions invoked from the killswitch path itself (``securityfs``,
> +  ``lockdown``, ``audit``, ``kprobe`` registration) -- the system
> +  may livelock or refuse to disengage.
> +* functions whose return value is read structurally (size, count,
> +  pointer-to-allocated-thing) rather than as success/failure.
> +  See the AF_ALG anti-patterns above for what goes wrong.
> +
> +When in doubt, measure first.
> +
> +Pre-soak before engaging
> +========================
> +
> +If the target's call rate is unknown, attach a counter for a few
> +seconds first. With perf::
> +
> +    perf probe --add 'esp_input'
> +    perf stat -a -e probe:esp_input -- sleep 5
> +
> +Or with bpftrace::
> +
> +    bpftrace -e 'kprobe:esp_input { @hits = count(); } interval:s:5 { exit(); }'
> +
> +A target with ten thousand hits per second is not a candidate -- the
> +kernel will not survive five seconds with that path returning a
> +fixed error.
> +
> +Relation to other facilities
> +============================
> +
> +* ``CONFIG_FUNCTION_ERROR_INJECTION`` provides the same architecture
> +  trampoline (``override_function_with_return``), which killswitch
> +  reuses. fail_function is debug-oriented: targets must be
> +  pre-annotated with ``ALLOW_ERROR_INJECTION()`` in source, the
> +  override is probabilistic, and the interface is on debugfs (blocked
> +  under ``lockdown=integrity``). Killswitch is the production cousin:
> +  no whitelist, deterministic, securityfs-visible under integrity
> +  lockdown, with audit and taint.
> +* livepatch can do everything killswitch can and more, at the cost
> +  of building, signing, and shipping a kernel module per mitigation.
> +  Killswitch is for the window before that module exists.
> +* BPF override (``bpf_override_return``) needs a BPF program and
> +  ``CONFIG_BPF_KPROBE_OVERRIDE``; appropriate when the policy is
> +  conditional, overkill for "always return -EPERM".
> +
> +Safety notes
> +============
> +
> +* In-flight calls during ``write()`` to ``control`` may run either
> +  the original body or the override. The override is ``return X``,
> +  which has no preconditions to violate.
> +* SMP visibility comes from ``text_poke_bp()``. ``write()`` to
> +  ``control`` returns only after every CPU sees the new path.
> +* The ftrace ops unregister waits for in-flight pre-handlers, so
> +  freeing the engagement attribute on disengage is safe.
> +* Inline functions, freed ``__init`` symbols, and anything compiled
> +  away cannot be killswitched. ``register_kprobe`` rejects them
> +  with whatever error the kprobe layer chooses.
> +
> +Diagnostics
> +===========
> +
> +Per-call hits are aggregated in a per-cpu counter readable at
> +``/sys/kernel/security/killswitch/fn/<name>/hits``. Per-hit logging
> +is not provided to avoid log storms on hot paths.
> +
> +A ``KILLSWITCH`` entry appears in the kernel taint vector once any
> +engagement succeeds (also visible as ``H`` in the oops banner).
> diff --git a/Documentation/admin-guide/tainted-kernels.rst b/Documentation/admin-guide/tainted-kernels.rst
> index 9ead927a37c0f..71a6e3364eddc 100644
> --- a/Documentation/admin-guide/tainted-kernels.rst
> +++ b/Documentation/admin-guide/tainted-kernels.rst
> @@ -102,6 +102,7 @@ Bit  Log  Number  Reason that got the kernel tainted
>    17  _/T  131072  kernel was built with the struct randomization plugin
>    18  _/N  262144  an in-kernel test has been run
>    19  _/J  524288  userspace used a mutating debug operation in fwctl
> + 20  _/H 1048576  killswitch override engaged (function short-circuited)
>   ===  ===  ======  ========================================================
>   
>   Note: The character ``_`` is representing a blank in this table to make reading
> @@ -189,3 +190,10 @@ More detailed explanation for tainting
>    19) ``J`` if userspace opened /dev/fwctl/* and performed a FWTCL_RPC_DEBUG_WRITE
>        to use the devices debugging features. Device debugging features could
>        cause the device to malfunction in undefined ways.
> +
> + 20) ``H`` if the killswitch primitive (see
> +     Documentation/admin-guide/killswitch.rst) has been engaged on at least
> +     one function. The kernel is no longer running its source: at least one
> +     function has been short-circuited to return a fixed value. The taint
> +     persists across ``disengage`` until the next reboot — once the running
> +     image has been modified, oops triage must reflect that.
> diff --git a/MAINTAINERS b/MAINTAINERS
> index b2040011a3865..b4005b61d444f 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -14350,6 +14350,17 @@ F:	lib/Kconfig.kmsan
>   F:	mm/kmsan/
>   F:	scripts/Makefile.kmsan
>   
> +KILLSWITCH (function short-circuit mitigation)
> +M:	Sasha Levin <sashal@kernel.org>
> +L:	linux-kernel@vger.kernel.org
> +S:	Maintained
> +F:	Documentation/admin-guide/killswitch.rst
> +F:	include/linux/killswitch.h
> +F:	kernel/Kconfig.killswitch
> +F:	kernel/killswitch.c
> +F:	lib/test_killswitch.c
> +F:	tools/testing/selftests/killswitch/
> +
>   KPROBES
>   M:	Naveen N Rao <naveen@kernel.org>
>   M:	"David S. Miller" <davem@davemloft.net>
> diff --git a/include/linux/killswitch.h b/include/linux/killswitch.h
> new file mode 100644
> index 0000000000000..3fad49e180ddf
> --- /dev/null
> +++ b/include/linux/killswitch.h
> @@ -0,0 +1,19 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
> + */
> +#ifndef _LINUX_KILLSWITCH_H
> +#define _LINUX_KILLSWITCH_H
> +
> +#ifdef CONFIG_KILLSWITCH
> +int killswitch_engage(const char *symbol, long retval);
> +int killswitch_disengage(const char *symbol);
> +bool killswitch_is_engaged(const char *symbol);
> +#else
> +static inline int killswitch_engage(const char *symbol, long retval)
> +{ return -EOPNOTSUPP; }
> +static inline int killswitch_disengage(const char *symbol) { return -EOPNOTSUPP; }
> +static inline bool killswitch_is_engaged(const char *symbol) { return false; }
> +#endif
> +
> +#endif /* _LINUX_KILLSWITCH_H */
> diff --git a/include/linux/panic.h b/include/linux/panic.h
> index f1dd417e54b29..6699261a61f13 100644
> --- a/include/linux/panic.h
> +++ b/include/linux/panic.h
> @@ -88,7 +88,8 @@ static inline void set_arch_panic_timeout(int timeout, int arch_default_timeout)
>   #define TAINT_RANDSTRUCT		17
>   #define TAINT_TEST			18
>   #define TAINT_FWCTL			19
> -#define TAINT_FLAGS_COUNT		20
> +#define TAINT_KILLSWITCH		20
> +#define TAINT_FLAGS_COUNT		21
>   #define TAINT_FLAGS_MAX			((1UL << TAINT_FLAGS_COUNT) - 1)
>   
>   struct taint_flag {
> diff --git a/include/linux/security.h b/include/linux/security.h
> index 41d7367cf4036..038027c33ba1a 100644
> --- a/include/linux/security.h
> +++ b/include/linux/security.h
> @@ -146,6 +146,7 @@ enum lockdown_reason {
>   	LOCKDOWN_DBG_WRITE_KERNEL,
>   	LOCKDOWN_RTAS_ERROR_INJECTION,
>   	LOCKDOWN_XEN_USER_ACTIONS,
> +	LOCKDOWN_KILLSWITCH,
>   	LOCKDOWN_INTEGRITY_MAX,
>   	LOCKDOWN_KCORE,
>   	LOCKDOWN_KPROBES,
> diff --git a/init/Kconfig b/init/Kconfig
> index 2937c4d308aec..5368dd4b5c65b 100644
> --- a/init/Kconfig
> +++ b/init/Kconfig
> @@ -2278,6 +2278,8 @@ config ASN1
>   
>   source "kernel/Kconfig.locks"
>   
> +source "kernel/Kconfig.killswitch"
> +
>   config ARCH_HAS_NON_OVERLAPPING_ADDRESS_SPACE
>   	bool
>   
> diff --git a/kernel/Kconfig.killswitch b/kernel/Kconfig.killswitch
> new file mode 100644
> index 0000000000000..a33f7ecb2861e
> --- /dev/null
> +++ b/kernel/Kconfig.killswitch
> @@ -0,0 +1,31 @@
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# Killswitch: per-function short-circuit mitigation primitive.
> +#
> +# Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
> +#
> +
> +config KILLSWITCH
> +	bool "Killswitch: short-circuit a kernel function as a CVE mitigation"
> +	depends on SECURITYFS
> +	depends on KPROBES && HAVE_KPROBES_ON_FTRACE
> +	depends on HAVE_FUNCTION_ERROR_INJECTION
> +	select FUNCTION_ERROR_INJECTION
> +	help
> +	  Provide an admin-facing mechanism to make a chosen kernel function
> +	  return a fixed value without executing its body, as a temporary
> +	  mitigation for a security bug before a real fix is available.
> +
> +	  Operators write "engage <symbol> <retval>" to
> +	  /sys/kernel/security/killswitch/control. The function entry is
> +	  redirected via a kprobe whose pre-handler sets the chosen return
> +	  value and short-circuits the call. There is no allowlist,
> +	  denylist, or return-type validation: if the kprobe layer accepts
> +	  the symbol the engagement proceeds, otherwise its error is
> +	  returned to userspace.
> +
> +	  This is *not* livepatch: there is no replacement implementation,
> +	  the function simply returns the chosen value. Engaging a killswitch
> +	  taints the kernel (TAINT_KILLSWITCH, 'H'). Requires CAP_SYS_ADMIN.
> +
> +	  If unsure, say N.
> diff --git a/kernel/Makefile b/kernel/Makefile
> index 6785982013dce..b3e408d9f275e 100644
> --- a/kernel/Makefile
> +++ b/kernel/Makefile
> @@ -100,6 +100,7 @@ obj-$(CONFIG_GCOV_KERNEL) += gcov/
>   obj-$(CONFIG_KCOV) += kcov.o
>   obj-$(CONFIG_KPROBES) += kprobes.o
>   obj-$(CONFIG_FAIL_FUNCTION) += fail_function.o
> +obj-$(CONFIG_KILLSWITCH) += killswitch.o
>   obj-$(CONFIG_KGDB) += debug/
>   obj-$(CONFIG_DETECT_HUNG_TASK) += hung_task.o
>   obj-$(CONFIG_LOCKUP_DETECTOR) += watchdog.o
> diff --git a/kernel/killswitch.c b/kernel/killswitch.c
> new file mode 100644
> index 0000000000000..7f509c62ea748
> --- /dev/null
> +++ b/kernel/killswitch.c
> @@ -0,0 +1,863 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Per-function short-circuit mitigation.
> + *
> + * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
> + *
> + * Engaging a killswitch installs a kprobe at the function's entry
> + * whose pre-handler sets the return register and skips the body via
> + * override_function_with_return().  Operator interface lives at
> + * /sys/kernel/security/killswitch/.
> + */
> +
> +#include <linux/audit.h>
> +#include <linux/capability.h>
> +#include <linux/cred.h>
> +#include <linux/ctype.h>
> +#include <linux/error-injection.h>
> +#include <linux/init.h>
> +#include <linux/killswitch.h>
> +#include <linux/kprobes.h>
> +#include <linux/kref.h>
> +#include <linux/list.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/notifier.h>
> +#include <linux/panic.h>
> +#include <linux/percpu.h>
> +#include <linux/printk.h>
> +#include <linux/sched.h>
> +#include <linux/security.h>
> +#include <linux/seq_file.h>
> +#include <linux/slab.h>
> +#include <linux/string.h>
> +#include <linux/uaccess.h>
> +#include <linux/uidgid.h>
> +
> +struct ks_attr {
> +	struct list_head	list;
> +	struct kprobe		kp;
> +	/* atomic so a writer racing an in-flight call can't tear the long. */
> +	atomic_long_t		retval;
> +	/* false once disengaged; per-fn file ops then return -EIDRM. */
> +	bool			engaged;
> +	unsigned long __percpu	*hits;
> +	struct dentry		*dir;
> +	/* engaged_list holds one ref; each open per-fn fd holds one. */
> +	struct kref		refcnt;
> +};
> +
> +static DEFINE_MUTEX(ks_lock);
> +static LIST_HEAD(ks_engaged_list);
> +static struct dentry *ks_root_dir;
> +static struct dentry *ks_fn_dir;	/* parent for per-fn directories */
> +
> +/* ------------------------------------------------------------------ *
> + * Pre-handler: the actual override                                   *
> + * ------------------------------------------------------------------ */
> +
> +static int ks_kprobe_pre_handler(struct kprobe *kp, struct pt_regs *regs)
> +{
> +	struct ks_attr *attr = container_of(kp, struct ks_attr, kp);
> +
> +	this_cpu_inc(*attr->hits);
> +	regs_set_return_value(regs, (unsigned long)atomic_long_read(&attr->retval));
> +	override_function_with_return(regs);
> +	return 1;
> +}
> +NOKPROBE_SYMBOL(ks_kprobe_pre_handler);
> +
> +/* Defined non-NULL so the kprobe layer keeps the IPMODIFY ops. */
> +static void ks_kprobe_post_handler(struct kprobe *kp, struct pt_regs *regs,
> +				   unsigned long flags)
> +{
> +}
> +
> +/* ------------------------------------------------------------------ *
> + * Attribute lifecycle                                                *
> + * ------------------------------------------------------------------ */
> +
> +static struct ks_attr *ks_attr_lookup(const char *symbol)
> +{
> +	struct ks_attr *attr;
> +
> +	list_for_each_entry(attr, &ks_engaged_list, list)
> +		if (!strcmp(attr->kp.symbol_name, symbol))
> +			return attr;
> +	return NULL;
> +}
> +
> +static unsigned long ks_attr_hits(const struct ks_attr *attr)
> +{
> +	unsigned long total = 0;
> +	int cpu;
> +
> +	for_each_possible_cpu(cpu)
> +		total += *per_cpu_ptr(attr->hits, cpu);
> +	return total;
> +}
> +
> +static void ks_attr_destroy(struct ks_attr *attr)
> +{
> +	if (!attr)
> +		return;
> +	free_percpu(attr->hits);
> +	kfree(attr->kp.symbol_name);
> +	kfree(attr);
> +}
> +
> +static void ks_attr_kref_release(struct kref *kref)
> +{
> +	ks_attr_destroy(container_of(kref, struct ks_attr, refcnt));
> +}
> +
> +static void ks_attr_get(struct ks_attr *attr)
> +{
> +	kref_get(&attr->refcnt);
> +}
> +
> +static void ks_attr_put(struct ks_attr *attr)
> +{
> +	kref_put(&attr->refcnt, ks_attr_kref_release);
> +}
> +
> +static struct ks_attr *ks_attr_alloc(const char *symbol)
> +{
> +	struct ks_attr *attr;
> +
> +	attr = kzalloc(sizeof(*attr), GFP_KERNEL);
> +	if (!attr)
> +		return NULL;
> +
> +	attr->kp.symbol_name = kstrdup(symbol, GFP_KERNEL);
> +	if (!attr->kp.symbol_name)
> +		goto err;
> +
> +	attr->hits = alloc_percpu(unsigned long);
> +	if (!attr->hits)
> +		goto err;
> +
> +	attr->kp.pre_handler = ks_kprobe_pre_handler;
> +	attr->kp.post_handler = ks_kprobe_post_handler;
> +	INIT_LIST_HEAD(&attr->list);
> +	kref_init(&attr->refcnt);
> +	return attr;
> +
> +err:
> +	ks_attr_destroy(attr);
> +	return NULL;
> +}
> +
> +/* ------------------------------------------------------------------ *
> + * Securityfs: per-fn attribute files                                 *
> + * ------------------------------------------------------------------ */
> +
> +/*
> + * Look up by symbol name (the parent dentry's basename) under
> + * ks_lock and confirm attr->dir is the file's parent dentry.  This
> + * binds the fd to the engagement it was opened against and avoids
> + * dereferencing inode->i_private, which a racing disengage may have
> + * freed.  d_parent is stable for the open's lifetime via the file's
> + * dentry reference.
> + */
> +static int ks_attr_open(struct inode *inode, struct file *file)
> +{
> +	struct dentry *parent = file->f_path.dentry->d_parent;
> +	const char *name = parent->d_name.name;
> +	struct ks_attr *attr;
> +
> +	mutex_lock(&ks_lock);
> +	attr = ks_attr_lookup(name);
> +	if (attr && attr->dir == parent)
> +		ks_attr_get(attr);
> +	else
> +		attr = NULL;
> +	mutex_unlock(&ks_lock);
> +	if (!attr)
> +		return -ENOENT;
> +	file->private_data = attr;
> +	return 0;
> +}
> +
> +static int ks_attr_release(struct inode *inode, struct file *file)
> +{
> +	ks_attr_put(file->private_data);
> +	file->private_data = NULL;
> +	return 0;
> +}
> +
> +/* Caller must hold ks_lock. */
> +static int ks_attr_check_live(const struct ks_attr *attr)
> +{
> +	return attr->engaged ? 0 : -EIDRM;
> +}
> +
> +static ssize_t ks_retval_read(struct file *file, char __user *ubuf,
> +			      size_t count, loff_t *ppos)
> +{
> +	struct ks_attr *attr = file->private_data;
> +	char buf[32];
> +	long val;
> +	int ret, len;
> +
> +	mutex_lock(&ks_lock);
> +	ret = ks_attr_check_live(attr);
> +	val = atomic_long_read(&attr->retval);
> +	mutex_unlock(&ks_lock);
> +	if (ret)
> +		return ret;
> +	len = scnprintf(buf, sizeof(buf), "%ld\n", val);
> +	return simple_read_from_buffer(ubuf, count, ppos, buf, len);
> +}
> +
> +static ssize_t ks_retval_write(struct file *file, const char __user *ubuf,
> +			       size_t count, loff_t *ppos)
> +{
> +	struct ks_attr *attr = file->private_data;
> +	char buf[32];
> +	long val;
> +	int ret;
> +
> +	if (count >= sizeof(buf))
> +		return -EINVAL;
> +	if (copy_from_user(buf, ubuf, count))
> +		return -EFAULT;
> +	buf[count] = '\0';
> +	strim(buf);
> +
> +	ret = kstrtol(buf, 0, &val);
> +	if (ret)
> +		return ret;
> +
> +	mutex_lock(&ks_lock);
> +	ret = ks_attr_check_live(attr);
> +	if (!ret)
> +		atomic_long_set(&attr->retval, val);
> +	mutex_unlock(&ks_lock);
> +
> +	return ret ? ret : count;
> +}
> +
> +static const struct file_operations ks_retval_fops = {
> +	.open		= ks_attr_open,
> +	.release	= ks_attr_release,
> +	.read		= ks_retval_read,
> +	.write	= ks_retval_write,
> +	.llseek	= default_llseek,
> +};
> +
> +static ssize_t ks_hits_read(struct file *file, char __user *ubuf,
> +			    size_t count, loff_t *ppos)
> +{
> +	struct ks_attr *attr = file->private_data;
> +	char buf[32];
> +	unsigned long hits;
> +	int ret, len;
> +
> +	mutex_lock(&ks_lock);
> +	ret = ks_attr_check_live(attr);
> +	hits = ks_attr_hits(attr);
> +	mutex_unlock(&ks_lock);
> +	if (ret)
> +		return ret;
> +	len = scnprintf(buf, sizeof(buf), "%lu\n", hits);
> +	return simple_read_from_buffer(ubuf, count, ppos, buf, len);
> +}
> +
> +static const struct file_operations ks_hits_fops = {
> +	.open		= ks_attr_open,
> +	.release	= ks_attr_release,
> +	.read		= ks_hits_read,
> +	.llseek		= default_llseek,
> +};
> +
> +static int ks_create_attr_dir(struct ks_attr *attr)
> +{
> +	struct dentry *d;
> +
> +	attr->dir = securityfs_create_dir(attr->kp.symbol_name, ks_fn_dir);
> +	if (IS_ERR(attr->dir))
> +		return PTR_ERR(attr->dir);
> +
> +	/* ks_attr_open looks the attr up by name; i_private is unused. */
> +	d = securityfs_create_file("retval", 0600, attr->dir,
> +				   NULL, &ks_retval_fops);
> +	if (IS_ERR(d))
> +		goto err;
> +	d = securityfs_create_file("hits", 0400, attr->dir,
> +				   NULL, &ks_hits_fops);
> +	if (IS_ERR(d))
> +		goto err;
> +	return 0;
> +err:
> +	securityfs_remove(attr->dir);
> +	attr->dir = NULL;
> +	return PTR_ERR(d);
> +}
> +
> +/* ------------------------------------------------------------------ *
> + * Engage / disengage                                                 *
> + * ------------------------------------------------------------------ */
> +
> +static int __ks_engage(const char *symbol, long retval, bool from_cmdline)
> +{
> +	struct ks_attr *attr;
> +	int ret;
> +
> +	if (!symbol || !*symbol)
> +		return -EINVAL;
> +
> +	if (!from_cmdline) {
> +		ret = security_locked_down(LOCKDOWN_KILLSWITCH);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	mutex_lock(&ks_lock);
> +
> +	if (ks_attr_lookup(symbol)) {
> +		ret = -EBUSY;
> +		goto out_unlock;
> +	}
> +
> +	attr = ks_attr_alloc(symbol);
> +	if (!attr) {
> +		ret = -ENOMEM;
> +		goto out_unlock;
> +	}
> +
> +	atomic_long_set(&attr->retval, retval);
> +
> +	ret = register_kprobe(&attr->kp);
> +	if (ret) {
> +		pr_warn("killswitch: register_kprobe(%s) failed: %d\n",
> +			symbol, ret);
> +		ks_attr_put(attr);
> +		goto out_unlock;
> +	}
> +
> +	ret = ks_create_attr_dir(attr);
> +	if (ret) {
> +		unregister_kprobe(&attr->kp);
> +		ks_attr_put(attr);
> +		goto out_unlock;
> +	}
> +
> +	list_add_tail(&attr->list, &ks_engaged_list);
> +	attr->engaged = true;
> +	add_taint(TAINT_KILLSWITCH, LOCKDEP_STILL_OK);
> +
> +	if (from_cmdline) {
> +		pr_warn("killswitch: engage %s=%ld source=cmdline\n",
> +			symbol, retval);
> +	} else {
> +		pr_warn("killswitch: engage %s=%ld uid=%u auid=%u ses=%u comm=%s\n",
> +			symbol, retval,
> +			from_kuid(&init_user_ns, current_uid()),
> +			from_kuid(&init_user_ns, audit_get_loginuid(current)),
> +			audit_get_sessionid(current),
> +			current->comm);
> +	}
> +	ret = 0;
> +
> +out_unlock:
> +	mutex_unlock(&ks_lock);
> +	return ret;
> +}
> +
> +int killswitch_engage(const char *symbol, long retval)
> +{
> +	return __ks_engage(symbol, retval, false);
> +}
> +
> +static int __ks_disengage(const char *symbol)
> +{
> +	struct ks_attr *attr;
> +	unsigned long hits;
> +	int ret = 0;
> +
> +	mutex_lock(&ks_lock);
> +	attr = ks_attr_lookup(symbol);
> +	if (!attr) {
> +		ret = -ENOENT;
> +		goto out_unlock;
> +	}
> +
> +	unregister_kprobe(&attr->kp);
> +	attr->engaged = false;
> +	list_del(&attr->list);
> +	hits = ks_attr_hits(attr);
> +	securityfs_remove(attr->dir);
> +
> +	pr_warn("killswitch: disengage %s hits=%lu uid=%u auid=%u ses=%u comm=%s\n",
> +		symbol, hits,
> +		from_kuid(&init_user_ns, current_uid()),
> +		from_kuid(&init_user_ns, audit_get_loginuid(current)),
> +		audit_get_sessionid(current),
> +		current->comm);
> +
> +	/* unregister_kprobe() already waited out in-flight pre-handlers. */
> +	ks_attr_put(attr);
> +
> +out_unlock:
> +	mutex_unlock(&ks_lock);
> +	return ret;
> +}
> +
> +int killswitch_disengage(const char *symbol)
> +{
> +	return __ks_disengage(symbol);
> +}
> +
> +bool killswitch_is_engaged(const char *symbol)
> +{
> +	bool engaged;
> +
> +	mutex_lock(&ks_lock);
> +	engaged = ks_attr_lookup(symbol) != NULL;
> +	mutex_unlock(&ks_lock);
> +	return engaged;
> +}
> +
> +static void ks_disengage_all_locked(void)
> +{
> +	struct ks_attr *attr, *n;
> +
> +	list_for_each_entry_safe(attr, n, &ks_engaged_list, list) {
> +		unregister_kprobe(&attr->kp);
> +		attr->engaged = false;
> +		list_del(&attr->list);
> +		securityfs_remove(attr->dir);
> +		pr_warn("killswitch: disengage %s hits=%lu (disengage_all)\n",
> +			attr->kp.symbol_name, ks_attr_hits(attr));
> +		ks_attr_put(attr);
> +	}
> +}
> +
> +/* ------------------------------------------------------------------ *
> + * Module unload: drop engagements on functions in the going module   *
> + * ------------------------------------------------------------------ */
> +
> +static int ks_module_notify(struct notifier_block *nb, unsigned long action,
> +			    void *data)
> +{
> +	struct module *mod = data;
> +	struct ks_attr *attr, *n;
> +
> +	if (action != MODULE_STATE_GOING)
> +		return NOTIFY_DONE;
> +
> +	mutex_lock(&ks_lock);
> +	list_for_each_entry_safe(attr, n, &ks_engaged_list, list) {
> +		if (!attr->kp.addr ||
> +		    __module_address((unsigned long)attr->kp.addr) != mod)
> +			continue;
> +
> +		pr_warn("killswitch: %s mitigation lost: module %s unloading; re-engage after reload if still needed\n",
> +			attr->kp.symbol_name, mod->name);
> +		unregister_kprobe(&attr->kp);
> +		attr->engaged = false;
> +		list_del(&attr->list);
> +		securityfs_remove(attr->dir);
> +		ks_attr_put(attr);
> +	}
> +	mutex_unlock(&ks_lock);
> +	return NOTIFY_DONE;
> +}
> +
> +static struct notifier_block ks_module_nb = {
> +	.notifier_call = ks_module_notify,
> +};
> +
> +/* ------------------------------------------------------------------ *
> + * Top-level securityfs files: control / engaged / taint              *
> + * ------------------------------------------------------------------ */
> +
> +static int ks_engaged_show(struct seq_file *m, void *v)
> +{
> +	struct ks_attr *attr;
> +
> +	mutex_lock(&ks_lock);
> +	list_for_each_entry(attr, &ks_engaged_list, list) {
> +		seq_printf(m, "%s retval=%ld hits=%lu\n",
> +			   attr->kp.symbol_name,
> +			   atomic_long_read(&attr->retval),
> +			   ks_attr_hits(attr));
> +	}
> +	mutex_unlock(&ks_lock);
> +	return 0;
> +}
> +
> +static int ks_engaged_open(struct inode *inode, struct file *file)
> +{
> +	return single_open(file, ks_engaged_show, NULL);
> +}
> +
> +static const struct file_operations ks_engaged_fops = {
> +	.open		= ks_engaged_open,
> +	.read		= seq_read,
> +	.llseek		= seq_lseek,
> +	.release	= single_release,
> +};
> +
> +static ssize_t ks_taint_read(struct file *file, char __user *ubuf,
> +			     size_t count, loff_t *ppos)
> +{
> +	char buf[4];
> +	int len;
> +
> +	len = scnprintf(buf, sizeof(buf), "%d\n",
> +			test_taint(TAINT_KILLSWITCH) ? 1 : 0);
> +	return simple_read_from_buffer(ubuf, count, ppos, buf, len);
> +}
> +
> +static const struct file_operations ks_taint_fops = {
> +	.open	= simple_open,
> +	.read	= ks_taint_read,
> +	.llseek	= default_llseek,
> +};
> +
> +/*
> + * control: parse one of:
> + *   engage <symbol> <retval>
> + *   disengage <symbol>
> + *   disengage_all
> + */
> +static ssize_t ks_control_write(struct file *file, const char __user *ubuf,
> +				size_t count, loff_t *ppos)
> +{
> +	char *buf, *cur, *verb, *sym, *retstr;
> +	long retval = 0;
> +	int ret;
> +
> +	if (!capable(CAP_SYS_ADMIN))
> +		return -EPERM;
> +
> +	if (count == 0 || count > 4096)
> +		return -EINVAL;
> +
> +	buf = memdup_user_nul(ubuf, count);
> +	if (IS_ERR(buf))
> +		return PTR_ERR(buf);
> +
> +	cur = strim(buf);
> +	verb = strsep(&cur, " \t\n");
> +	if (!verb || !*verb) {
> +		ret = -EINVAL;
> +		goto out;
> +	}
> +
> +	if (!strcmp(verb, "disengage_all")) {
> +		mutex_lock(&ks_lock);
> +		ks_disengage_all_locked();
> +		mutex_unlock(&ks_lock);
> +		ret = count;
> +		goto out;
> +	}
> +
> +	sym = strsep(&cur, " \t\n");
> +	if (!sym || !*sym) {
> +		ret = -EINVAL;
> +		goto out;
> +	}
> +
> +	if (!strcmp(verb, "disengage")) {
> +		ret = __ks_disengage(sym);
> +		ret = ret ? ret : count;
> +		goto out;
> +	}
> +
> +	if (strcmp(verb, "engage")) {
> +		ret = -EINVAL;
> +		goto out;
> +	}
> +
> +	retstr = strsep(&cur, " \t\n");
> +	if (!retstr || !*retstr) {
> +		ret = -EINVAL;
> +		goto out;
> +	}
> +	if (kstrtol(retstr, 0, &retval)) {
> +		ret = -EINVAL;
> +		goto out;
> +	}
> +
> +	ret = killswitch_engage(sym, retval);
> +	if (!ret)
> +		ret = count;
> +
> +out:
> +	kfree(buf);
> +	return ret;
> +}
> +
> +static const struct file_operations ks_control_fops = {
> +	.open	= simple_open,
> +	.write	= ks_control_write,
> +	.llseek	= noop_llseek,
> +};
> +
> +/* ------------------------------------------------------------------ *
> + * Boot parameter:                                                    *
> + *   killswitch=fn1=-1,fn2=0,fn3=-22                                  *
> + * ------------------------------------------------------------------ */
> +
> +#define KS_BOOT_BUF 1024
> +static char ks_boot_buf[KS_BOOT_BUF] __initdata;
> +static bool ks_boot_present __initdata;
> +
> +static int __init ks_boot_setup(char *str)
> +{
> +	if (!str)
> +		return 0;
> +	strscpy(ks_boot_buf, str, sizeof(ks_boot_buf));
> +	ks_boot_present = true;
> +	return 1;
> +}
> +__setup("killswitch=", ks_boot_setup);
> +
> +static void __init ks_apply_boot_params(void)
> +{
> +	char *cur, *tok;
> +	long retval;
> +
> +	if (!ks_boot_present)
> +		return;
> +
> +	cur = ks_boot_buf;
> +	while ((tok = strsep(&cur, ",")) != NULL) {
> +		char *eq, *sym, *retstr;
> +
> +		if (!*tok)
> +			continue;
> +		eq = strchr(tok, '=');
> +		if (!eq) {
> +			pr_warn("killswitch: cmdline missing '=': %s\n", tok);
> +			continue;
> +		}
> +		*eq++ = '\0';
> +		sym = tok;
> +		retstr = eq;
> +
> +		if (kstrtol(retstr, 0, &retval)) {
> +			pr_warn("killswitch: cmdline bad retval %s=%s\n",
> +				sym, retstr);
> +			continue;
> +		}
> +
> +		if (__ks_engage(sym, retval, true))
> +			pr_warn("killswitch: cmdline engage %s failed\n", sym);
> +	}
> +}
> +
> +/* ------------------------------------------------------------------ *
> + * Init                                                               *
> + * ------------------------------------------------------------------ */
> +
> +static int __init killswitch_init(void)
> +{
> +	struct dentry *d;
> +
> +	ks_root_dir = securityfs_create_dir("killswitch", NULL);
> +	if (IS_ERR(ks_root_dir))
> +		return PTR_ERR(ks_root_dir);
> +
> +	d = securityfs_create_file("control", 0200, ks_root_dir,
> +				   NULL, &ks_control_fops);
> +	if (IS_ERR(d))
> +		goto err;
> +	d = securityfs_create_file("engaged", 0444, ks_root_dir,
> +				   NULL, &ks_engaged_fops);
> +	if (IS_ERR(d))
> +		goto err;
> +	d = securityfs_create_file("taint", 0444, ks_root_dir,
> +				   NULL, &ks_taint_fops);
> +	if (IS_ERR(d))
> +		goto err;
> +
> +	ks_fn_dir = securityfs_create_dir("fn", ks_root_dir);
> +	if (IS_ERR(ks_fn_dir)) {
> +		d = ks_fn_dir;
> +		goto err;
> +	}
> +
> +	register_module_notifier(&ks_module_nb);
> +	ks_apply_boot_params();
> +
> +	pr_info("killswitch: ready (sysfs at /sys/kernel/security/killswitch/)\n");
> +	return 0;
> +
> +err:
> +	securityfs_remove(ks_root_dir);
> +	return PTR_ERR(d);
> +}
> +late_initcall(killswitch_init);
> +
> +/* ------------------------------------------------------------------ *
> + * KUnit tests                                                        *
> + * ------------------------------------------------------------------ */
> +
> +#if IS_ENABLED(CONFIG_KUNIT)
> +#include <kunit/test.h>
> +
> +/* Non-static so kallsyms resolves them without CONFIG_KALLSYMS_ALL. */
> +int ks_kunit_target_int(int x);
> +void *ks_kunit_target_ptr(int x);
> +
> +#if __has_attribute(__noipa__)
> +# define KS_KUNIT_NOIPA __attribute__((__noipa__))
> +#else
> +# define KS_KUNIT_NOIPA noinline __noclone
> +#endif
> +
> +KS_KUNIT_NOIPA int ks_kunit_target_int(int x)
> +{
> +	return x + 1;
> +}
> +
> +KS_KUNIT_NOIPA void *ks_kunit_target_ptr(int x)
> +{
> +	return ERR_PTR(-EIO);
> +}
> +
> +static int ks_kunit_init(struct kunit *test)
> +{
> +	if (security_locked_down(LOCKDOWN_KILLSWITCH))
> +		kunit_skip(test, "integrity lockdown blocks killswitch_engage()");
> +	return 0;
> +}
> +
> +static int ks_kunit_init_lockdown(struct kunit *test)
> +{
> +	if (!security_locked_down(LOCKDOWN_KILLSWITCH))
> +		kunit_skip(test, "requires lockdown=integrity");
> +	return 0;
> +}
> +
> +static void ks_disengage_quiet(const char *sym)
> +{
> +	if (killswitch_is_engaged(sym))
> +		killswitch_disengage(sym);
> +}
> +
> +static void ks_test_engage_int(struct kunit *test)
> +{
> +	int ret;
> +
> +	ret = killswitch_engage("ks_kunit_target_int", -EPERM);
> +	KUNIT_EXPECT_EQ(test, ret, 0);
> +	KUNIT_EXPECT_EQ(test, ks_kunit_target_int(7), -EPERM);
> +	KUNIT_EXPECT_EQ(test, killswitch_disengage("ks_kunit_target_int"), 0);
> +	KUNIT_EXPECT_EQ(test, ks_kunit_target_int(7), 8);
> +}
> +
> +static void ks_test_double_engage(struct kunit *test)
> +{
> +	KUNIT_ASSERT_EQ(test,
> +		killswitch_engage("ks_kunit_target_int", 0), 0);
> +	KUNIT_EXPECT_EQ(test,
> +		killswitch_engage("ks_kunit_target_int", 0), -EBUSY);
> +	ks_disengage_quiet("ks_kunit_target_int");
> +}
> +
> +static void ks_test_disengage_unknown(struct kunit *test)
> +{
> +	KUNIT_EXPECT_EQ(test,
> +		killswitch_disengage("ks_kunit_target_int"), -ENOENT);
> +}
> +
> +static void ks_test_pointer_target(struct kunit *test)
> +{
> +	long retval = (long)(unsigned long)ERR_PTR(-EACCES);
> +
> +	KUNIT_ASSERT_EQ(test,
> +		killswitch_engage("ks_kunit_target_ptr", retval), 0);
> +	KUNIT_EXPECT_TRUE(test, IS_ERR(ks_kunit_target_ptr(0)));
> +	KUNIT_EXPECT_EQ(test, PTR_ERR(ks_kunit_target_ptr(0)), -EACCES);
> +	ks_disengage_quiet("ks_kunit_target_ptr");
> +}
> +
> +static void ks_test_taint_set(struct kunit *test)
> +{
> +	KUNIT_ASSERT_EQ(test,
> +		killswitch_engage("ks_kunit_target_int", 0), 0);
> +	KUNIT_EXPECT_TRUE(test, test_taint(TAINT_KILLSWITCH));
> +	ks_disengage_quiet("ks_kunit_target_int");
> +	/* taint must persist even after disengage */
> +	KUNIT_EXPECT_TRUE(test, test_taint(TAINT_KILLSWITCH));
> +}
> +
> +static void ks_test_hits_counter(struct kunit *test)
> +{
> +	struct ks_attr *attr;
> +	int i;
> +
> +	KUNIT_ASSERT_EQ(test,
> +		killswitch_engage("ks_kunit_target_int", 0), 0);
> +
> +	for (i = 0; i < 17; i++)
> +		(void)ks_kunit_target_int(i);
> +
> +	mutex_lock(&ks_lock);
> +	attr = ks_attr_lookup("ks_kunit_target_int");
> +	KUNIT_EXPECT_NOT_NULL(test, attr);
> +	if (attr)
> +		KUNIT_EXPECT_EQ(test, ks_attr_hits(attr), 17UL);
> +	mutex_unlock(&ks_lock);
> +
> +	ks_disengage_quiet("ks_kunit_target_int");
> +}
> +
> +static struct kunit_case ks_kunit_cases[] = {
> +	KUNIT_CASE(ks_test_engage_int),
> +	KUNIT_CASE(ks_test_double_engage),
> +	KUNIT_CASE(ks_test_disengage_unknown),
> +	KUNIT_CASE(ks_test_pointer_target),
> +	KUNIT_CASE(ks_test_taint_set),
> +	KUNIT_CASE(ks_test_hits_counter),
> +	{}
> +};
> +
> +static struct kunit_suite ks_kunit_suite = {
> +	.name = "killswitch",
> +	.init = ks_kunit_init,
> +	.test_cases = ks_kunit_cases,
> +};
> +
> +/*
> + * Lockdown suite. Skipped unless the kernel was booted with
> + * lockdown=integrity (or higher). Run together with
> + * killswitch=ks_kunit_target_int=... on the same cmdline to also
> + * exercise the cmdline-bypass and disengage-under-lockdown paths.
> + */
> +static void ks_test_lockdown_runtime_engage(struct kunit *test)
> +{
> +	KUNIT_EXPECT_EQ(test,
> +		killswitch_engage("ks_kunit_target_int", 0), -EPERM);
> +}
> +
> +static void ks_test_lockdown_cmdline_disengage(struct kunit *test)
> +{
> +	if (!killswitch_is_engaged("ks_kunit_target_int"))
> +		kunit_skip(test,
> +			   "requires killswitch=ks_kunit_target_int=... on cmdline");
> +	KUNIT_EXPECT_EQ(test,
> +		killswitch_disengage("ks_kunit_target_int"), 0);
> +}
> +
> +static struct kunit_case ks_kunit_lockdown_cases[] = {
> +	KUNIT_CASE(ks_test_lockdown_runtime_engage),
> +	KUNIT_CASE(ks_test_lockdown_cmdline_disengage),
> +	{}
> +};
> +
> +static struct kunit_suite ks_kunit_lockdown_suite = {
> +	.name = "killswitch_lockdown",
> +	.init = ks_kunit_init_lockdown,
> +	.test_cases = ks_kunit_lockdown_cases,
> +};
> +
> +kunit_test_suites(&ks_kunit_suite, &ks_kunit_lockdown_suite);
> +
> +#endif /* CONFIG_KUNIT */
> +
> diff --git a/kernel/panic.c b/kernel/panic.c
> index 20feada5319d4..8ee174c7b7dd0 100644
> --- a/kernel/panic.c
> +++ b/kernel/panic.c
> @@ -825,6 +825,7 @@ const struct taint_flag taint_flags[TAINT_FLAGS_COUNT] = {
>   	TAINT_FLAG(RANDSTRUCT,			'T', ' '),
>   	TAINT_FLAG(TEST,			'N', ' '),
>   	TAINT_FLAG(FWCTL,			'J', ' '),
> +	TAINT_FLAG(KILLSWITCH,			'H', ' '),
>   };
>   
>   #undef TAINT_FLAG
> diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
> index 8ff5adcfe1e0a..5770639c7b0ea 100644
> --- a/lib/Kconfig.debug
> +++ b/lib/Kconfig.debug
> @@ -3349,6 +3349,19 @@ config TEST_HMM
>   
>   	  If unsure, say N.
>   
> +config TEST_KILLSWITCH
> +	tristate "Test module for the killswitch mitigation primitive"
> +	depends on KILLSWITCH && DEBUG_FS
> +	depends on m
> +	help
> +	  Build a module that exposes a deliberately-vulnerable function
> +	  ks_test_vuln() and a debugfs trigger /sys/kernel/debug/test_killswitch/fire.
> +	  The killswitch selftest in tools/testing/selftests/killswitch/
> +	  uses this to confirm engaging a killswitch suppresses the BUG()
> +	  the function would otherwise hit.
> +
> +	  If unsure, say N.
> +
>   config TEST_FREE_PAGES
>   	tristate "Test freeing pages"
>   	help
> diff --git a/lib/Makefile b/lib/Makefile
> index f33a24bf1c19a..d763225340674 100644
> --- a/lib/Makefile
> +++ b/lib/Makefile
> @@ -100,6 +100,7 @@ obj-$(CONFIG_TEST_MEMCAT_P) += test_memcat_p.o
>   obj-$(CONFIG_TEST_OBJAGG) += test_objagg.o
>   obj-$(CONFIG_TEST_MEMINIT) += test_meminit.o
>   obj-$(CONFIG_TEST_LOCKUP) += test_lockup.o
> +obj-$(CONFIG_TEST_KILLSWITCH) += test_killswitch.o
>   obj-$(CONFIG_TEST_HMM) += test_hmm.o
>   obj-$(CONFIG_TEST_FREE_PAGES) += test_free_pages.o
>   obj-$(CONFIG_TEST_REF_TRACKER) += test_ref_tracker.o
> diff --git a/lib/test_killswitch.c b/lib/test_killswitch.c
> new file mode 100644
> index 0000000000000..cc2584ad652ff
> --- /dev/null
> +++ b/lib/test_killswitch.c
> @@ -0,0 +1,85 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Test target for the killswitch selftest.  ks_test_vuln() returns
> + * -EBADMSG on a magic input, standing in for "the buggy path runs
> + * and produces a bad outcome".  Engaging killswitch on this function
> + * with retval 0 is the mitigation.
> + *
> + * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
> + */
> +
> +#include <linux/debugfs.h>
> +#include <linux/fs.h>
> +#include <linux/init.h>
> +#include <linux/kernel.h>
> +#include <linux/module.h>
> +#include <linux/uaccess.h>
> +
> +#define KS_TEST_MAGIC	0xC0FFEEL
> +
> +int ks_test_vuln(long magic);
> +
> +/*
> + * Returns -EBADMSG on the magic input -- stands in for "the buggy
> + * path runs and produces a bad outcome".  Engaging a killswitch on
> + * this function with retval 0 represents the mitigation: even on
> + * the magic input, callers see success because the body never runs.
> + *
> + * noipa prevents inlining/IPA so the call actually reaches the
> + * kprobe-instrumented entry point.
> + */
> +noinline int ks_test_vuln(long magic)
> +{
> +	if (magic == KS_TEST_MAGIC)
> +		return -EBADMSG;
> +	return 0;
> +}
> +EXPORT_SYMBOL_GPL(ks_test_vuln);
> +
> +static struct dentry *ks_test_dir;
> +
> +static ssize_t ks_test_fire_write(struct file *file, const char __user *ubuf,
> +				  size_t count, loff_t *ppos)
> +{
> +	char buf[32];
> +	long magic;
> +	int ret;
> +
> +	if (count == 0 || count >= sizeof(buf))
> +		return -EINVAL;
> +	if (copy_from_user(buf, ubuf, count))
> +		return -EFAULT;
> +	buf[count] = '\0';
> +
> +	ret = kstrtol(strim(buf), 0, &magic);
> +	if (ret)
> +		return ret;
> +
> +	ret = ks_test_vuln(magic);
> +	return ret ? ret : count;
> +}
> +
> +static const struct file_operations ks_test_fire_fops = {
> +	.write	= ks_test_fire_write,
> +	.open	= simple_open,
> +	.llseek	= noop_llseek,
> +};
> +
> +static int __init test_killswitch_init(void)
> +{
> +	ks_test_dir = debugfs_create_dir("test_killswitch", NULL);
> +	debugfs_create_file("fire", 0200, ks_test_dir, NULL,
> +			    &ks_test_fire_fops);
> +	pr_info("test_killswitch: loaded (magic=0x%lx)\n", KS_TEST_MAGIC);
> +	return 0;
> +}
> +module_init(test_killswitch_init);
> +
> +static void __exit test_killswitch_exit(void)
> +{
> +	debugfs_remove_recursive(ks_test_dir);
> +}
> +module_exit(test_killswitch_exit);
> +
> +MODULE_LICENSE("GPL v2");
> +MODULE_DESCRIPTION("Deliberately-vulnerable target for killswitch selftest");
> diff --git a/security/security.c b/security/security.c
> index 4e999f0236516..bf700abc911a9 100644
> --- a/security/security.c
> +++ b/security/security.c
> @@ -62,6 +62,7 @@ const char *const lockdown_reasons[LOCKDOWN_CONFIDENTIALITY_MAX + 1] = {
>   	[LOCKDOWN_DBG_WRITE_KERNEL] = "use of kgdb/kdb to write kernel RAM",
>   	[LOCKDOWN_RTAS_ERROR_INJECTION] = "RTAS error injection",
>   	[LOCKDOWN_XEN_USER_ACTIONS] = "Xen guest user action",
> +	[LOCKDOWN_KILLSWITCH] = "engaging a killswitch",
>   	[LOCKDOWN_INTEGRITY_MAX] = "integrity",
>   	[LOCKDOWN_KCORE] = "/proc/kcore access",
>   	[LOCKDOWN_KPROBES] = "use of kprobes",
> diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
> index 6e59b8f63e416..04c3f8c5ff229 100644
> --- a/tools/testing/selftests/Makefile
> +++ b/tools/testing/selftests/Makefile
> @@ -53,6 +53,7 @@ TARGETS += ipc
>   TARGETS += ir
>   TARGETS += kcmp
>   TARGETS += kexec
> +TARGETS += killswitch
>   TARGETS += kselftest_harness
>   TARGETS += kvm
>   TARGETS += landlock
> diff --git a/tools/testing/selftests/killswitch/.gitignore b/tools/testing/selftests/killswitch/.gitignore
> new file mode 100644
> index 0000000000000..cbf204ce18615
> --- /dev/null
> +++ b/tools/testing/selftests/killswitch/.gitignore
> @@ -0,0 +1 @@
> +cve_31431_test
> diff --git a/tools/testing/selftests/killswitch/Makefile b/tools/testing/selftests/killswitch/Makefile
> new file mode 100644
> index 0000000000000..ccf41165cb73d
> --- /dev/null
> +++ b/tools/testing/selftests/killswitch/Makefile
> @@ -0,0 +1,8 @@
> +# SPDX-License-Identifier: GPL-2.0
> +# Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
> +TEST_GEN_PROGS := cve_31431_test cve_43284_test
> +TEST_PROGS := killswitch_test.sh
> +
> +CFLAGS += -O2 -g -std=gnu99 -Wall $(KHDR_INCLUDES)
> +
> +include ../lib.mk
> diff --git a/tools/testing/selftests/killswitch/cve_31431_test.c b/tools/testing/selftests/killswitch/cve_31431_test.c
> new file mode 100644
> index 0000000000000..1ff817c51d881
> --- /dev/null
> +++ b/tools/testing/selftests/killswitch/cve_31431_test.c
> @@ -0,0 +1,162 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * AF_ALG AEAD round-trip prober.  The killswitch selftest uses this
> + * to demonstrate that engaging a killswitch on af_alg_sendmsg
> + * neuters AF_ALG operations (sendmsg returns -EPERM), mitigating
> + * any AF_ALG-reachable bug whose exploit primitive runs from the
> + * send path.
> + *
> + * Exit codes:
> + *   0  AEAD round-trip succeeded (function intact)
> + *   1  AEAD round-trip refused (mitigation engaged)
> + *   2  setup error (no AF_ALG, missing aead/gcm(aes), etc.) -> SKIP
> + *
> + * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
> + */
> +
> +#include <errno.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/socket.h>
> +#include <unistd.h>
> +#include <linux/if_alg.h>
> +
> +#define KEY_LEN		16
> +#define IV_LEN		12
> +#define AAD_LEN		16
> +#define PT_LEN		64
> +#define TAG_LEN		16
> +#define EXPECTED_LEN	(AAD_LEN + PT_LEN + TAG_LEN)
> +
> +#ifndef AF_ALG
> +#define AF_ALG		38
> +#endif
> +#ifndef SOL_ALG
> +#define SOL_ALG		279
> +#endif
> +
> +int main(void)
> +{
> +	struct sockaddr_alg sa = {
> +		.salg_family = AF_ALG,
> +		.salg_type   = "aead",
> +		.salg_name   = "gcm(aes)",
> +	};
> +	unsigned char key[KEY_LEN] = { 0 };
> +	unsigned char iv[IV_LEN]   = { 0 };
> +	unsigned char buf[1024]    = { 0 };
> +	struct msghdr msg = { 0 };
> +	struct iovec iov;
> +	struct cmsghdr *cmsg;
> +	struct af_alg_iv *aiv;
> +	char cbuf[256] = { 0 };
> +	int *p_op, *p_assoclen;
> +	int sk, opfd;
> +	ssize_t n;
> +
> +	sk = socket(AF_ALG, SOCK_SEQPACKET, 0);
> +	if (sk < 0) {
> +		fprintf(stderr, "AF_ALG socket: %s -- skip\n", strerror(errno));
> +		return 2;
> +	}
> +	if (bind(sk, (struct sockaddr *)&sa, sizeof(sa))) {
> +		fprintf(stderr, "bind aead/gcm(aes): %s -- skip\n",
> +			strerror(errno));
> +		close(sk);
> +		return 2;
> +	}
> +	if (setsockopt(sk, SOL_ALG, ALG_SET_KEY, key, KEY_LEN)) {
> +		fprintf(stderr, "ALG_SET_KEY: %s -- skip\n", strerror(errno));
> +		close(sk);
> +		return 2;
> +	}
> +	if (setsockopt(sk, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, TAG_LEN)) {
> +		fprintf(stderr, "ALG_SET_AEAD_AUTHSIZE: %s -- skip\n",
> +			strerror(errno));
> +		close(sk);
> +		return 2;
> +	}
> +
> +	opfd = accept(sk, NULL, 0);
> +	if (opfd < 0) {
> +		fprintf(stderr, "accept: %s -- skip\n", strerror(errno));
> +		close(sk);
> +		return 2;
> +	}
> +
> +	/* control message: ENCRYPT op + IV + assoclen */
> +	msg.msg_control    = cbuf;
> +	msg.msg_controllen = CMSG_SPACE(sizeof(int))
> +			   + CMSG_SPACE(sizeof(*aiv) + IV_LEN)
> +			   + CMSG_SPACE(sizeof(int));
> +
> +	cmsg = CMSG_FIRSTHDR(&msg);
> +	cmsg->cmsg_level = SOL_ALG;
> +	cmsg->cmsg_type  = ALG_SET_OP;
> +	cmsg->cmsg_len   = CMSG_LEN(sizeof(int));
> +	p_op = (int *)CMSG_DATA(cmsg);
> +	*p_op = ALG_OP_ENCRYPT;
> +
> +	cmsg = CMSG_NXTHDR(&msg, cmsg);
> +	cmsg->cmsg_level = SOL_ALG;
> +	cmsg->cmsg_type  = ALG_SET_IV;
> +	cmsg->cmsg_len   = CMSG_LEN(sizeof(*aiv) + IV_LEN);
> +	aiv = (struct af_alg_iv *)CMSG_DATA(cmsg);
> +	aiv->ivlen = IV_LEN;
> +	memcpy(aiv->iv, iv, IV_LEN);
> +
> +	cmsg = CMSG_NXTHDR(&msg, cmsg);
> +	cmsg->cmsg_level = SOL_ALG;
> +	cmsg->cmsg_type  = ALG_SET_AEAD_ASSOCLEN;
> +	cmsg->cmsg_len   = CMSG_LEN(sizeof(int));
> +	p_assoclen = (int *)CMSG_DATA(cmsg);
> +	*p_assoclen = AAD_LEN;
> +
> +	/* AAD || plaintext */
> +	memset(buf, 0xaa, AAD_LEN);
> +	memset(buf + AAD_LEN, 0x55, PT_LEN);
> +	iov.iov_base = buf;
> +	iov.iov_len  = AAD_LEN + PT_LEN;
> +	msg.msg_iov    = &iov;
> +	msg.msg_iovlen = 1;
> +
> +	n = sendmsg(opfd, &msg, 0);
> +	if (n < 0) {
> +		/*
> +		 * sendmsg refused: this is exactly the killswitch
> +		 * af_alg_sendmsg=-EPERM mitigation outcome.  Distinct
> +		 * exit code from setup failure so the test script can
> +		 * tell them apart.
> +		 */
> +		fprintf(stderr, "sendmsg: %s -- mitigation engaged?\n",
> +			strerror(errno));
> +		close(opfd); close(sk);
> +		return 1;
> +	}
> +
> +	/* recv: AAD echoed, plus ciphertext + tag */
> +	memset(buf, 0, sizeof(buf));
> +	n = read(opfd, buf, EXPECTED_LEN);
> +	close(opfd); close(sk);
> +
> +	if (n == 0) {
> +		printf("AEAD returned 0 bytes -- killswitch mitigation engaged\n");
> +		return 1;
> +	}
> +	if (n != EXPECTED_LEN) {
> +		fprintf(stderr,
> +			"AEAD short read: got %zd, expected %d -- mitigated?\n",
> +			n, EXPECTED_LEN);
> +		return 1;
> +	}
> +
> +	/* sanity: ciphertext (after AAD) shouldn't equal the plaintext bytes */
> +	if (memcmp(buf + AAD_LEN, buf + AAD_LEN + 1, PT_LEN - 1) == 0) {
> +		fprintf(stderr, "AEAD output looks unencrypted\n");
> +		return 2;
> +	}
> +
> +	printf("AEAD round-trip OK (%zd bytes)\n", n);
> +	return 0;
> +}
> diff --git a/tools/testing/selftests/killswitch/cve_43284_test.c b/tools/testing/selftests/killswitch/cve_43284_test.c
> new file mode 100644
> index 0000000000000..4771cb0957dc1
> --- /dev/null
> +++ b/tools/testing/selftests/killswitch/cve_43284_test.c
> @@ -0,0 +1,88 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * UDP loopback round-trip prober.  Wrapped by killswitch_test.sh with
> + * an IPsec ESP SA + policy pair on loopback, this demonstrates that
> + * engaging a killswitch on esp_input drops inbound ESP packets before
> + * decapsulation, mitigating CVE-2026-43284 ("Dirty Frag", upstream fix
> + * xfrm: esp: avoid in-place decrypt on shared skb frags).
> + *
> + * The binary itself knows nothing about ESP -- it sends one UDP
> + * datagram to itself and waits up to a second for delivery.
> + *
> + * Exit codes:
> + *   0  UDP round-trip succeeded (no mitigation in effect)
> + *   1  UDP recv timed out (mitigation engaged)
> + *   2  setup error -> SKIP
> + *
> + * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
> + */
> +
> +#include <arpa/inet.h>
> +#include <errno.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/socket.h>
> +#include <sys/time.h>
> +#include <unistd.h>
> +
> +#define UDP_PORT 53435
> +#define PROBE    "ks-43284-probe"
> +
> +int main(void)
> +{
> +	struct sockaddr_in addr = {
> +		.sin_family      = AF_INET,
> +		.sin_port        = htons(UDP_PORT),
> +		.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
> +	};
> +	struct timeval tv = { .tv_sec = 1, .tv_usec = 0 };
> +	char buf[64];
> +	int sk;
> +	ssize_t n;
> +
> +	sk = socket(AF_INET, SOCK_DGRAM, 0);
> +	if (sk < 0) {
> +		fprintf(stderr, "socket: %s -- skip\n", strerror(errno));
> +		return 2;
> +	}
> +	if (bind(sk, (struct sockaddr *)&addr, sizeof(addr))) {
> +		fprintf(stderr, "bind: %s -- skip\n", strerror(errno));
> +		close(sk);
> +		return 2;
> +	}
> +	if (setsockopt(sk, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))) {
> +		fprintf(stderr, "SO_RCVTIMEO: %s -- skip\n", strerror(errno));
> +		close(sk);
> +		return 2;
> +	}
> +
> +	if (sendto(sk, PROBE, sizeof(PROBE) - 1, 0,
> +		   (struct sockaddr *)&addr, sizeof(addr)) < 0) {
> +		fprintf(stderr, "sendto: %s -- skip\n", strerror(errno));
> +		close(sk);
> +		return 2;
> +	}
> +
> +	memset(buf, 0, sizeof(buf));
> +	n = recvfrom(sk, buf, sizeof(buf), 0, NULL, NULL);
> +	close(sk);
> +
> +	if (n < 0) {
> +		if (errno == EAGAIN || errno == EWOULDBLOCK) {
> +			fprintf(stderr,
> +				"recvfrom: timeout -- mitigation engaged?\n");
> +			return 1;
> +		}
> +		fprintf(stderr, "recvfrom: %s\n", strerror(errno));
> +		return 2;
> +	}
> +	if (n != (ssize_t)(sizeof(PROBE) - 1) ||
> +	    memcmp(buf, PROBE, sizeof(PROBE) - 1)) {
> +		fprintf(stderr, "recvfrom: bad payload (%zd bytes)\n", n);
> +		return 2;
> +	}
> +
> +	printf("UDP round-trip OK (%zd bytes)\n", n);
> +	return 0;
> +}
> diff --git a/tools/testing/selftests/killswitch/killswitch_test.sh b/tools/testing/selftests/killswitch/killswitch_test.sh
> new file mode 100755
> index 0000000000000..ea3fd394a984f
> --- /dev/null
> +++ b/tools/testing/selftests/killswitch/killswitch_test.sh
> @@ -0,0 +1,254 @@
> +#!/bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# End-to-end killswitch selftest.  Drives the test_killswitch module
> +# through an engage/disengage cycle and confirms each transition
> +# behaves as expected.  Also runs the AF_ALG mitigation proof.
> +#
> +# Requirements (see Documentation/admin-guide/killswitch.rst):
> +#   - CONFIG_KILLSWITCH=y
> +#   - CONFIG_TEST_KILLSWITCH=m
> +#   - run as root (CAP_SYS_ADMIN)
> +#
> +# Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
> +#
> +
> +set -u
> +
> +KS=/sys/kernel/security/killswitch
> +TRIG=/sys/kernel/debug/test_killswitch/fire
> +
> +NOMOD=0
> +SKIP_RC=4
> +N=0
> +FAIL=0
> +
> +ksft_pass() { N=$((N+1));    echo "ok $N - $*"; }
> +ksft_fail() { N=$((N+1)); FAIL=$((FAIL+1)); echo "not ok $N - $*"; }
> +ksft_skip() { echo "ok 1 - SKIP $*"; echo "1..1"; exit $SKIP_RC; }
> +
> +[[ $EUID -eq 0 ]] || ksft_skip "must be root"
> +[[ -d $KS    ]] || ksft_skip "$KS not present (CONFIG_KILLSWITCH disabled?)"
> +
> +if ! modprobe test_killswitch 2>/dev/null; then
> +	NOMOD=1
> +fi
> +[[ -e $TRIG ]] || ksft_skip "$TRIG missing (test_killswitch.ko not installed?)"
> +
> +cleanup() {
> +	echo "disengage_all" > $KS/control 2>/dev/null || true
> +	[[ $NOMOD -eq 0 ]] && rmmod test_killswitch 2>/dev/null || true
> +}
> +trap cleanup EXIT
> +
> +# --- pre-engage: bad path runs, write fails with EBADMSG ---
> +if echo 0xC0FFEE > $TRIG 2>/dev/null; then
> +	ksft_fail "pre-engage: write should have failed (-EBADMSG)"
> +else
> +	[[ $? -ne 0 ]] && ksft_pass "pre-engage: bad path returns error" \
> +	             || ksft_fail "pre-engage: unexpected outcome"
> +fi
> +
> +# --- engage ---
> +echo "engage ks_test_vuln 0" > $KS/control
> +grep -q "^ks_test_vuln" $KS/engaged \
> +	&& ksft_pass "engage: ks_test_vuln in engaged list" \
> +	|| ksft_fail "engage: missing from engaged list"
> +
> +[[ $(cat $KS/taint) == 1 ]] \
> +	&& ksft_pass "engage: taint set" \
> +	|| ksft_fail "engage: taint not set"
> +
> +[[ -d $KS/fn/ks_test_vuln ]] \
> +	&& ksft_pass "engage: per-fn dir created" \
> +	|| ksft_fail "engage: per-fn dir missing"
> +
> +# --- post-engage: BUG suppressed; write returns successfully ---
> +if echo 0xC0FFEE > $TRIG 2>/dev/null; then
> +	ksft_pass "post-engage: BUG suppressed, write succeeded"
> +else
> +	ksft_fail "post-engage: write should succeed"
> +fi
> +
> +[[ $(cat $KS/fn/ks_test_vuln/hits) -ge 1 ]] \
> +	&& ksft_pass "post-engage: hits counter incremented" \
> +	|| ksft_fail "post-engage: hits counter did not move"
> +
> +# --- retval rewrite is a plain write (no validation) ---
> +echo 7 > $KS/fn/ks_test_vuln/retval
> +[[ $(cat $KS/fn/ks_test_vuln/retval) == 7 ]] \
> +	&& ksft_pass "retval rewrite round-trips" \
> +	|| ksft_fail "retval rewrite failed"
> +
> +# --- engage on a kprobe-rejected function fails ---
> +# warn_thunk_thunk is in /sys/kernel/debug/kprobes/blacklist;
> +# register_kprobe() refuses it.
> +KP_REJECT=warn_thunk_thunk
> +if echo "engage $KP_REJECT 0" > $KS/control 2>/dev/null; then
> +	ksft_fail "register_kprobe should have rejected $KP_REJECT"
> +	echo "disengage $KP_REJECT" > $KS/control
> +else
> +	ksft_pass "register_kprobe refuses blacklisted target"
> +fi
> +
> +# --- disengage ---
> +echo "disengage ks_test_vuln" > $KS/control
> +[[ -z "$(cat $KS/engaged)" ]] \
> +	&& ksft_pass "disengage: engaged list empty" \
> +	|| ksft_fail "disengage: engaged list not empty"
> +
> +[[ ! -d $KS/fn/ks_test_vuln ]] \
> +	&& ksft_pass "disengage: per-fn dir removed" \
> +	|| ksft_fail "disengage: per-fn dir still present"
> +
> +[[ $(cat $KS/taint) == 1 ]] \
> +	&& ksft_pass "disengage: taint persists" \
> +	|| ksft_fail "disengage: taint should persist"
> +
> +# --- post-disengage: bad path active again ---
> +if echo 0xC0FFEE > $TRIG 2>/dev/null; then
> +	ksft_fail "post-disengage: write should fail again"
> +else
> +	ksft_pass "post-disengage: bad path active again"
> +fi
> +
> +# ---- CVE-2026-31431 mitigation proof (AF_ALG aead via af_alg_sendmsg) ----
> +# Skip the whole block if AF_ALG / AEAD machinery isn't compiled in.
> +if [[ -x $(dirname "$0")/cve_31431_test ]]; then
> +	CVE=$(dirname "$0")/cve_31431_test
> +	$CVE >/dev/null 2>&1 && PRE=$? || PRE=$?
> +	if [[ $PRE -eq 0 ]]; then
> +		ksft_pass "cve-31431: pre-engage AEAD round-trip OK"
> +
> +		echo "engage af_alg_sendmsg -1" > $KS/control
> +		$CVE >/dev/null 2>&1 && POST=$? || POST=$?
> +		if [[ $POST -eq 1 ]]; then
> +			ksft_pass "cve-31431: post-engage AEAD refused (mitigated)"
> +		else
> +			ksft_fail "cve-31431: post-engage exit=$POST (expected 1)"
> +		fi
> +
> +		HITS=$(cat $KS/fn/af_alg_sendmsg/hits 2>/dev/null || echo 0)
> +		[[ $HITS -ge 1 ]] && ksft_pass "cve-31431: hits=$HITS recorded" \
> +			|| ksft_fail "cve-31431: hits not recorded"
> +
> +		echo "disengage af_alg_sendmsg" > $KS/control
> +		$CVE >/dev/null 2>&1 && POST2=$? || POST2=$?
> +		[[ $POST2 -eq 0 ]] && ksft_pass "cve-31431: post-disengage restored" \
> +			|| ksft_fail "cve-31431: post-disengage exit=$POST2"
> +	elif [[ $PRE -eq 2 ]]; then
> +		echo "# SKIP cve-31431 (AF_ALG/AEAD not available)"
> +	else
> +		ksft_fail "cve-31431: pre-engage exit=$PRE"
> +	fi
> +fi
> +
> +# ---- CVE-2026-43284 mitigation proof (IPsec ESP via esp_input) ----
> +# Engaging esp_input causes inbound ESP packets to be dropped before
> +# decapsulation, neutering any bug downstream of the ESP receive path.
> +# Two netns + veth so traffic actually traverses xfrm (single-netns
> +# 127.0.0.0/8 traffic short-circuits before xfrm policy lookup).
> +NS0=ks-esp-0
> +NS1=ks-esp-1
> +esp_setup_ok=0
> +esp_cleanup() {
> +	[[ $esp_setup_ok -eq 1 ]] || return 0
> +	ip netns del $NS0 2>/dev/null
> +	ip netns del $NS1 2>/dev/null
> +}
> +trap 'cleanup; esp_cleanup' EXIT
> +
> +# UDP probe in python3 (always present on Debian/Fedora minimal installs).
> +esp_round_trip() {
> +	# $1: source netns, $2: dest netns, $3: dest ip, $4: port
> +	local tmp rpid rc
> +	tmp=$(mktemp)
> +	ip netns exec "$2" python3 -c '
> +import socket
> +r = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
> +r.bind(("0.0.0.0", '"$4"'))
> +r.settimeout(2.0)
> +try:
> +    d,_ = r.recvfrom(64)
> +    print(d.decode(errors="replace"))
> +except socket.timeout:
> +    print("timeout")
> +' > "$tmp" 2>&1 &
> +	rpid=$!
> +	sleep 0.3
> +	ip netns exec "$1" python3 -c '
> +import socket
> +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
> +s.sendto(b"ks-esp-probe", ("'"$3"'", '"$4"'))
> +' 2>/dev/null
> +	wait $rpid 2>/dev/null
> +	rc=1
> +	grep -q "ks-esp-probe" "$tmp" && rc=0
> +	rm -f "$tmp"
> +	return $rc
> +}
> +
> +if command -v ip >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then
> +	KEY=0x0123456789abcdef0123456789abcdef01234567
> +
> +	if ip netns add $NS0 2>/dev/null && \
> +	   ip netns add $NS1 2>/dev/null && \
> +	   ip link add veth0 type veth peer name veth1 2>/dev/null && \
> +	   ip link set veth0 netns $NS0 2>/dev/null && \
> +	   ip link set veth1 netns $NS1 2>/dev/null && \
> +	   ip -n $NS0 addr add 10.99.0.1/24 dev veth0 2>/dev/null && \
> +	   ip -n $NS1 addr add 10.99.0.2/24 dev veth1 2>/dev/null && \
> +	   ip -n $NS0 link set veth0 up 2>/dev/null && \
> +	   ip -n $NS1 link set veth1 up 2>/dev/null && \
> +	   ip -n $NS0 link set lo up 2>/dev/null && \
> +	   ip -n $NS1 link set lo up 2>/dev/null && \
> +	   ip -n $NS0 xfrm state add src 10.99.0.1 dst 10.99.0.2 proto esp \
> +		spi 0x1000 mode transport reqid 0x100 \
> +		aead 'rfc4106(gcm(aes))' $KEY 128 2>/dev/null && \
> +	   ip -n $NS0 xfrm state add src 10.99.0.2 dst 10.99.0.1 proto esp \
> +		spi 0x1001 mode transport reqid 0x100 \
> +		aead 'rfc4106(gcm(aes))' $KEY 128 2>/dev/null && \
> +	   ip -n $NS1 xfrm state add src 10.99.0.1 dst 10.99.0.2 proto esp \
> +		spi 0x1000 mode transport reqid 0x100 \
> +		aead 'rfc4106(gcm(aes))' $KEY 128 2>/dev/null && \
> +	   ip -n $NS1 xfrm state add src 10.99.0.2 dst 10.99.0.1 proto esp \
> +		spi 0x1001 mode transport reqid 0x100 \
> +		aead 'rfc4106(gcm(aes))' $KEY 128 2>/dev/null && \
> +	   ip -n $NS0 xfrm policy add src 10.99.0.1 dst 10.99.0.2 \
> +		dir out tmpl src 10.99.0.1 dst 10.99.0.2 proto esp \
> +		reqid 0x100 mode transport 2>/dev/null && \
> +	   ip -n $NS1 xfrm policy add src 10.99.0.1 dst 10.99.0.2 \
> +		dir in tmpl src 10.99.0.1 dst 10.99.0.2 proto esp \
> +		reqid 0x100 mode transport 2>/dev/null; then
> +		esp_setup_ok=1
> +	fi
> +
> +	if [[ $esp_setup_ok -eq 1 ]] \
> +	   && esp_round_trip $NS0 $NS1 10.99.0.2 53435; then
> +		ksft_pass "cve-43284: pre-engage ESP round-trip OK"
> +
> +		echo "engage esp_input -22" > $KS/control
> +		if esp_round_trip $NS0 $NS1 10.99.0.2 53435; then
> +			ksft_fail "cve-43284: post-engage ESP should have been dropped"
> +		else
> +			ksft_pass "cve-43284: post-engage ESP refused (mitigated)"
> +		fi
> +
> +		ESP_HITS=$(cat $KS/fn/esp_input/hits 2>/dev/null || echo 0)
> +		[[ $ESP_HITS -ge 1 ]] \
> +			&& ksft_pass "cve-43284: hits=$ESP_HITS recorded" \
> +			|| ksft_fail "cve-43284: hits not recorded"
> +
> +		echo "disengage esp_input" > $KS/control
> +		if esp_round_trip $NS0 $NS1 10.99.0.2 53435; then
> +			ksft_pass "cve-43284: post-disengage restored"
> +		else
> +			ksft_fail "cve-43284: post-disengage ESP still dropped"
> +		fi
> +	else
> +		echo "# SKIP cve-43284 (netns/veth/XFRM/ESP setup failed)"
> +	fi
> +fi
> +
> +echo "1..$N"
> +exit $((FAIL > 0))

^ permalink raw reply

* Re: [PATCH v11 3/6] iio: adc: ad4691: add triggered buffer support
From: David Lechner @ 2026-05-17 19:21 UTC (permalink / raw)
  To: Jonathan Cameron
  Cc: radu.sabau, Lars-Peter Clausen, Michael Hennerich, Nuno Sá,
	Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Uwe Kleine-König, Liam Girdwood, Mark Brown, Linus Walleij,
	Bartosz Golaszewski, Philipp Zabel, Jonathan Corbet, Shuah Khan,
	linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
	linux-doc
In-Reply-To: <20260517132526.27c71b70@jic23-huawei>

On 5/17/26 7:25 AM, Jonathan Cameron wrote:
> On Sat, 16 May 2026 12:32:51 -0500
> David Lechner <dlechner@baylibre.com> wrote:
> 
>> On 5/15/26 8:31 AM, Radu Sabau via B4 Relay wrote:
>>> From: Radu Sabau <radu.sabau@analog.com>
>>>
>>> Add buffered capture support using the IIO triggered buffer framework.
>>>
>>> CNV Burst Mode: the GP pin identified by interrupt-names in the device
>>> tree is configured as DATA_READY output. The IRQ handler stops
>>> conversions and fires the IIO trigger; the trigger handler executes a
>>> pre-built SPI message that reads all active channels from the AVG_IN
>>> accumulator registers and then resets accumulator state and restarts
>>> conversions for the next cycle.
>>>
>>> Manual Mode: CNV is tied to SPI CS so each transfer simultaneously
>>> reads the previous result and starts the next conversion (pipelined
>>> N+1 scheme). At preenable time a pre-built, optimised SPI message of
>>> N+1 transfers is constructed (N channel reads plus one NOOP to drain
>>> the pipeline). The trigger handler executes the message in a single
>>> spi_sync() call and collects the results. An external trigger (e.g.
>>> iio-trig-hrtimer) is required to drive the trigger at the desired
>>> sample rate.
>>>
>>> Both modes share the same trigger handler and push a complete scan —
>>> one big-endian 16-bit (__be16) slot per active channel, densely packed
>>> in scan_index order, followed by a timestamp.
>>>
>>> The CNV Burst Mode sampling frequency (PWM period) is exposed as a
>>> buffer-level attribute via IIO_DEVICE_ATTR.
>>>
>>> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
> 
>>> +
>>> +static int ad4691_manual_buffer_preenable(struct iio_dev *indio_dev)
>>> +{
>>> +	struct ad4691_state *st = iio_priv(indio_dev);
>>> +	unsigned int k, i;
>>> +	int ret;
>>> +
>>> +	memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
>>> +	memset(st->scan_tx, 0, sizeof(st->scan_tx));
>>> +
>>> +	spi_message_init(&st->scan_msg);
>>> +
>>> +	k = 0;
>>> +	iio_for_each_active_channel(indio_dev, i) {
>>> +		if (i >= indio_dev->num_channels - 1)
>>> +			break; /* skip soft timestamp */  
>>
>> I don't think timestamp gets set in the scan mask. It is handled separately.
> 
> FWIW that is a sashiko false postive (I believe anyway!)
> If we do hit this please shout as we have a core bug.
> 
> If anyone has time to look at how hard it would be to tweak
> iio_for_each_active_channel to skip a last element timestamp that
> would be great.
> 
> I think that iterates one too far which is what sashiko is tripping over.
> 
> I'm only keen to fix that if we can make it low cost and hid it entirely
> from drivers.
> 
> Jonathan
> 
This is what I came up with (totally untested).

Since timestamp can never be set in scan_mask/active_scan_mask, it should
be safe to exclude it from masklength without breaking existing code.

I didn't check all callers of masklength/iio_get_masklength() though.

---
diff --git a/drivers/iio/industrialio-buffer.c b/drivers/iio/industrialio-buffer.c
index 9d66510a1d49..17f539fc23e2 100644
--- a/drivers/iio/industrialio-buffer.c
+++ b/drivers/iio/industrialio-buffer.c
@@ -2300,8 +2300,10 @@ int iio_buffers_alloc_sysfs_and_mask(struct iio_dev *indio_dev)
 	if (channels) {
 		int ml = 0;
 
-		for (i = 0; i < indio_dev->num_channels; i++)
-			ml = max(ml, channels[i].scan_index + 1);
+		for (i = 0; i < indio_dev->num_channels; i++) {
+			if (channels[i].type != IIO_TIMESTAMP)
+				ml = max(ml, channels[i].scan_index + 1);
+		}
 		ACCESS_PRIVATE(indio_dev, masklength) = ml;
 	}
 



^ permalink raw reply related

* Re: [PATCH v4 05/16] vfio: Enforce preserved devices are retrieved via LIVEUPDATE_SESSION_RETRIEVE_FD
From: Zhu Yanjun @ 2026-05-17 19:04 UTC (permalink / raw)
  To: Vipin Sharma, kvm, linux-doc, linux-kernel, linux-kselftest,
	linux-pci, yanjun.zhu@linux.dev
  Cc: ajayachandra, alex, amastro, ankita, apopple, chrisl, corbet,
	dmatlack, graf, jacob.pan, jgg, jgg, jrhilke, julianr, kevin.tian,
	leon, leonro, lukas, michal.winiarski, parav, pasha.tatashin,
	praan, pratyush, rananta, rientjes, rodrigo.vivi, rppt, saeedm,
	skhan, skhawaja, vivek.kasireddy, witu, yi.l.liu
In-Reply-To: <20260511234802.2280368-6-vipinsh@google.com>


在 2026/5/11 16:47, Vipin Sharma 写道:
> From: David Matlack <dmatlack@google.com>
>
> Enforce that files for incoming (preserved by previous kernel) VFIO
> devices are retrieved via LIVEUPDATE_SESSION_RETRIEVE_FD rather than by
> opening the corresponding VFIO character device or via
> VFIO_GROUP_GET_DEVICE_FD.
>
> Both of these methods would result in VFIO initializing the device
> without access to the preserved state of the device passed by the
> previous kernel.
>
> Reviewed-by: Pranjal Shrivastava <praan@google.com>
> Signed-off-by: David Matlack <dmatlack@google.com>
> Co-developed-by: Vipin Sharma <vipinsh@google.com>
> Signed-off-by: Vipin Sharma <vipinsh@google.com>
> ---
>   drivers/vfio/device_cdev.c             |  8 ++++++++
>   drivers/vfio/group.c                   |  9 +++++++++
>   drivers/vfio/pci/vfio_pci_liveupdate.c |  6 ++++++
>   drivers/vfio/vfio.h                    | 18 ++++++++++++++++++
>   4 files changed, 41 insertions(+)
>
> diff --git a/drivers/vfio/device_cdev.c b/drivers/vfio/device_cdev.c
> index 1ab07ccaf3ab..4df0495941c6 100644
> --- a/drivers/vfio/device_cdev.c
> +++ b/drivers/vfio/device_cdev.c
> @@ -49,6 +49,14 @@ static int vfio_device_cdev_open(struct vfio_device *device, struct file **filep
>   		}
>   
>   		*filep = file;
> +	} else if (vfio_liveupdate_incoming_is_preserved(device)) {
> +		/*
> +		 * Since it is live update preserved device, it must be
> +		 * retrieved via LIVEUPDATE_SESSION_RETRIEVE_FD instead of
> +		 * opening /dev/vfio/devices/vfioX.
> +		 */
> +		ret = -EBUSY;
> +		goto err_free_device_file;

When vfio_liveupdate_incoming_is_preserved(device) returns true, 
vfio_device_put_registration(device) is not called in this path.

Is vfio_device_put_registration(device) instead invoked from the 
err_free_device_file error handling path?

Zhu Yanjun

>   	}
>   
>   	file->private_data = df;
> diff --git a/drivers/vfio/group.c b/drivers/vfio/group.c
> index b2299e5bc6df..62b4eaabc829 100644
> --- a/drivers/vfio/group.c
> +++ b/drivers/vfio/group.c
> @@ -316,6 +316,15 @@ static int vfio_group_ioctl_get_device_fd(struct vfio_group *group,
>   	if (IS_ERR(device))
>   		return PTR_ERR(device);
>   
> +	/*
> +	 * This device was preserved across a Live Update. Accessing it via
> +	 * VFIO_GROUP_GET_DEVICE_FD is not allowed.
> +	 */
> +	if (vfio_liveupdate_incoming_is_preserved(device)) {
> +		vfio_device_put_registration(device);
> +		return -EBUSY;
> +	}
> +
>   	fd = FD_ADD(O_CLOEXEC, vfio_device_open_file(device));
>   	if (fd < 0)
>   		vfio_device_put_registration(device);
> diff --git a/drivers/vfio/pci/vfio_pci_liveupdate.c b/drivers/vfio/pci/vfio_pci_liveupdate.c
> index 11c3bc8a8dcd..731a3e34085f 100644
> --- a/drivers/vfio/pci/vfio_pci_liveupdate.c
> +++ b/drivers/vfio/pci/vfio_pci_liveupdate.c
> @@ -47,6 +47,12 @@
>    *   ...
>    *   ioctl(session_fd, LIVEUPDATE_SESSION_FINISH, ...);
>    *
> + * .. note::
> + *    After kexec, if a device was preserved by the previous kernel, attempting
> + *    to open a new file for the device via its character device
> + *    (``/dev/vfio/devices/X``) or via ``VFIO_GROUP_GET_DEVICE_FD`` will fail
> + *    with ``-EBUSY``.
> + *
>    * Restrictions
>    * ============
>    *
> diff --git a/drivers/vfio/vfio.h b/drivers/vfio/vfio.h
> index 0854f3fa1a22..5269fe021ee3 100644
> --- a/drivers/vfio/vfio.h
> +++ b/drivers/vfio/vfio.h
> @@ -11,6 +11,7 @@
>   #include <linux/cdev.h>
>   #include <linux/module.h>
>   #include <linux/vfio.h>
> +#include <linux/pci.h>
>   
>   struct iommufd_ctx;
>   struct iommu_group;
> @@ -461,4 +462,21 @@ static inline void vfio_device_debugfs_init(struct vfio_device *vdev) { }
>   static inline void vfio_device_debugfs_exit(struct vfio_device *vdev) { }
>   #endif /* CONFIG_VFIO_DEBUGFS */
>   
> +#ifdef CONFIG_PCI_LIVEUPDATE
> +static inline bool vfio_liveupdate_incoming_is_preserved(struct vfio_device *device)
> +{
> +	struct device *d = device->dev;
> +
> +	if (dev_is_pci(d))
> +		return to_pci_dev(d)->liveupdate_incoming;
> +
> +	return false;
> +}
> +#else
> +static inline bool vfio_liveupdate_incoming_is_preserved(struct vfio_device *device)
> +{
> +	return false;
> +}
> +#endif /* CONFIG_PCI_LIVEUPDATE */
> +
>   #endif

-- 
Best Regards,
Yanjun.Zhu


^ permalink raw reply

* Re: [PATCH v2 05/10] liveupdate: defer session block allocation and PA setting
From: Pasha Tatashin @ 2026-05-17 18:52 UTC (permalink / raw)
  To: Mike Rapoport
  Cc: Pasha Tatashin, linux-kselftest, shuah, akpm, linux-mm, skhan,
	linux-doc, linux-kernel, corbet, dmatlack, kexec, pratyush,
	skhawaja, graf
In-Reply-To: <agn7XnuO7VU4eK52@kernel.org>

On 05-17 20:31, Mike Rapoport wrote:
> On Thu, May 14, 2026 at 10:26:23PM +0000, Pasha Tatashin wrote:
> > Currently, luo_session_setup_outgoing() allocates the session block and
> > sets its physical address in the header immediately. With upcoming
> > dynamic block-based session management, this makes the first block
> > different from the rest. Move the allocation to where it is first needed.
> > 
> > Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
> 
> Acked-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
> 
> > ---
> > @@ -77,15 +77,16 @@
> >  
> >  /**
> >   * struct luo_session_header - Header struct for managing LUO sessions.
> > - * @count:      The number of sessions currently tracked in the @list.
> > - * @list:       The head of the linked list of `struct luo_session` instances.
> > - * @rwsem:      A read-write semaphore providing synchronized access to the
> > - *              session list and other fields in this structure.
> > - * @header_ser: The header data of serialization array.
> > - * @ser:        The serialized session data (an array of
> > - *              `struct luo_session_ser`).
> > - * @active:     Set to true when first initialized. If previous kernel did not
> > - *              send session data, active stays false for incoming.
> > + * @count:       The number of sessions currently tracked in the @list.
> > + * @list:        The head of the linked list of `struct luo_session` instances.
> > + * @rwsem:       A read-write semaphore providing synchronized access to the
> > + *               session list and other fields in this structure.
> > + * @header_ser:  The header data of serialization array.
> > + * @ser:         The serialized session data (an array of
> > + *               `struct luo_session_ser`).
> > + * @sessions_pa: Points to the location of sessions_pa within struct luo_ser.
> > + * @active:      Set to true when first initialized. If previous kernel did not
> > + *               send session data, active stays false for incoming.
> 
> Hmm, why addition of a single field changed the entire block? :/

Yes, I had to decide whether to shorten the field name or change the 
entire block. I opted for the latter. :-)

> 
> >   */
> >  struct luo_session_header {
> >  	long count;
> 
> -- 
> Sincerely yours,
> Mike.

^ permalink raw reply

* Re: [RFC PATCH 3/5] mm/damon/core: floor effective quota size at minimum region size
From: SeongJae Park @ 2026-05-17 18:47 UTC (permalink / raw)
  To: Ravi Jonnalagadda
  Cc: SeongJae Park, damon, linux-mm, linux-kernel, linux-doc, akpm,
	corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun
In-Reply-To: <20260516210357.2247-4-ravis.opensrc@gmail.com>

On Sat, 16 May 2026 14:03:55 -0700 Ravi Jonnalagadda <ravis.opensrc@gmail.com> wrote:

> The CONSIST quota goal tuner initializes esz_bp to 0, producing an
> effective quota size (esz) of 1 byte on the first tick.
> damos_quota_is_full() rejects all regions when esz < min_region_sz
> (default PAGE_SIZE = 4096), so no regions can be tried and no
> feedback reaches the tuner — a bootstrapping deadlock.

That depend on whether the goal is already [over]-achieved.  If the goal is
achieved, the tuner will think no change is needed, so keep the
effectively-zero quota.  If the goal is over-achived, the tuner will think the
DAMOS scheme should be less aggressive, but it is already effectively-zero
quota, so keep having effectively-zero quota.

If the ogal is under-achived, the logic will iteratively increase the internal
esz (esz_bp), until it exceeds the min_region_sz, and finally start making some
effects.

So, unless the goal is already [over]-achieved, there is no deadlock.  If the
goal is already [over]-achieved, why we would want to make DAMOS do something?

Am I missing something?

I'd like to discuss this high level thing first, before digging deep into the
details.


Thanks,
SJ

[...]

^ permalink raw reply

* Re: [PATCH v2 04/10] liveupdate: add support for linked-block serialization
From: Pasha Tatashin @ 2026-05-17 18:40 UTC (permalink / raw)
  To: Mike Rapoport
  Cc: Pasha Tatashin, linux-kselftest, shuah, akpm, linux-mm, skhan,
	linux-doc, linux-kernel, corbet, dmatlack, kexec, pratyush,
	skhawaja, graf
In-Reply-To: <agn6QeIamoeMkesv@kernel.org>

On 05-17 20:26, Mike Rapoport wrote:
> On Thu, May 14, 2026 at 10:26:22PM +0000, Pasha Tatashin wrote:
> > Introduce a linked-block serialization mechanism for LUO state.
> > 
> > Previously, LUO used contiguous memory blocks for serializing sessions
> > and files, which imposed limits on the total number of items that could
> > be preserved across a live update.
> > 
> > This commit adds the infrastructure for a more flexible, block-based
> > approach where serialized data is stored in a chain of linked blocks.
> > This is a preparatory step to allow an unlimited number of
> > luo_sessions and luo_files to be preserved.
> 
> Shouldn't it be a part of KHO?

It can be. However, at the moment, it is only used by LUO, so I kept it 
there. Sami is also planning to use it for IOMMU; if so, he will adopt 
the interface for his usage as well, and move it to a separate KHO 
library.

>  
> > Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
> > ---
> >  Documentation/core-api/liveupdate.rst |   8 +
> >  include/linux/kho/abi/luo.h           |  22 ++
> >  kernel/liveupdate/Makefile            |   1 +
> >  kernel/liveupdate/luo_block.c         | 388 ++++++++++++++++++++++++++
> >  kernel/liveupdate/luo_internal.h      |  57 ++++
> >  5 files changed, 476 insertions(+)
> >  create mode 100644 kernel/liveupdate/luo_block.c
> 
> -- 
> Sincerely yours,
> Mike.

^ permalink raw reply

* [PATCH v5 13/13] docs: iio: add documentation for ad9910 driver
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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 | 666 +++++++++++++++++++++++++++++++++++++++++++
 Documentation/iio/index.rst  |   1 +
 MAINTAINERS                  |   1 +
 3 files changed, 668 insertions(+)

diff --git a/Documentation/iio/ad9910.rst b/Documentation/iio/ad9910.rst
new file mode 100644
index 000000000000..dbcf8f8a1dda
--- /dev/null
+++ b/Documentation/iio/ad9910.rst
@@ -0,0 +1,666 @@
+.. 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. 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 have an ``enable`` attribute that turns the mode on/off.
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Channel
+     - Label
+     - Parent
+     - Description
+
+   * - ``out_altvoltage100``
+     - ``phy``
+     -
+     - Physical output: system clock and profile control.
+       See `Physical channel`_.
+
+   * - ``out_altvoltage110`` ... ``out_altvoltage117``
+     - ``profile0`` ... ``profile7``
+     - ``phy``
+     - Single tone control: frequency, phase, amplitude.
+       See `Single Tone mode`_.
+
+   * - ``out_altvoltage120``
+     - ``parallel_port``
+     - ``phy``
+     - Parallel port modulation channel.
+       See `Parallel Port mode`_.
+
+   * - ``out_altvoltage130``
+     - ``digital_ramp_generator``
+     - ``phy``
+     - Digital ramp generator (DRG) control: enable.
+       See `Digital ramp generator (DRG)`_.
+
+   * - ``out_altvoltage131``
+     - ``digital_ramp_up``
+     - ``digital_ramp_generator``
+     - DRG ramp-up parameters: dwell enable, limits, rate of change, ramp rate.
+
+   * - ``out_altvoltage132``
+     - ``digital_ramp_down``
+     - ``digital_ramp_generator``
+     - DRG ramp-down parameters: dwell enable, limits, rate of change, ramp rate.
+
+   * - ``out_altvoltage140``
+     - ``ram_control``
+     - ``phy``
+     - RAM playback: enable, frequency, phase and sampling frequency for active
+       profile. See `RAM mode`_.
+
+   * - ``out_altvoltage150``
+     - ``output_shift_keying``
+     - ``phy``
+     - Output shift keying (OSK): enable, amplitude scale, ramp rate,
+       rate of change control. See `Output Shift Keying (OSK)`_.
+
+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 Frequency Tuning Word, FTW (frequency_offset)
+     - Parallel data port enabled and data destination is frequency
+
+   * -
+     - FTW register (frequency)
+     - RAM enabled and data destination is not frequency
+
+   * - Lowest Priority
+     - FTW (frequency) in single tone channel for the active profile
+     - All other cases
+
+.. flat-table:: DDS Phase Control
+   :header-rows: 1
+
+   * - Priority
+     - Data Source
+     - Conditions
+
+   * - Highest Priority
+     - RAM
+     - RAM enabled and data destination is phase or polar
+
+   * -
+     - DRG
+     - DRG enabled and data destination is phase
+
+   * -
+     - Parallel data port
+     - Parallel data port enabled and data destination is phase
+
+   * -
+     - Parallel data port and Phase Offset Word, POW register LSBs (phase_offset)
+     - Parallel data port enabled and data destination is polar
+
+   * -
+     - POW register (phase)
+     - RAM enabled and destination is not phase nor polar
+
+   * - Lowest Priority
+     - POW (phase) in single tone channel for the active profile
+     - All other cases
+
+.. flat-table:: DDS Amplitude Control
+   :header-rows: 1
+
+   * - Priority
+     - Data Source
+     - Conditions
+
+   * - Highest Priority
+     - Amplitude Scale Factor, ASF register and OSK generator
+     - OSK enabled
+
+   * -
+     - RAM
+     - RAM enabled and data destination is amplitude or polar
+
+   * -
+     - DRG
+     - DRG enabled and data destination is amplitude
+
+   * -
+     - Parallel data port
+     - Parallel data port enabled and data destination is amplitude
+
+   * -
+     - Parallel data port and ASF register LSBs (scale_offset)
+     - Parallel data port enabled and data destination is polar
+
+   * - Lowest Priority
+     - ASF (scale) in single tone channel for the active profile
+     - (Amplitude scale is already enabled by default)
+
+While debugging or testing, the debug attributes ``frequency_source``,
+``phase_source`` and ``amplitude_source`` can be used to read the label of
+the channel that is actively controlling the correspondent DDS parameter,
+which reflects the priority list described above.
+
+Single Tone mode
+----------------
+
+Single tone is the baseline operating mode. The ``profileY`` channels
+provide enable, frequency, phase and amplitude control:
+
+.. flat-table::
+   :header-rows: 1
+
+   * - Attribute
+     - Unit
+     - Description
+
+   * - ``en``
+     - boolean (0 or 1)
+     - Enable/disable profile Y. Only one profile can be active at a
+       time. 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_altvoltage112_frequency
+  echo 0.5 > /sys/bus/iio/devices/iio:device0/out_altvoltage112_scale
+  echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage112_phase
+
+  # Activate profile 2
+  echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage112_en
+
+Read back the current single tone frequency:
+
+.. code-block:: bash
+
+  cat /sys/bus/iio/devices/iio:device0/out_altvoltage112_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
+     - Frequency modulation (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_altvoltage120_frequency_scale
+  echo 50000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage120_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``).
+
+The DRG can target only one destination at a time (frequency, phase or
+amplitude). Destination selection follows a "last write wins" policy: writing
+any value (including zero) to a destination-specific attribute (e.g.
+``frequency``, ``frequency_roc``, ``phase``, ``phase_roc``, ``scale`` or
+``scale_roc``) switches the DRG destination accordingly. Reading an attribute
+whose destination is not currently active returns ``-EBUSY``.
+
+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_altvoltage131_dwell_en
+  echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage132_dwell_en
+
+  # Set ramp limits
+  echo 60000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage131_frequency
+  echo 40000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage132_frequency
+
+  # Set ramp rate
+  echo 25000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage131_sampling_frequency
+  echo 25000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage132_sampling_frequency
+
+  # Set frequency rate of change (Hz/s)
+  echo 25000000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage131_frequency_roc
+  echo 25000000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage132_frequency_roc
+
+  # Enable the DRG
+  echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage130_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 firmware data
+follows a binary format (version 1) with an 80-byte header followed by data
+words. All fields are big-endian.
+
+.. flat-table:: RAM firmware header (80 bytes)
+   :header-rows: 1
+
+   * - Offset
+     - Size
+     - Field
+     - Description
+
+   * - 0
+     - 4
+     - ``magic``
+     - Magic number: ``0x00AD9910``
+
+   * - 4
+     - 2
+     - ``version``
+     - Format version: ``0x0001``
+
+   * - 6
+     - 2
+     - ``wcount``
+     - Number of 32-bit RAM data words (0--1024)
+
+   * - 8
+     - 4
+     - ``crc``
+     - CRC32 checksum over ``cfr1``, ``profiles`` and ``words``
+
+   * - 12
+     - 4
+     - ``cfr1``
+     - CFR1 register value. Only RAM-relevant bits are used:
+       bits [30:29] set data destination (00: frequency, 01: phase,
+       10: amplitude, 11: polar); bits [20:17] set internal profile
+       control (see datasheet Table 14)
+
+   * - 16
+     - 64
+     - ``profiles[0..7]``
+     - 8 sets of 8-byte RAM profile configurations (see below)
+
+   * - 80
+     - 4 x wcount
+     - ``words[]``
+     - RAM data words in reverse order
+
+Each 8-byte profile entry contains:
+
+.. flat-table:: RAM profile entry (8 bytes)
+   :header-rows: 1
+
+   * - Bits
+     - Field
+     - Description
+
+   * - [55:40]
+     - Address step rate
+     - Controls playback speed for this profile
+
+   * - [39:30]
+     - End address
+     - Last RAM address for this profile
+
+   * - [23:14]
+     - Start address
+     - First RAM address for this profile
+
+   * - [5]
+     - No-dwell high
+     - No-dwell at high limit (ramp-up mode)
+
+   * - [3]
+     - Zero-crossing
+     - Zero-crossing enable (direct-switch mode)
+
+   * - [2:0]
+     - Operating mode
+     - 000: direct switch, 001: ramp-up, 010: bidirectional,
+       011: bidirectional continuous, 100: ramp-up continuous
+
+Usage examples
+^^^^^^^^^^^^^^
+
+Configure RAM mode with firmware data and enable it:
+
+.. code-block:: bash
+
+  # Load RAM data via firmware upload
+  echo 1 > /sys/class/firmware/iio\:device0\:ram/loading
+  cat ad9910-ram.bin > /sys/class/firmware/iio\:device0\:ram/data
+  echo 0 > /sys/class/firmware/iio\:device0\:ram/loading
+
+  # Enable RAM mode
+  echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage140_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)`.
+
+   * - ``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 ASF 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.
+
+   * - ``sampling_frequency``
+     - Hz
+     - OSK ramp rate. It is controlled by an integer divider so the requested
+       value will adjust to nearest supported value.
+
+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. When the internal PLL is enabled
+       (via the ``adi,pll-enable`` devicetree property), configures the PLL
+       multiplier (range 420--1000 MHz). Without PLL, the reference 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 c39affe4157a..363b7af827c3 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

* [PATCH v5 11/13] iio: frequency: ad9910: show channel priority in debugfs
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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

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

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

diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index 91f28c7de68b..8c8e73f340f8 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -22,6 +22,7 @@
 #include <linux/property.h>
 #include <linux/regulator/consumer.h>
 #include <linux/reset.h>
+#include <linux/seq_file.h>
 #include <linux/spi/spi.h>
 #include <linux/sysfs.h>
 #include <linux/types.h>
@@ -2116,6 +2117,146 @@ 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 ad9910_state *st)
+{
+	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_CFR2].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 seq_file *s, void *ignored)
+{
+	seq_printf(s, "%s\n", ad9910_frequency_source_get(s->private));
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(ad9910_frequency_source);
+
+static inline const char *ad9910_phase_source_get(struct ad9910_state *st)
+{
+	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_CFR2].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 seq_file *s, void *ignored)
+{
+	seq_printf(s, "%s\n", ad9910_phase_source_get(s->private));
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(ad9910_phase_source);
+
+static inline const char *ad9910_amplitude_source_get(struct ad9910_state *st)
+{
+	bool ram_en, mode_en;
+	u32 destination;
+
+	guard(mutex)(&st->lock);
+
+	/* OSK enabled */
+	mode_en = FIELD_GET(AD9910_CFR1_OSK_ENABLE_MSK,
+			    st->reg[AD9910_REG_CFR1].val32);
+	if (mode_en)
+		return ad9910_channel_str[AD9910_CHAN_IDX_OSK];
+
+	/* RAM enabled and data destination is amplitude or polar */
+	ram_en = AD9910_RAM_ENABLED(st);
+	if (ram_en) {
+		destination = FIELD_GET(AD9910_CFR1_RAM_PLAYBACK_DEST_MSK,
+					st->reg[AD9910_REG_CFR1].val32);
+		if (destination == AD9910_DEST_AMPLITUDE ||
+		    destination == AD9910_DEST_POLAR)
+			return ad9910_channel_str[AD9910_CHAN_IDX_RAM];
+	}
+
+	/* DRG enabled and data destination is amplitude */
+	mode_en = FIELD_GET(AD9910_CFR2_DRG_ENABLE_MSK,
+			    st->reg[AD9910_REG_CFR2].val32);
+	if (mode_en && AD9910_DEST_AMPLITUDE ==
+		       FIELD_GET(AD9910_CFR2_DRG_DEST_MSK,
+				 st->reg[AD9910_REG_CFR2].val32))
+		return ad9910_channel_str[AD9910_CHAN_IDX_DRG];
+
+	/* Parallel data port enabled and data destination is amplitude */
+	mode_en = FIELD_GET(AD9910_CFR2_PARALLEL_DATA_PORT_EN_MSK,
+			    st->reg[AD9910_REG_CFR2].val32);
+	if (mode_en) /* 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 seq_file *s, void *ignored)
+{
+	seq_printf(s, "%s\n", ad9910_amplitude_source_get(s->private));
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(ad9910_amplitude_source);
+
 static inline void ad9910_debugfs_init(struct ad9910_state *st,
 				       struct iio_dev *indio_dev)
 {
@@ -2131,6 +2272,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, st,
+			    &ad9910_frequency_source_fops);
+	debugfs_create_file("phase_source", 0400, d, st,
+			    &ad9910_phase_source_fops);
+	debugfs_create_file("amplitude_source", 0400, d, st,
+			    &ad9910_amplitude_source_fops);
 }
 
 static int ad9910_probe(struct spi_device *spi)

-- 
2.43.0



^ permalink raw reply related

* [PATCH v5 12/13] Documentation: ABI: testing: add docs for ad9910 sysfs entries
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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     | 76 ++++++++++++++++++++++
 MAINTAINERS                                        |  1 +
 2 files changed, 77 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..934e6e8f0595
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
@@ -0,0 +1,76 @@
+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 derived from this offset combined with the processed buffer sample
+		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
+		derived from this offset combined with the processed buffer sample
+		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 derived from this offset combined with the processed
+		buffer sample 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 b2b7f54f5a24..c39affe4157a 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

* [PATCH v5 10/13] iio: frequency: ad9910: add output shift keying support
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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 through 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 (max), manual mode (pin controlled)

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

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

diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index 86ed350011cf..91f28c7de68b 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -235,6 +235,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,
@@ -251,6 +252,7 @@ enum ad9910_channel {
 	AD9910_CHANNEL_DRG_RAMP_UP = 131,
 	AD9910_CHANNEL_DRG_RAMP_DOWN = 132,
 	AD9910_CHANNEL_RAM = 140,
+	AD9910_CHANNEL_OSK = 150,
 };
 
 /**
@@ -306,6 +308,7 @@ enum {
 	AD9910_CHAN_IDX_DRG_RAMP_UP,
 	AD9910_CHAN_IDX_DRG_RAMP_DOWN,
 	AD9910_CHAN_IDX_RAM,
+	AD9910_CHAN_IDX_OSK,
 };
 
 enum {
@@ -318,6 +321,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 {
@@ -977,6 +982,135 @@ 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);
+		nstep = min(nstep, NANO);
+		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, \
@@ -1015,6 +1149,23 @@ static const struct iio_chan_spec_ext_info ad9910_drg_ramp_ext_info[] = {
 	{ }
 };
 
+static const struct iio_chan_spec_ext_info ad9910_osk_ext_info[] = {
+	{
+		.name = "scale_roc",
+		.read = ad9910_osk_attrs_read,
+		.write = ad9910_osk_attrs_write,
+		.private = AD9910_OSK_AUTO_ROC,
+		.shared = IIO_SEPARATE,
+	},
+	{
+		.name = "scale_roc_available",
+		.read = ad9910_osk_attrs_read,
+		.private = AD9910_OSK_AUTO_ROC_AVAIL,
+		.shared = IIO_SEPARATE,
+	},
+	{ }
+};
+
 #define AD9910_PROFILE_CHAN(idx) {				\
 	.type = IIO_ALTVOLTAGE,					\
 	.indexed = 1,						\
@@ -1102,6 +1253,18 @@ static const struct iio_chan_spec ad9910_channels[] = {
 				      BIT(IIO_CHAN_INFO_SAMP_FREQ),
 		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
 	},
+	[AD9910_CHAN_IDX_OSK] = {
+		.type = IIO_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,
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
 };
 
 static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -1134,6 +1297,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 +1406,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 +1432,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;
 		}
@@ -1341,6 +1518,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;
 		}
@@ -1510,6 +1692,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;
 		}
@@ -1549,6 +1739,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;
 		}
@@ -1571,6 +1766,7 @@ static int ad9910_write_raw_get_fmt(struct iio_dev *indio_dev,
 		switch (chan->channel) {
 		case AD9910_CHANNEL_PROFILE_0 ... AD9910_CHANNEL_PROFILE_7:
 		case AD9910_CHANNEL_RAM:
+		case AD9910_CHANNEL_OSK:
 			return IIO_VAL_INT_PLUS_MICRO;
 		case AD9910_CHANNEL_DRG_RAMP_UP:
 		case AD9910_CHANNEL_DRG_RAMP_DOWN:
@@ -1648,6 +1844,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,
@@ -1896,6 +2093,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

* [PATCH v5 09/13] iio: frequency: ad9910: add RAM mode support
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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

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

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

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

diff --git a/drivers/iio/frequency/Kconfig b/drivers/iio/frequency/Kconfig
index 180e74f62d11..0b058dce2a6f 100644
--- a/drivers/iio/frequency/Kconfig
+++ b/drivers/iio/frequency/Kconfig
@@ -29,6 +29,9 @@ config AD9910
 	tristate "Analog Devices AD9910 Direct Digital Synthesizer"
 	depends on SPI
 	depends on GPIOLIB
+	select CRC32
+	select FW_LOADER
+	select FW_UPLOAD
 	help
 	  Say yes here to build support for Analog Devices AD9910
 	  1 GSPS, 14-Bit DDS with integrated DAC.
diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index 4ad80475139d..86ed350011cf 100644
--- a/drivers/iio/frequency/ad9910.c
+++ b/drivers/iio/frequency/ad9910.c
@@ -8,9 +8,12 @@
 #include <linux/array_size.h>
 #include <linux/bitfield.h>
 #include <linux/clk.h>
+#include <linux/crc32.h>
+#include <linux/debugfs.h>
 #include <linux/delay.h>
 #include <linux/device/devres.h>
 #include <linux/err.h>
+#include <linux/firmware.h>
 #include <linux/gpio/consumer.h>
 #include <linux/log2.h>
 #include <linux/math64.h>
@@ -147,6 +150,15 @@
 #define AD9910_PROFILE_ST_POW_MSK		GENMASK_ULL(47, 32)
 #define AD9910_PROFILE_ST_FTW_MSK		GENMASK_ULL(31, 0)
 
+/* Profile Register Format (RAM Mode) */
+#define AD9910_PROFILE_RAM_OPEN_MSK		GENMASK_ULL(61, 56)
+#define AD9910_PROFILE_RAM_STEP_RATE_MSK	GENMASK_ULL(55, 40)
+#define AD9910_PROFILE_RAM_END_ADDR_MSK		GENMASK_ULL(39, 30)
+#define AD9910_PROFILE_RAM_START_ADDR_MSK	GENMASK_ULL(23, 14)
+#define AD9910_PROFILE_RAM_NO_DWELL_HIGH_MSK	BIT_ULL(5)
+#define AD9910_PROFILE_RAM_ZERO_CROSSING_MSK	BIT_ULL(3)
+#define AD9910_PROFILE_RAM_MODE_CONTROL_MSK	GENMASK_ULL(2, 0)
+
 /* Device constants */
 #define AD9910_PI_NANORAD		3141592653UL
 
@@ -160,6 +172,16 @@
 #define AD9910_STEP_RATE_MAX		GENMASK(15, 0)
 #define AD9910_NUM_PROFILES		8
 
+#define AD9910_RAM_FW_MAGIC		0x00AD9910
+#define AD9910_RAM_FW_V1		0x0001
+#define AD9910_RAM_SIZE_MAX_WORDS	1024
+#define AD9910_RAM_WORD_SIZE		sizeof(u32)
+#define AD9910_RAM_SIZE_MAX_BYTES	(AD9910_RAM_SIZE_MAX_WORDS * AD9910_RAM_WORD_SIZE)
+#define AD9910_RAM_ADDR_MAX		(AD9910_RAM_SIZE_MAX_WORDS - 1)
+
+#define AD9910_RAM_ENABLED(st)		\
+	FIELD_GET(AD9910_CFR1_RAM_ENABLE_MSK, (st)->reg[AD9910_REG_CFR1].val32)
+
 /* PLL constants */
 #define AD9910_PLL_MIN_N		12
 #define AD9910_PLL_MAX_N		127
@@ -191,7 +213,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 +234,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 +250,7 @@ enum ad9910_channel {
 	AD9910_CHANNEL_DRG = 130,
 	AD9910_CHANNEL_DRG_RAMP_UP = 131,
 	AD9910_CHANNEL_DRG_RAMP_DOWN = 132,
+	AD9910_CHANNEL_RAM = 140,
 };
 
 /**
@@ -244,6 +268,29 @@ enum ad9910_destination {
 	AD9910_DEST_POLAR,
 };
 
+/**
+ * struct ad9910_ram_fw - AD9910 RAM firmware format
+ * @magic:	Magic number for RAM firmware validation
+ * @version:	Firmware version number
+ * @wcount:	Number of RAM words to be written
+ * @crc:	CRC32 checksum of the RAM data for integrity verification
+ * @cfr1:	Value of CFR1 register to be configured (not all fields are
+ *		used, but this is included here for convenience)
+ * @profiles:	Array of RAM profile configurations
+ * @words:	Array of RAM words to be written. Data pattern should be set in
+ *		reverse order and wcount specifies the number of words in this
+ *		array
+ */
+struct ad9910_ram_fw {
+	__be32 magic;
+	__be16 version;
+	__be16 wcount;
+	__be32 crc;
+	__be32 cfr1;
+	__be64 profiles[AD9910_NUM_PROFILES];
+	__be32 words[] __counted_by_be(wcount);
+} __packed;
+
 enum {
 	AD9910_CHAN_IDX_PHY,
 	AD9910_CHAN_IDX_PROFILE_0,
@@ -258,6 +305,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 +338,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 +347,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 +386,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) {
@@ -1015,6 +1090,18 @@ static const struct iio_chan_spec ad9910_channels[] = {
 		.ext_info = ad9910_drg_ramp_ext_info,
 		.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG],
 	},
+	[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),
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
 };
 
 static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -1043,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;
 		}
@@ -1052,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);
@@ -1070,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;
 		}
@@ -1082,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;
@@ -1107,6 +1201,12 @@ static int ad9910_read_raw(struct iio_dev *indio_dev,
 			tmp64 = (tmp64 * AD9910_PI_NANORAD) >> 31;
 			*val = div_s64_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;
 		}
@@ -1115,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;
@@ -1155,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;
 		}
@@ -1176,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);
 
@@ -1202,6 +1306,41 @@ 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;
+
+			/* swap profile configs */
+			for (i = 0; i < AD9910_NUM_PROFILES; i++) {
+				tmp64 = st->reg[AD9910_REG_PROFILE(i)].val64;
+				ret = ad9910_reg64_write(st,
+							 AD9910_REG_PROFILE(i),
+							 st->reg_profile[i],
+							 false);
+				if (ret)
+					break;
+				st->reg_profile[i] = tmp64;
+			}
+
+			if (ret) {
+				/*
+				 * After the write failure, profiles 0..i-1 were
+				 * already swapped in SW, but Hw registers are
+				 * still pending an IO update, so swap them back
+				 * in SW to keep the state consistent.
+				 */
+				while (i--) {
+					tmp64 = st->reg[AD9910_REG_PROFILE(i)].val64;
+					st->reg[AD9910_REG_PROFILE(i)].val64 = st->reg_profile[i];
+					st->reg_profile[i] = tmp64;
+				}
+				return ret;
+			}
+
+			tmp32 = FIELD_PREP(AD9910_CFR1_RAM_ENABLE_MSK, !!val);
+			return ad9910_reg32_update(st, AD9910_REG_CFR1,
+						   AD9910_CFR1_RAM_ENABLE_MSK,
+						   tmp32, true);
 		default:
 			return -EINVAL;
 		}
@@ -1215,6 +1354,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,
@@ -1241,6 +1385,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;
 		}
@@ -1258,6 +1404,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,
@@ -1296,6 +1449,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;
 		}
@@ -1309,6 +1471,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,
@@ -1369,6 +1538,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;
 		}
@@ -1390,6 +1570,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:
@@ -1466,6 +1647,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,
@@ -1475,6 +1657,126 @@ static int ad9910_read_label(struct iio_dev *indio_dev,
 	return sysfs_emit(label, "%s\n", ad9910_channel_str[chan->address]);
 }
 
+static enum fw_upload_err ad9910_ram_fwu_prepare(struct fw_upload *fw_upload,
+						 const u8 *data, u32 size)
+{
+	struct ad9910_state *st = fw_upload->dd_handle;
+	const struct ad9910_ram_fw *fw_data = (const struct ad9910_ram_fw *)data;
+	size_t wcount, bcount;
+
+	if (size < sizeof(struct ad9910_ram_fw))
+		return FW_UPLOAD_ERR_INVALID_SIZE;
+
+	if (get_unaligned_be32(&fw_data->magic) != AD9910_RAM_FW_MAGIC)
+		return FW_UPLOAD_ERR_FW_INVALID;
+
+	if (get_unaligned_be16(&fw_data->version) != AD9910_RAM_FW_V1)
+		return FW_UPLOAD_ERR_FW_INVALID;
+
+	wcount = get_unaligned_be16(&fw_data->wcount);
+	bcount = size - sizeof(struct ad9910_ram_fw);
+	if (wcount > AD9910_RAM_SIZE_MAX_WORDS ||
+	    bcount != (wcount * AD9910_RAM_WORD_SIZE))
+		return FW_UPLOAD_ERR_INVALID_SIZE;
+
+	bcount += sizeof(fw_data->cfr1) + sizeof(fw_data->profiles);
+	if (crc32(0, &fw_data->cfr1, bcount) != get_unaligned_be32(&fw_data->crc))
+		return FW_UPLOAD_ERR_FW_INVALID;
+
+	guard(mutex)(&st->lock);
+	st->ram_fwu_cancel = false;
+
+	return FW_UPLOAD_ERR_NONE;
+}
+
+static enum fw_upload_err ad9910_ram_fwu_write(struct fw_upload *fw_upload,
+					       const u8 *data, u32 offset,
+					       u32 size, u32 *written)
+{
+	const struct ad9910_ram_fw *fw_data = (const struct ad9910_ram_fw *)data;
+	struct ad9910_state *st = fw_upload->dd_handle;
+	int ret, ret2, idx, wcount;
+	u64 tmp64, backup;
+
+	if (offset != 0)
+		return FW_UPLOAD_ERR_INVALID_SIZE;
+
+	guard(mutex)(&st->lock);
+
+	if (st->ram_fwu_cancel)
+		return FW_UPLOAD_ERR_CANCELED;
+
+	if (AD9910_RAM_ENABLED(st))
+		return FW_UPLOAD_ERR_HW_ERROR;
+
+	for (idx = 0; idx < AD9910_NUM_PROFILES; idx++)
+		st->reg_profile[idx] = get_unaligned_be64(&fw_data->profiles[idx]) |
+				       AD9910_PROFILE_RAM_OPEN_MSK;
+
+	ret = ad9910_reg32_update(st, AD9910_REG_CFR1,
+				  AD9910_CFR1_RAM_PLAYBACK_DEST_MSK |
+				  AD9910_CFR1_INT_PROFILE_CTL_MSK,
+				  get_unaligned_be32(&fw_data->cfr1), true);
+	if (ret)
+		return FW_UPLOAD_ERR_RW_ERROR;
+
+	wcount = get_unaligned_be16(&fw_data->wcount);
+	if (!wcount) {
+		*written = size;
+		return FW_UPLOAD_ERR_NONE; /* nothing else to write */
+	}
+
+	ret = ad9910_profile_set(st, st->profile);
+	if (ret)
+		return FW_UPLOAD_ERR_HW_ERROR;
+
+	/* backup profile register and update it with required address range */
+	backup = st->reg[AD9910_REG_PROFILE(st->profile)].val64;
+	tmp64 = AD9910_PROFILE_RAM_STEP_RATE_MSK |
+		FIELD_PREP(AD9910_PROFILE_RAM_START_ADDR_MSK, 0) |
+		FIELD_PREP(AD9910_PROFILE_RAM_END_ADDR_MSK, wcount - 1);
+	ret = ad9910_reg64_write(st, AD9910_REG_PROFILE(st->profile), tmp64, true);
+	if (ret)
+		return FW_UPLOAD_ERR_RW_ERROR;
+
+	memcpy(&st->tx_buf[1], fw_data->words, wcount * AD9910_RAM_WORD_SIZE);
+
+	/* write ram data and restore profile register */
+	ret = ad9910_spi_write(st, AD9910_REG_RAM,
+			       wcount * AD9910_RAM_WORD_SIZE, false);
+	ret2 = ad9910_reg64_write(st, AD9910_REG_PROFILE(st->profile), backup, true);
+	if (ret || ret2)
+		return FW_UPLOAD_ERR_RW_ERROR;
+
+	*written = size;
+	return FW_UPLOAD_ERR_NONE;
+}
+
+static enum fw_upload_err ad9910_ram_fwu_poll_complete(struct fw_upload *fw_upload)
+{
+	return FW_UPLOAD_ERR_NONE;
+}
+
+static void ad9910_ram_fwu_cancel(struct fw_upload *fw_upload)
+{
+	struct ad9910_state *st = fw_upload->dd_handle;
+
+	guard(mutex)(&st->lock);
+	st->ram_fwu_cancel = true;
+}
+
+static void ad9910_ram_fwu_unregister(void *data)
+{
+	firmware_upload_unregister(data);
+}
+
+static const struct fw_upload_ops ad9910_ram_fwu_ops = {
+	.prepare = ad9910_ram_fwu_prepare,
+	.write = ad9910_ram_fwu_write,
+	.poll_complete = ad9910_ram_fwu_poll_complete,
+	.cancel = ad9910_ram_fwu_cancel
+};
+
 static const struct iio_info ad9910_info = {
 	.read_raw = ad9910_read_raw,
 	.write_raw = ad9910_write_raw,
@@ -1601,9 +1903,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 +2014,25 @@ static int ad9910_probe(struct spi_device *spi)
 	if (ret)
 		return dev_err_probe(dev, ret, "device setup failed\n");
 
-	return devm_iio_device_register(dev, indio_dev);
+	snprintf(st->ram_fwu_name, sizeof(st->ram_fwu_name), "%s:ram",
+		 dev_name(&indio_dev->dev));
+	st->ram_fwu = firmware_upload_register(THIS_MODULE, dev, st->ram_fwu_name,
+					       &ad9910_ram_fwu_ops, st);
+	if (IS_ERR(st->ram_fwu))
+		return dev_err_probe(dev, PTR_ERR(st->ram_fwu),
+				     "failed to register ram upload ops\n");
+
+	ret = devm_add_action_or_reset(dev, ad9910_ram_fwu_unregister, st->ram_fwu);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to add ram upload unregister action\n");
+
+	ret = devm_iio_device_register(dev, indio_dev);
+	if (ret)
+		return ret;
+
+	ad9910_debugfs_init(st, indio_dev);
+	return 0;
 }
 
 static const struct spi_device_id ad9910_id[] = {

-- 
2.43.0



^ permalink raw reply related

* [PATCH v5 08/13] iio: frequency: ad9910: add digital ramp generator support
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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 | 515 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 513 insertions(+), 2 deletions(-)

diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index d2f40927b9be..4ad80475139d 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 = 116,
 	AD9910_CHANNEL_PROFILE_7 = 117,
 	AD9910_CHANNEL_PARALLEL_PORT = 120,
+	AD9910_CHANNEL_DRG = 130,
+	AD9910_CHANNEL_DRG_RAMP_UP = 131,
+	AD9910_CHANNEL_DRG_RAMP_DOWN = 132,
+};
+
+/**
+ * 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 {
@@ -481,6 +522,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,
@@ -499,6 +560,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;
 	}
@@ -538,6 +607,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;
 	}
@@ -643,6 +729,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, \
@@ -657,6 +916,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),
 	{ }
@@ -670,6 +932,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,						\
@@ -710,6 +980,41 @@ static const struct iio_chan_spec ad9910_channels[] = {
 		.ext_info = ad9910_pp_ext_info,
 		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
 	},
+	[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),
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
+	[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_FREQUENCY) |
+				      BIT(IIO_CHAN_INFO_PHASE) |
+				      BIT(IIO_CHAN_INFO_SCALE) |
+				      BIT(IIO_CHAN_INFO_SAMP_FREQ),
+		.ext_info = ad9910_drg_ramp_ext_info,
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG],
+	},
+	[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_FREQUENCY) |
+				      BIT(IIO_CHAN_INFO_PHASE) |
+				      BIT(IIO_CHAN_INFO_SCALE) |
+				      BIT(IIO_CHAN_INFO_SAMP_FREQ),
+		.ext_info = ad9910_drg_ramp_ext_info,
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_DRG],
+	},
 };
 
 static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -719,6 +1024,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);
 
@@ -733,6 +1039,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;
 		}
@@ -744,6 +1054,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;
 		}
@@ -761,6 +1087,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_s64_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_s64_rem(tmp64, NANO, val2);
+			return IIO_VAL_INT_PLUS_NANO;
 		default:
 			return -EINVAL;
 		}
@@ -773,6 +1119,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;
 		}
@@ -781,9 +1147,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;
 	}
@@ -817,6 +1197,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;
 		}
@@ -834,6 +1219,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;
 		}
@@ -855,6 +1262,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;
 		}
@@ -872,11 +1313,65 @@ 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);
+
+		if (val < 0 || val2 < 0 || val > st->data.sysclk_freq_hz / 4)
+			return -EINVAL;
+
+		tmp64 = ((u64)val * MICRO + val2) * 4;
+		if (!tmp64)
+			return -EINVAL;
+
+		tmp64 = DIV64_U64_ROUND_CLOSEST((u64)st->data.sysclk_freq_hz * MICRO, tmp64);
+		tmp32 = clamp(tmp64, 1U, AD9910_STEP_RATE_MAX);
+
+		switch (chan->channel) {
+		case AD9910_CHANNEL_DRG_RAMP_UP:
+			tmp32 = FIELD_PREP(AD9910_DRG_RATE_INC_MSK, tmp32);
+			return ad9910_reg32_update(st, AD9910_REG_DRG_RATE,
+						   AD9910_DRG_RATE_INC_MSK,
+						   tmp32, true);
+		case AD9910_CHANNEL_DRG_RAMP_DOWN:
+			tmp32 = FIELD_PREP(AD9910_DRG_RATE_DEC_MSK, tmp32);
+			return ad9910_reg32_update(st, AD9910_REG_DRG_RATE,
+						   AD9910_DRG_RATE_DEC_MSK,
+						   tmp32, true);
+		default:
+			return -EINVAL;
+		}
 	default:
 		return -EINVAL;
 	}
@@ -896,11 +1391,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;
 	}
@@ -963,6 +1463,9 @@ static const char * const ad9910_channel_str[] = {
 	[AD9910_CHAN_IDX_PROFILE_6] = "profile6",
 	[AD9910_CHAN_IDX_PROFILE_7] = "profile7",
 	[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,
@@ -1090,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

* [PATCH v5 06/13] iio: frequency: ad9910: initial driver implementation
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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 | 1060 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 1080 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index ea70b8449eb4..b2b7f54f5a24 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..c7b1e474c92d
--- /dev/null
+++ b/drivers/iio/frequency/ad9910.c
@@ -0,0 +1,1060 @@
+// 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	AD9910_MAX_SYSCLK_HZ
+
+#define AD9910_VCO0_RANGE_AUTO_MAX_HZ	(457 * HZ_PER_MHZ)
+#define AD9910_VCO1_RANGE_AUTO_MAX_HZ	(530 * HZ_PER_MHZ)
+#define AD9910_VCO2_RANGE_AUTO_MAX_HZ	(632 * HZ_PER_MHZ)
+#define AD9910_VCO3_RANGE_AUTO_MAX_HZ	(775 * HZ_PER_MHZ)
+#define AD9910_VCO4_RANGE_AUTO_MAX_HZ	(897 * HZ_PER_MHZ)
+#define AD9910_VCO_RANGE_NUM		6
+
+#define AD9910_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 = 110,
+	AD9910_CHANNEL_PROFILE_1 = 111,
+	AD9910_CHANNEL_PROFILE_2 = 112,
+	AD9910_CHANNEL_PROFILE_3 = 113,
+	AD9910_CHANNEL_PROFILE_4 = 114,
+	AD9910_CHANNEL_PROFILE_5 = 115,
+	AD9910_CHANNEL_PROFILE_6 = 116,
+	AD9910_CHANNEL_PROFILE_7 = 117,
+};
+
+enum {
+	AD9910_CHAN_IDX_PHY,
+	AD9910_CHAN_IDX_PROFILE_0,
+	AD9910_CHAN_IDX_PROFILE_1,
+	AD9910_CHAN_IDX_PROFILE_2,
+	AD9910_CHAN_IDX_PROFILE_3,
+	AD9910_CHAN_IDX_PROFILE_4,
+	AD9910_CHAN_IDX_PROFILE_5,
+	AD9910_CHAN_IDX_PROFILE_6,
+	AD9910_CHAN_IDX_PROFILE_7,
+};
+
+enum {
+	AD9910_POWERDOWN,
+};
+
+struct ad9910_data {
+	u32 sysclk_freq_hz;
+	u32 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;
+
+	if (!freq_hz || freq_hz > AD9910_MAX_SYSCLK_HZ)
+		return -EINVAL;
+
+	refclk_freq_hz = clk_get_rate(st->refclk);
+	if (st->data.pll_enabled) {
+		if (refclk_freq_hz < AD9910_PLL_IN_MIN_FREQ_HZ ||
+		    refclk_freq_hz > AD9910_PLL_IN_MAX_FREQ_HZ) {
+			dev_err(dev,
+				"REF_CLK frequency %u Hz is out of PLL input range\n",
+				refclk_freq_hz);
+			return -ERANGE;
+		}
+
+		tmp32 = DIV_ROUND_CLOSEST(freq_hz, refclk_freq_hz);
+		tmp32 = clamp(tmp32, DIV_ROUND_UP(AD9910_PLL_OUT_MIN_FREQ_HZ, refclk_freq_hz),
+			      AD9910_PLL_OUT_MAX_FREQ_HZ / refclk_freq_hz);
+		tmp32 = clamp(tmp32, AD9910_PLL_MIN_N, AD9910_PLL_MAX_N);
+		sysclk_freq_hz = refclk_freq_hz * tmp32;
+
+		if (sysclk_freq_hz <= AD9910_VCO0_RANGE_AUTO_MAX_HZ)
+			vco_sel = 0;
+		else if (sysclk_freq_hz <= AD9910_VCO1_RANGE_AUTO_MAX_HZ)
+			vco_sel = 1;
+		else if (sysclk_freq_hz <= AD9910_VCO2_RANGE_AUTO_MAX_HZ)
+			vco_sel = 2;
+		else if (sysclk_freq_hz <= AD9910_VCO3_RANGE_AUTO_MAX_HZ)
+			vco_sel = 3;
+		else if (sysclk_freq_hz <= AD9910_VCO4_RANGE_AUTO_MAX_HZ)
+			vco_sel = 4;
+		else
+			vco_sel = 5;
+
+		ret = ad9910_reg32_update(st, AD9910_REG_CFR3,
+					  AD9910_CFR3_N_MSK | AD9910_CFR3_VCO_SEL_MSK,
+					  FIELD_PREP(AD9910_CFR3_N_MSK, tmp32) |
+					  FIELD_PREP(AD9910_CFR3_VCO_SEL_MSK, vco_sel),
+					  update);
+		if (ret)
+			return ret;
+	} else {
+		tmp32 = DIV_ROUND_CLOSEST(refclk_freq_hz, freq_hz);
+		tmp32 = clamp(tmp32, 1U, 2U);
+		sysclk_freq_hz = refclk_freq_hz / tmp32;
+		tmp32 = AD9910_CFR3_VCO_SEL_MSK |
+			FIELD_PREP(AD9910_CFR3_REFCLK_DIV_BYPASS_MSK, tmp32 % 2);
+		ret = ad9910_reg32_update(st, AD9910_REG_CFR3,
+					  AD9910_CFR3_VCO_SEL_MSK |
+					  AD9910_CFR3_REFCLK_DIV_BYPASS_MSK,
+					  tmp32, update);
+		if (ret)
+			return ret;
+	}
+
+	st->data.sysclk_freq_hz = sysclk_freq_hz;
+
+	return 0;
+}
+
+static int ad9910_profile_set(struct ad9910_state *st, u8 profile)
+{
+	DECLARE_BITMAP(values, BITS_PER_TYPE(profile));
+
+	st->profile = profile;
+	values[0] = profile;
+	gpiod_multi_set_value_cansleep(st->gpio_profile, values);
+
+	return 0;
+}
+
+static inline bool ad9910_sw_powerdown_get(struct ad9910_state *st)
+{
+	return FIELD_GET(AD9910_CFR1_SOFT_POWER_DOWN_MSK,
+			 st->reg[AD9910_REG_CFR1].val32) ? true : false;
+}
+
+static int ad9910_sw_powerdown_set(struct ad9910_state *st, bool enable)
+{
+	if (ad9910_sw_powerdown_get(st) == enable)
+		return 0;
+
+	return ad9910_reg32_update(st, AD9910_REG_CFR1,
+				   AD9910_CFR1_SOFT_POWER_DOWN_MSK,
+				   enable ? AD9910_CFR1_SOFT_POWER_DOWN_MSK : 0,
+				   true);
+}
+
+static ssize_t ad9910_ext_info_read(struct iio_dev *indio_dev,
+				    uintptr_t private,
+				    const struct iio_chan_spec *chan,
+				    char *buf)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	int val;
+
+	guard(mutex)(&st->lock);
+
+	switch (private) {
+	case AD9910_POWERDOWN:
+		val = ad9910_sw_powerdown_get(st);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return iio_format_value(buf, IIO_VAL_INT, 1, &val);
+}
+
+static ssize_t ad9910_ext_info_write(struct iio_dev *indio_dev,
+				     uintptr_t private,
+				     const struct iio_chan_spec *chan,
+				     const char *buf, size_t len)
+{
+	struct ad9910_state *st = iio_priv(indio_dev);
+	u32 val32;
+	int ret;
+
+	ret = kstrtou32(buf, 10, &val32);
+	if (ret)
+		return ret;
+
+	guard(mutex)(&st->lock);
+
+	switch (private) {
+	case AD9910_POWERDOWN:
+		ret = ad9910_sw_powerdown_set(st, val32 ? true : false);
+		if (ret)
+			return ret;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return len;
+}
+
+#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),		\
+	.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],	\
+}
+
+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 (val < 0 || val2 < 0 || val >= st->data.sysclk_freq_hz / 2)
+			return -EINVAL;
+
+		tmp64 = ad9910_rational_scale((u64)val * MICRO + val2, BIT_ULL(32),
+					      (u64)MICRO * st->data.sysclk_freq_hz);
+		tmp64 = min(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 || val2 < 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] = "profile0",
+	[AD9910_CHAN_IDX_PROFILE_1] = "profile1",
+	[AD9910_CHAN_IDX_PROFILE_2] = "profile2",
+	[AD9910_CHAN_IDX_PROFILE_3] = "profile3",
+	[AD9910_CHAN_IDX_PROFILE_4] = "profile4",
+	[AD9910_CHAN_IDX_PROFILE_5] = "profile5",
+	[AD9910_CHAN_IDX_PROFILE_6] = "profile6",
+	[AD9910_CHAN_IDX_PROFILE_7] = "profile7",
+};
+
+static int ad9910_read_label(struct iio_dev *indio_dev,
+			     struct iio_chan_spec const *chan,
+			     char *label)
+{
+	return sysfs_emit(label, "%s\n", ad9910_channel_str[chan->address]);
+}
+
+static const struct iio_info ad9910_info = {
+	.read_raw = ad9910_read_raw,
+	.write_raw = ad9910_write_raw,
+	.write_raw_get_fmt = ad9910_write_raw_get_fmt,
+	.read_label = ad9910_read_label,
+	.debugfs_reg64_access = &ad9910_debugfs_reg_access,
+};
+
+static int ad9910_cfg_sysclk(struct ad9910_state *st, bool update)
+{
+	u32 cfr3 = AD9910_CFR3_OPEN_MSK;
+	u32 tmp32;
+
+	cfr3 |= FIELD_PREP(AD9910_CFR3_DRV0_MSK, st->data.refclk_out_drv);
+
+	if (st->data.pll_enabled) {
+		tmp32 = st->data.pll_charge_pump_current - AD9910_ICP_MIN_uA;
+		tmp32 = DIV_ROUND_CLOSEST(tmp32, AD9910_ICP_STEP_uA);
+		cfr3 |= FIELD_PREP(AD9910_CFR3_ICP_MSK, tmp32) |
+			AD9910_CFR3_PLL_EN_MSK;
+	} else {
+		cfr3 |= AD9910_CFR3_ICP_MSK |
+			AD9910_CFR3_REFCLK_DIV_RESETB_MSK |
+			AD9910_CFR3_PFD_RESET_MSK;
+	}
+	st->reg[AD9910_REG_CFR3].val32 = cfr3;
+
+	return ad9910_set_sysclk_freq(st, AD9910_MAX_SYSCLK_HZ, update);
+}
+
+static int ad9910_parse_fw(struct ad9910_state *st)
+{
+	static const char * const refclk_out_drv0[] = {
+		"disabled", "low", "medium", "high",
+	};
+	struct device *dev = &st->spi->dev;
+	u32 tmp[2];
+	int ret;
+
+	st->data.pll_enabled = device_property_read_bool(dev, "adi,pll-enable");
+	if (st->data.pll_enabled) {
+		tmp[0] = AD9910_ICP_MIN_uA;
+		device_property_read_u32(dev, "adi,charge-pump-current-microamp", &tmp[0]);
+		if (tmp[0] < AD9910_ICP_MIN_uA || tmp[0] > AD9910_ICP_MAX_uA)
+			return dev_err_probe(dev, -ERANGE,
+					     "invalid charge pump current %u\n", tmp[0]);
+		st->data.pll_charge_pump_current = tmp[0];
+
+		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[1] = AD9910_DAC_IOUT_DEFAULT_uA;
+	device_property_read_u32_array(dev, "output-range-microamp", tmp,
+				       ARRAY_SIZE(tmp));
+	if (tmp[1] < AD9910_DAC_IOUT_MIN_uA || tmp[1] > AD9910_DAC_IOUT_MAX_uA)
+		return dev_err_probe(dev, -ERANGE,
+				     "Invalid DAC output current %u uA\n", tmp[1]);
+	st->data.dac_output_current = tmp[1];
+
+	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,
+				 (st->spi->mode & SPI_3WIRE ? 0 :
+				 AD9910_CFR1_SDIO_INPUT_ONLY_MSK), false);
+	if (ret)
+		return ret;
+
+	ret = devm_add_action_or_reset(dev, ad9910_sw_powerdown_action, st);
+	if (ret)
+		return ret;
+
+	ret = ad9910_reg32_write(st, AD9910_REG_CFR2,
+				 AD9910_CFR2_AMP_SCALE_SINGLE_TONE_MSK |
+				 AD9910_CFR2_SYNC_TIMING_VAL_DISABLE_MSK |
+				 AD9910_CFR2_DRG_NO_DWELL_MSK |
+				 AD9910_CFR2_DATA_ASM_HOLD_LAST_MSK |
+				 AD9910_CFR2_SYNC_CLK_EN_MSK |
+				 AD9910_CFR2_PDCLK_ENABLE_MSK, false);
+	if (ret)
+		return ret;
+
+	ret = ad9910_cfg_sysclk(st, false);
+	if (ret)
+		return ret;
+
+	ret = ad9910_set_dac_current(st, 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

* [PATCH v5 07/13] iio: frequency: ad9910: add basic parallel port support
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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 | 147 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 147 insertions(+)

diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
index c7b1e474c92d..d2f40927b9be 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 = 115,
 	AD9910_CHANNEL_PROFILE_6 = 116,
 	AD9910_CHANNEL_PROFILE_7 = 117,
+	AD9910_CHANNEL_PARALLEL_PORT = 120,
 };
 
 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 {
@@ -482,6 +495,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;
 	}
@@ -510,6 +527,115 @@ 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 = ((u64)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 (val < 0 || val2 < 0 || val >= 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 != 0 || val2 < 0 || val2 >= (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 != 0 || val2 < 0 || val2 >= (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;
 	}
@@ -528,11 +654,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 +701,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,
+		.ext_info = ad9910_pp_ext_info,
+		.parent = &ad9910_channels[AD9910_CHAN_IDX_PHY],
+	},
 };
 
 static int ad9910_read_raw(struct iio_dev *indio_dev,
@@ -816,6 +962,7 @@ static const char * const ad9910_channel_str[] = {
 	[AD9910_CHAN_IDX_PROFILE_5] = "profile5",
 	[AD9910_CHAN_IDX_PROFILE_6] = "profile6",
 	[AD9910_CHAN_IDX_PROFILE_7] = "profile7",
+	[AD9910_CHAN_IDX_PARALLEL_PORT] = "parallel_port",
 };
 
 static int ad9910_read_label(struct iio_dev *indio_dev,

-- 
2.43.0



^ permalink raw reply related

* [PATCH v5 05/13] dt-bindings: iio: frequency: add ad9910
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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         | 200 +++++++++++++++++++++
 MAINTAINERS                                        |   7 +
 2 files changed, 207 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..5ab0db62dbf1
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
@@ -0,0 +1,200 @@
+# 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.
+
+  output-range-microamp:
+    description: DAC full-scale output current in microamps.
+    items:
+      - const: 0
+      - minimum: 8640
+        maximum: 31590
+        default: 20070
+
+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";
+            output-range-microamp = <0 20070>;
+        };
+    };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index 3115538ce829..ea70b8449eb4 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

* [PATCH v5 03/13] iio: core: add hierarchical channel relationships
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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

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

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

diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
index 5c8404efd0a5..348ac7a59738 100644
--- a/drivers/iio/industrialio-core.c
+++ b/drivers/iio/industrialio-core.c
@@ -776,6 +776,14 @@ static ssize_t iio_read_channel_label(struct device *dev,
 					 to_iio_dev_attr(attr)->c, buf);
 }
 
+static ssize_t iio_read_channel_parent(struct device *dev,
+				       struct device_attribute *attr,
+				       char *buf)
+{
+	return do_iio_read_channel_label(dev_to_iio_dev(dev),
+					 to_iio_dev_attr(attr)->c->parent, buf);
+}
+
 static ssize_t iio_read_channel_info(struct device *dev,
 				     struct device_attribute *attr,
 				     char *buf)
@@ -1263,6 +1271,31 @@ static int iio_device_add_channel_label(struct iio_dev *indio_dev,
 	return 1;
 }
 
+static int iio_device_add_channel_parent(struct iio_dev *indio_dev,
+					 struct iio_chan_spec const *chan)
+{
+	struct iio_dev_opaque *iio_dev_opaque = to_iio_dev_opaque(indio_dev);
+	int ret;
+
+	if (!chan->parent || (!indio_dev->info->read_label &&
+			      !chan->parent->extend_name))
+		return 0;
+
+	ret = __iio_add_chan_devattr("parent",
+				     chan,
+				     &iio_read_channel_parent,
+				     NULL,
+				     0,
+				     IIO_SEPARATE,
+				     &indio_dev->dev,
+				     NULL,
+				     &iio_dev_opaque->channel_attr_list);
+	if (ret < 0)
+		return ret;
+
+	return 1;
+}
+
 static int iio_device_add_info_mask_type(struct iio_dev *indio_dev,
 					 struct iio_chan_spec const *chan,
 					 enum iio_shared_by shared_by,
@@ -1401,6 +1434,11 @@ static int iio_device_add_channel_sysfs(struct iio_dev *indio_dev,
 		return ret;
 	attrcount += ret;
 
+	ret = iio_device_add_channel_parent(indio_dev, chan);
+	if (ret < 0)
+		return ret;
+	attrcount += ret;
+
 	if (chan->ext_info) {
 		unsigned int i = 0;
 
diff --git a/include/linux/iio/iio.h b/include/linux/iio/iio.h
index 86d17ee69e05..09a97518e4bd 100644
--- a/include/linux/iio/iio.h
+++ b/include/linux/iio/iio.h
@@ -258,6 +258,10 @@ struct iio_scan_type {
  *			by all channels.
  * @info_mask_shared_by_all_available: What availability information is to be
  *			exported that is shared by all channels.
+ * @parent:		Optional pointer to the parent channel spec for
+ *			hierarchical channel relationships. When set, a read-only
+ *			"parent" sysfs attribute is created containing the
+ *			parent channel's label.
  * @event_spec:		Array of events which should be registered for this
  *			channel.
  * @num_event_specs:	Size of the event_spec array.
@@ -306,6 +310,7 @@ struct iio_chan_spec {
 	unsigned long			info_mask_shared_by_dir_available;
 	unsigned long			info_mask_shared_by_all;
 	unsigned long			info_mask_shared_by_all_available;
+	const struct iio_chan_spec *parent;
 	const struct iio_event_spec *event_spec;
 	unsigned int		num_event_specs;
 	const struct iio_chan_spec_ext_info *ext_info;

-- 
2.43.0



^ permalink raw reply related

* [PATCH v5 02/13] iio: core: support 64-bit register through debugfs
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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

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

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

diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
index e587aca79b8e..5c8404efd0a5 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\n", 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) || *ppos != 0)
@@ -429,7 +439,7 @@ static ssize_t iio_debugfs_write_reg(struct file *file,
 
 	buf[ret] = '\0';
 
-	ret = sscanf(buf, "%i %i", &reg, &val);
+	ret = sscanf(buf, "%i %lli", &reg, &val64);
 
 	switch (ret) {
 	case 1:
@@ -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-opaque.h b/include/linux/iio/iio-opaque.h
index b87841a355f8..98330385e08d 100644
--- a/include/linux/iio/iio-opaque.h
+++ b/include/linux/iio/iio-opaque.h
@@ -73,7 +73,7 @@ struct iio_dev_opaque {
 #if defined(CONFIG_DEBUG_FS)
 	struct dentry			*debugfs_dentry;
 	unsigned int			cached_reg_addr;
-	char				read_buf[20];
+	char				read_buf[24];
 	unsigned int			read_buf_len;
 #endif
 };
diff --git a/include/linux/iio/iio.h b/include/linux/iio/iio.h
index 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

* [PATCH v5 04/13] Documentation: ABI: testing: add parent entry for iio channels
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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

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

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

diff --git a/Documentation/ABI/testing/sysfs-bus-iio b/Documentation/ABI/testing/sysfs-bus-iio
index 925a33fd309a..399944974e34 100644
--- a/Documentation/ABI/testing/sysfs-bus-iio
+++ b/Documentation/ABI/testing/sysfs-bus-iio
@@ -2118,6 +2118,19 @@ Description:
 		specific attributes. This is useful for userspace to be able to
 		better identify an individual channel.
 
+What:		/sys/bus/iio/devices/iio:deviceX/in_voltageY_parent
+What:		/sys/bus/iio/devices/iio:deviceX/out_voltageY_parent
+What:		/sys/bus/iio/devices/iio:deviceX/in_altvoltageY_parent
+What:		/sys/bus/iio/devices/iio:deviceX/out_altvoltageY_parent
+KernelVersion:	7.1
+Contact:	linux-iio@vger.kernel.org
+Description:
+		Read-only attribute containing the label of the parent channel
+		for hierarchical channel relationships. Only present on channels
+		that have a parent channel with a valid label. This is useful for
+		userspace to organize channels in tree-like structures that reflects
+		the physical or logical relationships between them.
+
 What:		/sys/bus/iio/devices/iio:deviceX/in_phaseY_raw
 KernelVersion:	4.18
 Contact:	linux-iio@vger.kernel.org

-- 
2.43.0



^ permalink raw reply related

* [PATCH v5 01/13] iio: core: validate file offset in iio_debugfs_write_reg()
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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, sashiko-bot
In-Reply-To: <20260517-ad9910-iio-driver-v5-0-31599c88314a@analog.com>

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

Check that a file offset is zero so that simple_write_to_buffer() can be
used safely, i.e., buf array is not left with uninitialized memory at its
start. It is not a big concern as it is a debug interface, but it is still
a hardening measure. The issue was introduced when direct call to
copy_from_user() was replaced by simple_write_to_buffer().

Fixes: 6d5dd486c715 ("iio: core: make use of simple_write_to_buffer()")
Reported-by: sashiko-bot@kernel.org
Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
---
 drivers/iio/industrialio-core.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/iio/industrialio-core.c b/drivers/iio/industrialio-core.c
index bd6f4f9f4533..e587aca79b8e 100644
--- a/drivers/iio/industrialio-core.c
+++ b/drivers/iio/industrialio-core.c
@@ -419,7 +419,7 @@ static ssize_t iio_debugfs_write_reg(struct file *file,
 	char buf[80];
 	int ret;
 
-	if (count >= sizeof(buf))
+	if (count >= sizeof(buf) || *ppos != 0)
 		return -EINVAL;
 
 	ret = simple_write_to_buffer(buf, sizeof(buf) - 1, ppos, userbuf,

-- 
2.43.0



^ permalink raw reply related

* [PATCH v5 00/13] AD9910 Direct Digital Synthesizer
From: Rodrigo Alencar via B4 Relay @ 2026-05-17 18:37 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, sashiko-bot

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

This is a follow-up of the V3/V4 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.

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

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

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

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

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

Kind regards,

Rodrigo Alencar

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

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

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

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

---
Rodrigo Alencar (13):
      iio: core: validate file offset in iio_debugfs_write_reg()
      iio: core: support 64-bit register through debugfs
      iio: core: add hierarchical channel relationships
      Documentation: ABI: testing: add parent entry for iio channels
      dt-bindings: iio: frequency: add ad9910
      iio: frequency: ad9910: initial driver implementation
      iio: frequency: ad9910: add basic parallel port support
      iio: frequency: ad9910: add digital ramp generator support
      iio: frequency: ad9910: add RAM mode support
      iio: frequency: ad9910: add output shift keying support
      iio: frequency: ad9910: show channel priority in debugfs
      Documentation: ABI: testing: add docs for ad9910 sysfs entries
      docs: iio: add documentation for ad9910 driver

 Documentation/ABI/testing/sysfs-bus-iio            |   13 +
 .../ABI/testing/sysfs-bus-iio-frequency-ad9910     |   76 +
 .../bindings/iio/frequency/adi,ad9910.yaml         |  200 ++
 Documentation/iio/ad9910.rst                       |  666 ++++++
 Documentation/iio/index.rst                        |    1 +
 MAINTAINERS                                        |   10 +
 drivers/iio/frequency/Kconfig                      |   21 +
 drivers/iio/frequency/Makefile                     |    1 +
 drivers/iio/frequency/ad9910.c                     | 2413 ++++++++++++++++++++
 drivers/iio/industrialio-core.c                    |   73 +-
 include/linux/iio/iio-opaque.h                     |    2 +-
 include/linux/iio/iio.h                            |    9 +
 12 files changed, 3474 insertions(+), 11 deletions(-)
---
base-commit: 1548c54e9adc32a719499216f63fba14b2fc07c3
change-id: 20260218-ad9910-iio-driver-9b3d214c251f

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



^ permalink raw reply

* Re: [PATCH v2 02/10] liveupdate: Extract luo_file_deserialize_one helper
From: Pasha Tatashin @ 2026-05-17 18:37 UTC (permalink / raw)
  To: Mike Rapoport
  Cc: Pasha Tatashin, linux-kselftest, shuah, akpm, linux-mm, skhan,
	linux-doc, linux-kernel, corbet, dmatlack, kexec, pratyush,
	skhawaja, graf
In-Reply-To: <agn50TRvhDpGm_9v@kernel.org>

On 05-17 20:24, Mike Rapoport wrote:
> On Thu, May 14, 2026 at 10:26:20PM +0000, Pasha Tatashin wrote:
> > Extract the logic for deserializing single entries for files into
> > separate helper functions. In preparation to a linked-block
> > serialization for files.
> 
> It would be nice to mention that this is a pure code movement without
> indented changes.

OK, will do that if there is another spin.

>  
> > Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
> > ---
> >  kernel/liveupdate/luo_file.c | 77 ++++++++++++++++++++----------------
> >  1 file changed, 44 insertions(+), 33 deletions(-)
> 
> Acked-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
> 
> -- 
> Sincerely yours,
> Mike.

^ permalink raw reply


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