Devicetree
 help / color / mirror / Atom feed
* [PATCH v4 2/2] PCI: mediatek: Add support for EcoNet EN7528 SoC
From: Caleb James DeLisle @ 2026-04-04 18:28 UTC (permalink / raw)
  To: linux-pci
  Cc: linux-mips, naseefkm, ryder.lee, helgaas, lpieralisi, kwilczynski,
	mani, robh, krzk+dt, conor+dt, matthias.bgg,
	angelogioacchino.delregno, ansuelsmth, linux-mediatek, devicetree,
	linux-kernel, Caleb James DeLisle
In-Reply-To: <20260404182854.2183651-1-cjd@cjdns.fr>

Add support for the PCIe present on the EcoNet EN7528 (and EN751221) SoCs.

These SoCs have a mix of Gen1 and Gen2 capable ports, but the Gen2 ports
require re-training after startup.

Co-developed-by: Ahmed Naseef <naseefkm@gmail.com>
Signed-off-by: Ahmed Naseef <naseefkm@gmail.com>
Signed-off-by: Caleb James DeLisle <cjd@cjdns.fr>
---
 drivers/pci/controller/Kconfig         |   2 +-
 drivers/pci/controller/pcie-mediatek.c | 133 +++++++++++++++++++++++++
 2 files changed, 134 insertions(+), 1 deletion(-)

diff --git a/drivers/pci/controller/Kconfig b/drivers/pci/controller/Kconfig
index 686349e09cd3..5808d5e407fd 100644
--- a/drivers/pci/controller/Kconfig
+++ b/drivers/pci/controller/Kconfig
@@ -209,7 +209,7 @@ config PCI_MVEBU
 
 config PCIE_MEDIATEK
 	tristate "MediaTek PCIe controller"
-	depends on ARCH_AIROHA || ARCH_MEDIATEK || COMPILE_TEST
+	depends on ARCH_AIROHA || ARCH_MEDIATEK || ECONET || COMPILE_TEST
 	depends on OF
 	depends on PCI_MSI
 	select IRQ_MSI_LIB
diff --git a/drivers/pci/controller/pcie-mediatek.c b/drivers/pci/controller/pcie-mediatek.c
index 75722524fe74..915a35825ce1 100644
--- a/drivers/pci/controller/pcie-mediatek.c
+++ b/drivers/pci/controller/pcie-mediatek.c
@@ -7,6 +7,7 @@
  *	   Honghui Zhang <honghui.zhang@mediatek.com>
  */
 
+#include <asm-generic/errno-base.h>
 #include <linux/clk.h>
 #include <linux/delay.h>
 #include <linux/iopoll.h>
@@ -14,6 +15,7 @@
 #include <linux/irqchip/chained_irq.h>
 #include <linux/irqchip/irq-msi-lib.h>
 #include <linux/irqdomain.h>
+#include <linux/kconfig.h>
 #include <linux/kernel.h>
 #include <linux/mfd/syscon.h>
 #include <linux/msi.h>
@@ -77,6 +79,7 @@
 
 #define PCIE_CONF_VEND_ID	0x100
 #define PCIE_CONF_DEVICE_ID	0x102
+#define PCIE_CONF_REV_CLASS	0x104
 #define PCIE_CONF_CLASS_ID	0x106
 
 #define PCIE_INT_MASK		0x420
@@ -89,6 +92,11 @@
 #define MSI_MASK		BIT(23)
 #define MTK_MSI_IRQS_NUM	32
 
+#define EN7528_HOST_MODE	0x00804201
+#define EN7528_LINKUP_REG	0x50
+#define EN7528_RC0_LINKUP	BIT(1)
+#define EN7528_RC1_LINKUP	BIT(2)
+
 #define PCIE_AHB_TRANS_BASE0_L	0x438
 #define PCIE_AHB_TRANS_BASE0_H	0x43c
 #define AHB2PCIE_SIZE(x)	((x) & GENMASK(4, 0))
@@ -148,12 +156,15 @@ struct mtk_pcie_port;
  * @MTK_PCIE_FIX_DEVICE_ID: host's device ID needed to be fixed
  * @MTK_PCIE_NO_MSI: Bridge has no MSI support, and relies on an external block
  * @MTK_PCIE_SKIP_RSTB: Skip calling RSTB bits on PCIe probe
+ * @MTK_PCIE_RETRAIN: Re-train link to bridge after startup because some
+ *                    Gen2-capable devices start as Gen1.
  */
 enum mtk_pcie_quirks {
 	MTK_PCIE_FIX_CLASS_ID = BIT(0),
 	MTK_PCIE_FIX_DEVICE_ID = BIT(1),
 	MTK_PCIE_NO_MSI = BIT(2),
 	MTK_PCIE_SKIP_RSTB = BIT(3),
+	MTK_PCIE_RETRAIN = BIT(4),
 };
 
 /**
@@ -753,6 +764,80 @@ static int mtk_pcie_startup_port_v2(struct mtk_pcie_port *port)
 	return 0;
 }
 
+static int mtk_pcie_startup_port_en7528(struct mtk_pcie_port *port)
+{
+	struct mtk_pcie *pcie = port->pcie;
+	struct pci_host_bridge *host = pci_host_bridge_from_priv(pcie);
+	struct resource *mem = NULL;
+	struct resource_entry *entry;
+	u32 val, link_mask;
+	int err;
+
+	entry = resource_list_first_type(&host->windows, IORESOURCE_MEM);
+	if (entry)
+		mem = entry->res;
+	if (!mem)
+		return -EINVAL;
+
+	if (!pcie->cfg) {
+		dev_err(pcie->dev, "EN7528: pciecfg syscon not available\n");
+		return -EINVAL;
+	}
+
+	/* Assert all reset signals */
+	writel(0, port->base + PCIE_RST_CTRL);
+
+	/*
+	 * Enable PCIe link down reset, if link status changed from link up to
+	 * link down, this will reset MAC control registers and configuration
+	 * space.
+	 */
+	writel(PCIE_LINKDOWN_RST_EN, port->base + PCIE_RST_CTRL);
+
+	msleep(PCIE_T_PVPERL_MS);
+
+	/* De-assert PHY, PE, PIPE, MAC and configuration reset */
+	val = readl(port->base + PCIE_RST_CTRL);
+	val |= PCIE_PHY_RSTB | PCIE_PERSTB | PCIE_PIPE_SRSTB |
+	       PCIE_MAC_SRSTB | PCIE_CRSTB;
+	writel(val, port->base + PCIE_RST_CTRL);
+
+	writel(PCIE_CLASS_CODE | PCIE_REVISION_ID,
+	       port->base + PCIE_CONF_REV_CLASS);
+	writel(EN7528_HOST_MODE, port->base);
+
+	link_mask = (port->slot == 0) ? EN7528_RC0_LINKUP : EN7528_RC1_LINKUP;
+
+	/* 100ms timeout value should be enough for Gen1/2 training */
+	err = regmap_read_poll_timeout(pcie->cfg, EN7528_LINKUP_REG, val,
+				       !!(val & link_mask), 20,
+				       PCI_PM_D3COLD_WAIT * USEC_PER_MSEC);
+	if (err) {
+		dev_err(pcie->dev, "EN7528: port%d link timeout\n", port->slot);
+		return -ETIMEDOUT;
+	}
+
+	/* Activate INTx interrupts */
+	val = readl(port->base + PCIE_INT_MASK);
+	val &= ~INTX_MASK;
+	writel(val, port->base + PCIE_INT_MASK);
+
+	if (IS_ENABLED(CONFIG_PCI_MSI))
+		mtk_pcie_enable_msi(port);
+
+	/* Set AHB to PCIe translation windows */
+	val = lower_32_bits(mem->start) |
+	      AHB2PCIE_SIZE(fls(resource_size(mem)));
+	writel(val, port->base + PCIE_AHB_TRANS_BASE0_L);
+
+	val = upper_32_bits(mem->start);
+	writel(val, port->base + PCIE_AHB_TRANS_BASE0_H);
+
+	writel(WIN_ENABLE, port->base + PCIE_AXI_WINDOW0);
+
+	return 0;
+}
+
 static void __iomem *mtk_pcie_map_bus(struct pci_bus *bus,
 				      unsigned int devfn, int where)
 {
@@ -1149,6 +1234,46 @@ static int mtk_pcie_probe(struct platform_device *pdev)
 	if (err)
 		goto put_resources;
 
+	/* EN7528 PCIe initially comes up as Gen1 even if Gen2 is supported.
+	 * The cannonical way to achieve Gen2 is to re-train the link
+	 * immediately after setup. However, to save a lot of duplicated code
+	 * we use pcie_retrain_link() which is usable once we have the pci_dev
+	 * struct for the bridge, i.e. after pci_host_probe(). */
+	if (pcie->soc->quirks & MTK_PCIE_RETRAIN) {
+		int slot = of_get_pci_domain_nr(dev->of_node);
+		struct pci_dev *rc = NULL;
+		int ret = -ENOENT;
+
+		if (slot >= 0)
+			rc = pci_get_slot(host->bus, PCI_DEVFN(slot, 0));
+
+		if (rc) {
+			ret = -EOPNOTSUPP;
+
+			/* pcie_retrain_link() is not an exported symbol but
+			 * this driver supports being built as a loadable
+			 * module. Someone using this on an EN7528 should make
+			 * it builtin, or accept Gen1 PCI. */
+#if IS_BUILTIN(CONFIG_PCIE_MEDIATEK)
+			ret = pcie_retrain_link(rc, true);
+#endif
+		}
+
+		if (ret) {
+			dev_info(dev, "port%d failed to retrain %pe\n", slot,
+				 ERR_PTR(ret));
+		} else {
+			u16 lnksta;
+			u32 speed;
+
+			pcie_capability_read_word(rc, PCI_EXP_LNKSTA, &lnksta);
+			speed = lnksta & PCI_EXP_LNKSTA_CLS;
+
+			dev_info(dev, "port%d link retrained, speed %s\n", slot,
+				 pci_speed_string(pcie_link_speed[speed]));
+		}
+	}
+
 	return 0;
 
 put_resources:
@@ -1264,8 +1389,16 @@ static const struct mtk_pcie_soc mtk_pcie_soc_mt7629 = {
 	.quirks = MTK_PCIE_FIX_CLASS_ID | MTK_PCIE_FIX_DEVICE_ID,
 };
 
+static const struct mtk_pcie_soc mtk_pcie_soc_en7528 = {
+	.ops = &mtk_pcie_ops_v2,
+	.startup = mtk_pcie_startup_port_en7528,
+	.setup_irq = mtk_pcie_setup_irq,
+	.quirks = MTK_PCIE_RETRAIN,
+};
+
 static const struct of_device_id mtk_pcie_ids[] = {
 	{ .compatible = "airoha,an7583-pcie", .data = &mtk_pcie_soc_an7583 },
+	{ .compatible = "econet,en7528-pcie", .data = &mtk_pcie_soc_en7528 },
 	{ .compatible = "mediatek,mt2701-pcie", .data = &mtk_pcie_soc_v1 },
 	{ .compatible = "mediatek,mt7623-pcie", .data = &mtk_pcie_soc_v1 },
 	{ .compatible = "mediatek,mt2712-pcie", .data = &mtk_pcie_soc_mt2712 },
-- 
2.39.5


^ permalink raw reply related

* [PATCH v4 0/2] PCI: mediatek: Add support for EcoNet SoCs
From: Caleb James DeLisle @ 2026-04-04 18:28 UTC (permalink / raw)
  To: linux-pci
  Cc: linux-mips, naseefkm, ryder.lee, helgaas, lpieralisi, kwilczynski,
	mani, robh, krzk+dt, conor+dt, matthias.bgg,
	angelogioacchino.delregno, ansuelsmth, linux-mediatek, devicetree,
	linux-kernel, Caleb James DeLisle

Add EcoNet EN7528 (and EN751221) PCIe support.

Changes from v3:
* s/initiallized/initialized/
* Use PCIE_T_PVPERL_MS for sleep time
* Use PCI_PM_D3COLD_WAIT for startup wait time
* Clarify comment "Activate INTx interrupts"
* Add MTK_PCIE_RETRAIN quirk for devices which require link re-train
* Do not retrain *all* bridges, only root bridge
* Better comments and logging in retraining logic
* v3: https://lore.kernel.org/linux-mips/20260320094212.696671-1-cjd@cjdns.fr/

Changes from v2:
* mediatek-pcie.yaml -> s/power-domain/power-domains/ and drop example
* Patch 3 dropped as it has been applied (Thanks!)
* v2: https://lore.kernel.org/linux-mips/20260316155157.679533-1-cjd@cjdns.fr/

Changes from v1:
* mediatek-pcie.yaml slot0 needs device-type = "pci", fix dt_binding_check
Link: https://lore.kernel.org/linux-mips/177334026016.3889069.9474337544951486443.robh@kernel.org
* v1: https://lore.kernel.org/linux-mips/20260312165332.569772-1-cjd@cjdns.fr/

This was split from a larger PCIe patchset which crossed multiple
subsystems. I'm not labeling this a v3 because it's a new patchset, but
I'm keeping the historical record anyway.

Changes from econet-pcie v2:
* mediatek-pcie.yaml add missing constraints to PCI node properties
* econet-pcie v2: https://lore.kernel.org/linux-mips/20260309131818.74467-1-cjd@cjdns.fr

Changes from econet-pcie v1:
* pcie-mediatek.c Exclude pcie_retrain_link() when building as a module
* econet-pcie v1: https://lore.kernel.org/linux-mips/20260303190948.694783-1-cjd@cjdns.fr/

Caleb James DeLisle (2):
  dt-bindings: PCI: mediatek: Add support for EcoNet EN7528
  PCI: mediatek: Add support for EcoNet EN7528 SoC

 .../bindings/pci/mediatek-pcie.yaml           |  26 ++++
 drivers/pci/controller/Kconfig                |   2 +-
 drivers/pci/controller/pcie-mediatek.c        | 133 ++++++++++++++++++
 3 files changed, 160 insertions(+), 1 deletion(-)

-- 
2.39.5


^ permalink raw reply

* [PATCH v4 1/2] dt-bindings: PCI: mediatek: Add support for EcoNet EN7528
From: Caleb James DeLisle @ 2026-04-04 18:28 UTC (permalink / raw)
  To: linux-pci
  Cc: linux-mips, naseefkm, ryder.lee, helgaas, lpieralisi, kwilczynski,
	mani, robh, krzk+dt, conor+dt, matthias.bgg,
	angelogioacchino.delregno, ansuelsmth, linux-mediatek, devicetree,
	linux-kernel, Caleb James DeLisle
In-Reply-To: <20260404182854.2183651-1-cjd@cjdns.fr>

Introduce EcoNet EN7528 SoC compatible in MediaTek PCIe controller
binding.

EcoNet PCIe controller has the same configuration model as
Mediatek v2 but is initialized more similarly to an MT7621
PCIe.

Signed-off-by: Caleb James DeLisle <cjd@cjdns.fr>
---
 .../bindings/pci/mediatek-pcie.yaml           | 26 +++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/Documentation/devicetree/bindings/pci/mediatek-pcie.yaml b/Documentation/devicetree/bindings/pci/mediatek-pcie.yaml
index 0b8c78ec4f91..7e1b0876c291 100644
--- a/Documentation/devicetree/bindings/pci/mediatek-pcie.yaml
+++ b/Documentation/devicetree/bindings/pci/mediatek-pcie.yaml
@@ -14,6 +14,7 @@ properties:
     oneOf:
       - enum:
           - airoha,an7583-pcie
+          - econet,en7528-pcie
           - mediatek,mt2712-pcie
           - mediatek,mt7622-pcie
           - mediatek,mt7629-pcie
@@ -226,6 +227,31 @@ allOf:
 
         mediatek,pbus-csr: false
 
+  - if:
+      properties:
+        compatible:
+          contains:
+            const: econet,en7528-pcie
+    then:
+      properties:
+        clocks:
+          maxItems: 1
+
+        clock-names:
+          maxItems: 1
+
+        reset: false
+
+        reset-names: false
+
+        power-domains: false
+
+        mediatek,pbus-csr: false
+
+      required:
+        - phys
+        - phy-names
+
 unevaluatedProperties: false
 
 examples:
-- 
2.39.5


^ permalink raw reply related

* [PATCH v2 2/2] ASoC: fsl_sai: Add RX/TX BCLK swap support
From: Marek Vasut @ 2026-04-04 18:35 UTC (permalink / raw)
  To: linux-sound
  Cc: Marek Vasut, Conor Dooley, Fabio Estevam, Jaroslav Kysela,
	Krzysztof Kozlowski, Liam Girdwood, Mark Brown, Nicolin Chen,
	Rob Herring, Shengjiu Wang, Takashi Iwai, Xiubo Li, devicetree,
	linux-kernel, linuxppc-dev
In-Reply-To: <20260404183547.46509-1-marex@nabladev.com>

Add support for setting the Bit Clock Swap bit in CR2 register
via new "fsl,sai-bit-clock-swap" DT property. This bit swaps the
bit clock used by the transmitter or receiver in asynchronous mode,
i.e. makes transmitter use RX_BCLK and TX_SYNC, and vice versa,
makes receiver use TX_BCLK and RX_SYNC.

Signed-off-by: Marek Vasut <marex@nabladev.com>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: Fabio Estevam <festevam@gmail.com>
Cc: Jaroslav Kysela <perex@perex.cz>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Liam Girdwood <lgirdwood@gmail.com>
Cc: Mark Brown <broonie@kernel.org>
Cc: Nicolin Chen <nicoleotsuka@gmail.com>
Cc: Rob Herring <robh@kernel.org>
Cc: Shengjiu Wang <shengjiu.wang@gmail.com>
Cc: Takashi Iwai <tiwai@suse.com>
Cc: Xiubo Li <Xiubo.Lee@gmail.com>
Cc: devicetree@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-sound@vger.kernel.org
Cc: linuxppc-dev@lists.ozlabs.org
---
V2: Update email, rebase on next
---
 sound/soc/fsl/fsl_sai.c | 7 ++++++-
 sound/soc/fsl/fsl_sai.h | 2 ++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/sound/soc/fsl/fsl_sai.c b/sound/soc/fsl/fsl_sai.c
index bd336d2e4cb38..87a40e2b9fdf7 100644
--- a/sound/soc/fsl/fsl_sai.c
+++ b/sound/soc/fsl/fsl_sai.c
@@ -355,6 +355,9 @@ static int fsl_sai_set_dai_fmt_tr(struct snd_soc_dai *cpu_dai,
 	unsigned int ofs = sai->soc_data->reg_offset;
 	u32 val_cr2 = 0, val_cr4 = 0;
 
+	if (sai->is_bit_clock_swap)
+		val_cr2 |= FSL_SAI_CR2_BCS;
+
 	if (!sai->is_lsb_first)
 		val_cr4 |= FSL_SAI_CR4_MF;
 
@@ -453,7 +456,8 @@ static int fsl_sai_set_dai_fmt_tr(struct snd_soc_dai *cpu_dai,
 	}
 
 	regmap_update_bits(sai->regmap, FSL_SAI_xCR2(tx, ofs),
-			   FSL_SAI_CR2_BCP | FSL_SAI_CR2_BCD_MSTR, val_cr2);
+			   FSL_SAI_CR2_BCS | FSL_SAI_CR2_BCP | FSL_SAI_CR2_BCD_MSTR,
+			   val_cr2);
 	regmap_update_bits(sai->regmap, FSL_SAI_xCR4(tx, ofs),
 			   FSL_SAI_CR4_MF | FSL_SAI_CR4_FSE |
 			   FSL_SAI_CR4_FSP | FSL_SAI_CR4_FSD_MSTR, val_cr4);
@@ -1507,6 +1511,7 @@ static int fsl_sai_probe(struct platform_device *pdev)
 	sai->soc_data = of_device_get_match_data(dev);
 
 	sai->is_lsb_first = of_property_read_bool(np, "lsb-first");
+	sai->is_bit_clock_swap = of_property_read_bool(np, "fsl,sai-bit-clock-swap");
 
 	base = devm_platform_get_and_ioremap_resource(pdev, 0, &sai->res);
 	if (IS_ERR(base))
diff --git a/sound/soc/fsl/fsl_sai.h b/sound/soc/fsl/fsl_sai.h
index af967833b6eda..6d84e5ff2258e 100644
--- a/sound/soc/fsl/fsl_sai.h
+++ b/sound/soc/fsl/fsl_sai.h
@@ -118,6 +118,7 @@
 
 /* SAI Transmit and Receive Configuration 2 Register */
 #define FSL_SAI_CR2_SYNC	BIT(30)
+#define FSL_SAI_CR2_BCS		BIT(29)
 #define FSL_SAI_CR2_BCI		BIT(28)
 #define FSL_SAI_CR2_MSEL_MASK	(0x3 << 26)
 #define FSL_SAI_CR2_MSEL_BUS	0
@@ -301,6 +302,7 @@ struct fsl_sai {
 	struct fsl_sai_dl_cfg *dl_cfg;
 	unsigned int dl_cfg_cnt;
 	bool mclk_direction_output;
+	bool is_bit_clock_swap;
 
 	unsigned int mclk_id[2];
 	unsigned int mclk_streams;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 1/2] ASoC: dt-bindings: fsl-sai: Document RX/TX BCLK swap support
From: Marek Vasut @ 2026-04-04 18:35 UTC (permalink / raw)
  To: linux-sound
  Cc: Marek Vasut, Conor Dooley, Fabio Estevam, Jaroslav Kysela,
	Krzysztof Kozlowski, Liam Girdwood, Mark Brown, Nicolin Chen,
	Rob Herring, Shengjiu Wang, Takashi Iwai, Xiubo Li, devicetree,
	linux-kernel, linuxppc-dev

Document support for setting the Bit Clock Swap bit in CR2 register
via new "fsl,sai-bit-clock-swap" DT property. This bit swaps the
bit clock used by the transmitter or receiver in asynchronous mode,
i.e. makes transmitter use RX_BCLK and TX_SYNC, and vice versa,
makes receiver use TX_BCLK and RX_SYNC.

Signed-off-by: Marek Vasut <marex@nabladev.com>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: Fabio Estevam <festevam@gmail.com>
Cc: Jaroslav Kysela <perex@perex.cz>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Liam Girdwood <lgirdwood@gmail.com>
Cc: Mark Brown <broonie@kernel.org>
Cc: Nicolin Chen <nicoleotsuka@gmail.com>
Cc: Rob Herring <robh@kernel.org>
Cc: Shengjiu Wang <shengjiu.wang@gmail.com>
Cc: Takashi Iwai <tiwai@suse.com>
Cc: Xiubo Li <Xiubo.Lee@gmail.com>
Cc: devicetree@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-sound@vger.kernel.org
Cc: linuxppc-dev@lists.ozlabs.org
---
V2: - Drop | from description
    - Update email, rebase on next
---
 Documentation/devicetree/bindings/sound/fsl,sai.yaml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/Documentation/devicetree/bindings/sound/fsl,sai.yaml b/Documentation/devicetree/bindings/sound/fsl,sai.yaml
index 83b5ea5f3d70e..ba65b3f3d0662 100644
--- a/Documentation/devicetree/bindings/sound/fsl,sai.yaml
+++ b/Documentation/devicetree/bindings/sound/fsl,sai.yaml
@@ -162,6 +162,13 @@ properties:
       of transmitter.
     type: boolean
 
+  fsl,sai-bit-clock-swap:
+    description:
+      Enable Bit Clock Swap, which swaps the bit clock used by the transmitter
+      or receiver in asynchronous mode, i.e. makes transmitter use RX_BCLK and
+      TX_SYNC, and vice versa, makes receiver use TX_BCLK and RX_SYNC.
+    type: boolean
+
   fsl,shared-interrupt:
     description: Interrupt is shared with other modules.
     type: boolean
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 4/4] clk: fsl-sai: Add MCLK generation support
From: Marek Vasut @ 2026-04-04 18:33 UTC (permalink / raw)
  To: linux-clk
  Cc: Marek Vasut, Michael Walle, Conor Dooley, Krzysztof Kozlowski,
	Michael Turquette, Michael Walle, Rob Herring, Stephen Boyd,
	devicetree, linux-kernel
In-Reply-To: <20260404183419.46455-1-marex@nabladev.com>

The driver currently supports generating BCLK. There are systems which
require generation of MCLK instead. Register new MCLK clock and handle
clock-cells = <1> to differentiate between BCLK and MCLK. In case of a
legacy system with clock-cells = <0>, the driver behaves as before, i.e.
always returns BCLK.

Note that it is not possible re-use the current SAI audio driver to
generate MCLK and correctly enable and disable the MCLK.

If SAI (audio driver) is used to control the MCLK enablement, then MCLK
clock is not always enabled, and it is not necessarily enabled when the
codec may need the clock to be enabled. There is also no way for the
codec node to specify phandle to clock provider in DT, because the SAI
(audio driver) is not clock provider.

If SAI (clock driver) is used to control the MCLK enablement, then MCLK
clock is enabled when the codec needs the clock enabled, because the
codec is the clock consumer and the SAI (clock driver) is the clock
provider, and the codec driver can request the clock to be enabled when
needed. There is also the usual phandle to clock provider in DT, because
the SAI (clock driver) is clock provider.

Acked-by: Michael Walle <mwalle@kernel.org>
Signed-off-by: Marek Vasut <marex@nabladev.com>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Michael Turquette <mturquette@baylibre.com>
Cc: Michael Walle <michael@walle.cc>
Cc: Rob Herring <robh@kernel.org>
Cc: Stephen Boyd <sboyd@kernel.org>
Cc: devicetree@vger.kernel.org
Cc: linux-clk@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
---
V2: No change
V3: - Rebase on current next, update mail address
    - Update commit message according to clarify the difference between
      SAI audio and SAI clock driver
    - Pick ancient AB from Michael, although this may be outdated
      https://patchwork.kernel.org/project/alsa-devel/patch/20241226162234.40141-4-marex@denx.de/
---
 drivers/clk/clk-fsl-sai.c | 74 ++++++++++++++++++++++++++++++++-------
 1 file changed, 61 insertions(+), 13 deletions(-)

diff --git a/drivers/clk/clk-fsl-sai.c b/drivers/clk/clk-fsl-sai.c
index 336aa8477d0ea..f00b49edb2e9f 100644
--- a/drivers/clk/clk-fsl-sai.c
+++ b/drivers/clk/clk-fsl-sai.c
@@ -7,6 +7,7 @@
 
 #include <linux/module.h>
 #include <linux/platform_device.h>
+#include <linux/clk.h>
 #include <linux/clk-provider.h>
 #include <linux/err.h>
 #include <linux/of.h>
@@ -15,21 +16,37 @@
 
 #define I2S_CSR		0x00
 #define I2S_CR2		0x08
+#define I2S_MCR		0x100
 #define CSR_BCE_BIT	28
+#define CSR_TE_BIT	31
 #define CR2_BCD		BIT(24)
 #define CR2_DIV_SHIFT	0
 #define CR2_DIV_WIDTH	8
+#define MCR_MOE		BIT(30)
 
 struct fsl_sai_clk {
-	struct clk_divider div;
-	struct clk_gate gate;
+	struct clk_divider bclk_div;
+	struct clk_divider mclk_div;
+	struct clk_gate bclk_gate;
+	struct clk_gate mclk_gate;
+	struct clk_hw *bclk_hw;
+	struct clk_hw *mclk_hw;
 	spinlock_t lock;
 };
 
 struct fsl_sai_data {
 	unsigned int	offset;	/* Register offset */
+	bool		have_mclk; /* Have MCLK control */
 };
 
+static struct clk_hw *
+fsl_sai_of_clk_get(struct of_phandle_args *clkspec, void *data)
+{
+	struct fsl_sai_clk *sai_clk = data;
+
+	return clkspec->args[0] ? sai_clk->mclk_hw : sai_clk->bclk_hw;
+}
+
 static int fsl_sai_clk_probe(struct platform_device *pdev)
 {
 	struct device *dev = &pdev->dev;
@@ -54,37 +71,68 @@ static int fsl_sai_clk_probe(struct platform_device *pdev)
 
 	spin_lock_init(&sai_clk->lock);
 
-	sai_clk->gate.reg = base + data->offset + I2S_CSR;
-	sai_clk->gate.bit_idx = CSR_BCE_BIT;
-	sai_clk->gate.lock = &sai_clk->lock;
+	sai_clk->bclk_gate.reg = base + data->offset + I2S_CSR;
+	sai_clk->bclk_gate.bit_idx = CSR_BCE_BIT;
+	sai_clk->bclk_gate.lock = &sai_clk->lock;
 
-	sai_clk->div.reg = base + data->offset + I2S_CR2;
-	sai_clk->div.shift = CR2_DIV_SHIFT;
-	sai_clk->div.width = CR2_DIV_WIDTH;
-	sai_clk->div.lock = &sai_clk->lock;
+	sai_clk->bclk_div.reg = base + data->offset + I2S_CR2;
+	sai_clk->bclk_div.shift = CR2_DIV_SHIFT;
+	sai_clk->bclk_div.width = CR2_DIV_WIDTH;
+	sai_clk->bclk_div.lock = &sai_clk->lock;
 
 	/* set clock direction, we are the BCLK master */
 	writel(CR2_BCD, base + data->offset + I2S_CR2);
 
-	hw = devm_clk_hw_register_composite_pdata(dev, dev->of_node->name,
+	hw = devm_clk_hw_register_composite_pdata(dev, "BCLK",
 						  &pdata, 1, NULL, NULL,
-						  &sai_clk->div.hw,
+						  &sai_clk->bclk_div.hw,
 						  &clk_divider_ops,
-						  &sai_clk->gate.hw,
+						  &sai_clk->bclk_gate.hw,
 						  &clk_gate_ops,
 						  CLK_SET_RATE_GATE);
 	if (IS_ERR(hw))
 		return PTR_ERR(hw);
 
-	return devm_of_clk_add_hw_provider(dev, of_clk_hw_simple_get, hw);
+	sai_clk->bclk_hw = hw;
+
+	if (data->have_mclk) {
+		sai_clk->mclk_gate.reg = base + data->offset + I2S_CSR;
+		sai_clk->mclk_gate.bit_idx = CSR_TE_BIT;
+		sai_clk->mclk_gate.lock = &sai_clk->lock;
+
+		sai_clk->mclk_div.reg = base + I2S_MCR;
+		sai_clk->mclk_div.shift = CR2_DIV_SHIFT;
+		sai_clk->mclk_div.width = CR2_DIV_WIDTH;
+		sai_clk->mclk_div.lock = &sai_clk->lock;
+
+		pdata.index = 1; /* MCLK1 */
+		hw = devm_clk_hw_register_composite_pdata(dev, "MCLK",
+							  &pdata, 1, NULL, NULL,
+							  &sai_clk->mclk_div.hw,
+							  &clk_divider_ops,
+							  &sai_clk->mclk_gate.hw,
+							  &clk_gate_ops,
+							  CLK_SET_RATE_GATE);
+		if (IS_ERR(hw))
+			return PTR_ERR(hw);
+
+		sai_clk->mclk_hw = hw;
+
+		/* set clock direction, we are the MCLK output */
+		writel(MCR_MOE, base + I2S_MCR);
+	}
+
+	return devm_of_clk_add_hw_provider(dev, fsl_sai_of_clk_get, sai_clk);
 }
 
 static const struct fsl_sai_data fsl_sai_vf610_data = {
 	.offset	= 0,
+	.have_mclk = false,
 };
 
 static const struct fsl_sai_data fsl_sai_imx8mq_data = {
 	.offset	= 8,
+	.have_mclk = true,
 };
 
 static const struct of_device_id of_fsl_sai_clk_ids[] = {
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 3/4] dt-bindings: clock: fsl-sai: Document clock-cells = <1> support
From: Marek Vasut @ 2026-04-04 18:33 UTC (permalink / raw)
  To: linux-clk
  Cc: Marek Vasut, Conor Dooley, Conor Dooley, Krzysztof Kozlowski,
	Michael Turquette, Michael Walle, Rob Herring, Stephen Boyd,
	devicetree, linux-kernel
In-Reply-To: <20260404183419.46455-1-marex@nabladev.com>

The driver now supports generation of both BCLK and MCLK, document
support for #clock-cells = <0> for legacy case and #clock-cells = <1>
for the new case which can differentiate between BCLK and MCLK.

Acked-by: Conor Dooley <conor.dooley@microchip.com>
Signed-off-by: Marek Vasut <marex@nabladev.com>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Michael Turquette <mturquette@baylibre.com>
Cc: Michael Walle <michael@walle.cc>
Cc: Rob Herring <robh@kernel.org>
Cc: Stephen Boyd <sboyd@kernel.org>
Cc: devicetree@vger.kernel.org
Cc: linux-clk@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
---
V2: Update commit message, align it with the bindings one
V3: - Rebase on current next, update mail address
    - Pick ancient AB from Conor, although this may be outdated
      https://patchwork.kernel.org/project/alsa-devel/patch/20241226162234.40141-3-marex@denx.de/
---
 Documentation/devicetree/bindings/clock/fsl,sai-clock.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Documentation/devicetree/bindings/clock/fsl,sai-clock.yaml b/Documentation/devicetree/bindings/clock/fsl,sai-clock.yaml
index 90799b3b505ee..041a63fa2d2b0 100644
--- a/Documentation/devicetree/bindings/clock/fsl,sai-clock.yaml
+++ b/Documentation/devicetree/bindings/clock/fsl,sai-clock.yaml
@@ -10,7 +10,7 @@ maintainers:
   - Michael Walle <michael@walle.cc>
 
 description: |
-  It is possible to use the BCLK pin of a SAI module as a generic
+  It is possible to use the BCLK or MCLK pin of a SAI module as a generic
   clock output. Some SoC are very constrained in their pin multiplexer
   configuration. E.g. pins can only be changed in groups. For example, on
   the LS1028A SoC you can only enable SAIs in pairs. If you use only one SAI,
@@ -47,7 +47,7 @@ properties:
       - const: mclk1
 
   '#clock-cells':
-    const: 0
+    maximum: 1
 
 allOf:
   - if:
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 2/4] clk: fsl-sai: Add i.MX8M support with 8 byte register offset
From: Marek Vasut @ 2026-04-04 18:33 UTC (permalink / raw)
  To: linux-clk
  Cc: Marek Vasut, Peng Fan, Conor Dooley, Krzysztof Kozlowski,
	Michael Turquette, Michael Walle, Rob Herring, Stephen Boyd,
	devicetree, linux-kernel
In-Reply-To: <20260404183419.46455-1-marex@nabladev.com>

The i.MX8M/Mini/Nano/Plus variant of the SAI IP has control registers
shifted by +8 bytes and requires additional bus clock. Add support for
the i.MX8M variant of the IP with this register shift and additional
clock.

Reviewed-by: Peng Fan <peng.fan@nxp.com>
Signed-off-by: Marek Vasut <marex@nabladev.com>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Michael Turquette <mturquette@baylibre.com>
Cc: Michael Walle <michael@walle.cc>
Cc: Rob Herring <robh@kernel.org>
Cc: Stephen Boyd <sboyd@kernel.org>
Cc: devicetree@vger.kernel.org
Cc: linux-clk@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
---
V2: Update commit message, align it with the bindings one
V3: - Rebase on current next, update mail address
    - Pick ancient RB from Peng, although this may be outdated
      https://patchwork.kernel.org/project/alsa-devel/patch/20241226162234.40141-2-marex@denx.de/
    - Optionally enable "bus" clock, which are needed on MX8M to operate
      register file
---
 drivers/clk/Kconfig       |  2 +-
 drivers/clk/clk-fsl-sai.c | 27 +++++++++++++++++++++++----
 2 files changed, 24 insertions(+), 5 deletions(-)

diff --git a/drivers/clk/Kconfig b/drivers/clk/Kconfig
index cc8743b11bb1f..9f7f391a5615a 100644
--- a/drivers/clk/Kconfig
+++ b/drivers/clk/Kconfig
@@ -255,7 +255,7 @@ config COMMON_CLK_FSL_FLEXSPI
 
 config COMMON_CLK_FSL_SAI
 	bool "Clock driver for BCLK of Freescale SAI cores"
-	depends on ARCH_LAYERSCAPE || COMPILE_TEST
+	depends on ARCH_LAYERSCAPE || ARCH_MXC || COMPILE_TEST
 	help
 	  This driver supports the Freescale SAI (Synchronous Audio Interface)
 	  to be used as a generic clock output. Some SoCs have restrictions
diff --git a/drivers/clk/clk-fsl-sai.c b/drivers/clk/clk-fsl-sai.c
index cba45e07562da..336aa8477d0ea 100644
--- a/drivers/clk/clk-fsl-sai.c
+++ b/drivers/clk/clk-fsl-sai.c
@@ -26,11 +26,17 @@ struct fsl_sai_clk {
 	spinlock_t lock;
 };
 
+struct fsl_sai_data {
+	unsigned int	offset;	/* Register offset */
+};
+
 static int fsl_sai_clk_probe(struct platform_device *pdev)
 {
 	struct device *dev = &pdev->dev;
+	const struct fsl_sai_data *data = device_get_match_data(dev);
 	struct fsl_sai_clk *sai_clk;
 	struct clk_parent_data pdata = { .index = 0 };
+	struct clk *clk_bus;
 	void __iomem *base;
 	struct clk_hw *hw;
 
@@ -42,19 +48,23 @@ static int fsl_sai_clk_probe(struct platform_device *pdev)
 	if (IS_ERR(base))
 		return PTR_ERR(base);
 
+	clk_bus = devm_clk_get_optional_enabled(dev, "bus");
+	if (IS_ERR(clk_bus))
+		return PTR_ERR(clk_bus);
+
 	spin_lock_init(&sai_clk->lock);
 
-	sai_clk->gate.reg = base + I2S_CSR;
+	sai_clk->gate.reg = base + data->offset + I2S_CSR;
 	sai_clk->gate.bit_idx = CSR_BCE_BIT;
 	sai_clk->gate.lock = &sai_clk->lock;
 
-	sai_clk->div.reg = base + I2S_CR2;
+	sai_clk->div.reg = base + data->offset + I2S_CR2;
 	sai_clk->div.shift = CR2_DIV_SHIFT;
 	sai_clk->div.width = CR2_DIV_WIDTH;
 	sai_clk->div.lock = &sai_clk->lock;
 
 	/* set clock direction, we are the BCLK master */
-	writel(CR2_BCD, base + I2S_CR2);
+	writel(CR2_BCD, base + data->offset + I2S_CR2);
 
 	hw = devm_clk_hw_register_composite_pdata(dev, dev->of_node->name,
 						  &pdata, 1, NULL, NULL,
@@ -69,8 +79,17 @@ static int fsl_sai_clk_probe(struct platform_device *pdev)
 	return devm_of_clk_add_hw_provider(dev, of_clk_hw_simple_get, hw);
 }
 
+static const struct fsl_sai_data fsl_sai_vf610_data = {
+	.offset	= 0,
+};
+
+static const struct fsl_sai_data fsl_sai_imx8mq_data = {
+	.offset	= 8,
+};
+
 static const struct of_device_id of_fsl_sai_clk_ids[] = {
-	{ .compatible = "fsl,vf610-sai-clock" },
+	{ .compatible = "fsl,vf610-sai-clock", .data = &fsl_sai_vf610_data },
+	{ .compatible = "fsl,imx8mq-sai-clock", .data = &fsl_sai_imx8mq_data },
 	{ }
 };
 MODULE_DEVICE_TABLE(of, of_fsl_sai_clk_ids);
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 1/4] dt-bindings: clock: fsl-sai: Document i.MX8M support
From: Marek Vasut @ 2026-04-04 18:33 UTC (permalink / raw)
  To: linux-clk
  Cc: Marek Vasut, Conor Dooley, Conor Dooley, Krzysztof Kozlowski,
	Michael Turquette, Michael Walle, Rob Herring, Stephen Boyd,
	devicetree, linux-kernel

The i.MX8M/Mini/Nano/Plus variant of the SAI IP has control registers
shifted by +8 bytes and requires additional bus clock. Document support
for the i.MX8M variant of the IP with this register shift and additional
clock. Update the description slightly.

Acked-by: Conor Dooley <conor.dooley@microchip.com>
Signed-off-by: Marek Vasut <marex@nabladev.com>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Michael Turquette <mturquette@baylibre.com>
Cc: Michael Walle <michael@walle.cc>
Cc: Rob Herring <robh@kernel.org>
Cc: Stephen Boyd <sboyd@kernel.org>
Cc: devicetree@vger.kernel.org
Cc: linux-clk@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
---
V2: No change
V3: - Rebase on current next, update mail address
    - Pick ancient AB from Conor, although this may be outdated
      https://patchwork.kernel.org/project/alsa-devel/patch/20241226162234.40141-1-marex@denx.de/
    - Invert the allOf conditional to match on VF610 and limit
      the clocks/clock-names there. MX8M can have one or two
      input clock, "bus" is mandatory and "mclk1" is optional.
      The "mclk1" are used by the driver in 4/4 .
---
 .../bindings/clock/fsl,sai-clock.yaml         | 41 ++++++++++++++++---
 1 file changed, 35 insertions(+), 6 deletions(-)

diff --git a/Documentation/devicetree/bindings/clock/fsl,sai-clock.yaml b/Documentation/devicetree/bindings/clock/fsl,sai-clock.yaml
index 3bca9d11c148f..90799b3b505ee 100644
--- a/Documentation/devicetree/bindings/clock/fsl,sai-clock.yaml
+++ b/Documentation/devicetree/bindings/clock/fsl,sai-clock.yaml
@@ -10,10 +10,10 @@ maintainers:
   - Michael Walle <michael@walle.cc>
 
 description: |
-  It is possible to use the BCLK pin of a SAI module as a generic clock
-  output. Some SoC are very constrained in their pin multiplexer
-  configuration. Eg. pins can only be changed groups. For example, on the
-  LS1028A SoC you can only enable SAIs in pairs. If you use only one SAI,
+  It is possible to use the BCLK pin of a SAI module as a generic
+  clock output. Some SoC are very constrained in their pin multiplexer
+  configuration. E.g. pins can only be changed in groups. For example, on
+  the LS1028A SoC you can only enable SAIs in pairs. If you use only one SAI,
   the second pins are wasted. Using this binding it is possible to use the
   clock of the second SAI as a MCLK clock for an audio codec, for example.
 
@@ -21,17 +21,46 @@ description: |
 
 properties:
   compatible:
-    const: fsl,vf610-sai-clock
+    oneOf:
+      - items:
+          - enum:
+              - fsl,imx8mm-sai-clock
+              - fsl,imx8mn-sai-clock
+              - fsl,imx8mp-sai-clock
+          - const: fsl,imx8mq-sai-clock
+      - items:
+          - enum:
+              - fsl,imx8mq-sai-clock
+              - fsl,vf610-sai-clock
 
   reg:
     maxItems: 1
 
   clocks:
-    maxItems: 1
+    minItems: 1
+    maxItems: 2
+
+  clock-names:
+    minItems: 1
+    items:
+      - const: bus
+      - const: mclk1
 
   '#clock-cells':
     const: 0
 
+allOf:
+  - if:
+      properties:
+        compatible:
+          contains:
+            const: fsl,vf610-sai-clock
+    then:
+      properties:
+        clocks:
+          maxItems: 1
+        clock-names: false
+
 required:
   - compatible
   - reg
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH v4 0/9] driver core: Fix some race conditions
From: Rafael J. Wysocki @ 2026-04-04 17:11 UTC (permalink / raw)
  To: Douglas Anderson
  Cc: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Alan Stern, Saravana Kannan, Christoph Hellwig, Eric Dumazet,
	Johan Hovold, Leon Romanovsky, Alexander Lobakin,
	Alexey Kardashevskiy, Robin Murphy, Andrew Morton, Frank.Li,
	Jason Gunthorpe, alex, alexander.stein, andre.przywara, andrew,
	andrew, andriy.shevchenko, aou, ardb, bhelgaas, brgl, broonie,
	catalin.marinas, chleroy, davem, david, devicetree, dmaengine,
	driver-core, gbatra, gregory.clement, hkallweit1, iommu,
	jirislaby, joel, joro, kees, kevin.brodsky, kuba, lenb, lgirdwood,
	linux-acpi, linux-arm-kernel, linux-aspeed, linux-cxl,
	linux-kernel, linux-mips, linux-mm, linux-pci, linux-riscv,
	linux-serial, linux-snps-arc, linux-usb, linux, linuxppc-dev,
	m.szyprowski, maddy, mani, maz, miko.lenczewski, mpe, netdev,
	npiggin, osalvador, oupton, pabeni, palmer, peter.ujfalusi,
	peterz, pjw, robh, sebastian.hesselbarth, tglx, tsbogend, vgupta,
	vkoul, will, willy, yangyicong, yeoreum.yun
In-Reply-To: <20260404000644.522677-1-dianders@chromium.org>

On Sat, Apr 4, 2026 at 2:07 AM Douglas Anderson <dianders@chromium.org> wrote:
>
> The main goal of this series is to fix the observed bug talked about
> in the first patch ("driver core: Don't let a device probe until it's
> ready"). That patch fixes a problem that has been observed in the real
> world and could land even if the rest of the patches are found
> unacceptable or need to be spun.
>
> That said, during patch review Danilo correctly pointed out that many
> of the bitfield accesses in "struct device" are unsafe. I added a
> bunch of patches in the series to address each one.
>
> Danilo said he's most worried about "can_match", so I put that one
> first. After that, I tried to transition bitfields to flags in reverse
> order to when the bitfield was added.
>
> Even if transitioning from bitfields to flags isn't truly needed for
> correctness, it seems silly (and wasteful of space in struct device)
> to have some in bitfields and some as flags. Thus I didn't spend time
> for each bitfield showing that it's truly needed for correctness.
>
> Transition was done semi manually. Presumably someone skilled at
> coccinelle could do a better job, but I just used sed in a heavy-
> handed manner and then reviewed/fixed the results, undoing anything my
> script got wrong. My terrible/ugly script was:
>
> var=can_match
> caps="${var^^}"
> for f in $(git grep -l "[>\.]${var}[^1-9_a-zA-Z\[]"); do
>   echo $f
>   sed -i~ -e "s/\([a-zA-Z_0-9\.>()-][a-zA-Z_0-9\.>()-]*\)->${var} = true/set_bit(DEV_FLAG_${caps}, \&\\1->flags)/" "$f"
>   sed -i~ -e "s/\([a-zA-Z_0-9\.>()-][a-zA-Z_0-9\.>()-]*\)\.${var} = true/dev_set_${caps}(\&\\1)/" "$f"
>   sed -i~ -e "s/\([a-zA-Z_0-9\.>()-][a-zA-Z_0-9\.>()-]*\)->${var} = false/clear_bit(DEV_FLAG_${caps}, \&\\1->flags)/" "$f"
>   sed -i~ -e "s/\([a-zA-Z_0-9\.>()-][a-zA-Z_0-9\.>()-]*\)\.${var} = false/dev_clear_${caps}(\&\\1)/" "$f"
>   sed -i~ -e "s/\([a-zA-Z_0-9\.>()-][a-zA-Z_0-9\.>()-]*\)->${var} = \([^;]*\)/assign_bit(DEV_FLAG_${caps}, \&\\1->flags, \\2)/" "$f"
>   sed -i~ -e "s/\([a-zA-Z_0-9\.>()-][a-zA-Z_0-9\.>()-]*\)\.${var} = \([^;]*\)/dev_assign_${caps}(\&\\1, \\2)/" "$f"
>   sed -i~ -e "s/\([a-zA-Z_0-9\.>()-][a-zA-Z_0-9\.>()-]*\)->${var}\([^1-9_a-zA-Z\[]\)/test_bit(DEV_FLAG_${caps}, \&\\1->flags)\\2/" "$f"
>   sed -i~ -e "s/\([a-zA-Z_0-9\.>()-][a-zA-Z_0-9\.>()-]*\)\.${var}\([^1-9_a-zA-Z\[]\)/dev_${caps}(\&\\1)\\2/" "$f"
> done
>
> From v3 to v4, I transitioned to accessor functions with another ugly
> sed script. I had git format the old patches, then transformed them
> with:
>
> for f in *.patch; do
>   echo $f
>   sed -i~ -e "s/test_and_set_bit(DEV_FLAG_\([^,]*\), \&\(.*\)->flags)/dev_test_and_set_\\L\\1(\\2)/" "$f"
>   sed -i~ -e "s/test_and_set_bit(DEV_FLAG_\([^,]*\), \(.*\)\.flags)/dev_test_and_set_\\L\\1(\\2)/" "$f"
>   sed -i~ -e "s/test_bit(DEV_FLAG_\([^,]*\), \&\(.*\)->flags)/dev_\\L\\1(\\2)/" "$f"
>   sed -i~ -e "s/test_bit(DEV_FLAG_\([^,]*\), \(.*\)\.flags)/dev_\\L\\1(\\2)/" "$f"
>   sed -i~ -e "s/set_bit(DEV_FLAG_\([^,]*\), \&\(.*\)->flags)/dev_set_\\L\\1(\\2)/" "$f"
>   sed -i~ -e "s/set_bit(DEV_FLAG_\([^,]*\), \(.*\)\.flags)/dev_set_\\L\\1(\\2)/" "$f"
>   sed -i~ -e "s/clear_bit(DEV_FLAG_\([^,]*\), \&\(.*\)->flags)/dev_clear_\\L\\1(\\2)/" "$f"
>   sed -i~ -e "s/clear_bit(DEV_FLAG_\([^,]*\), \(.*\)\.flags)/dev_clear_\\L\\1(\\2)/" "$f"
>   sed -i~ -e "s/assign_bit(DEV_FLAG_\([^,]*\), \&\(.*\)->flags, \(.*\))/dev_assign_\\L\\1(\\2, \\3)/" "$f"
>   sed -i~ -e "s/assign_bit(DEV_FLAG_\([^,]*\), \(.*\)\.flags, \(.*\))/dev_assign_\\L\\1(\\2, \\3)/" "$f"
> done
>
> ...and then did a few manual touchups for spacing.
>
> NOTE: one potentially "controversial" choice I made in some patches
> was to always reserve a flag ID even if a flag is only used under
> certain CONFIG_ settings. This is a change from how things were
> before. Keeping the numbering consistent and allowing easy
> compile-testing of both CONFIG settings seemed worth it, especially
> since it won't take up any extra space until we've added a lot more
> flags.
>
> I only marked the first patch as a "Fix" since it is the only one
> fixing observed problems. Other patches could be considered fixes too
> if folks want.
>
> I tested the first patch in the series backported to kernel 6.6 on the
> Pixel phone that was experiencing the race. I added extra printouts to
> make sure that the problem was hitting / addressed. The rest of the
> patches are tested with allmodconfig with arm32, arm64, ppc, and
> x86. I boot tested on an arm64 Chromebook running mainline.
>
> Changes in v4:
> - Use accessor functions for flags
>
> Changes in v3:
> - Use a new "flags" bitfield
> - Add missing \n in probe error message
>
> Changes in v2:
> - Instead of adjusting the ordering, use "ready_to_probe" flag
>
> Douglas Anderson (9):
>   driver core: Don't let a device probe until it's ready
>   driver core: Replace dev->can_match with dev_can_match()
>   driver core: Replace dev->dma_iommu with dev_dma_iommu()
>   driver core: Replace dev->dma_skip_sync with dev_dma_skip_sync()
>   driver core: Replace dev->dma_ops_bypass with dev_dma_ops_bypass()
>   driver core: Replace dev->state_synced with dev_state_synced()
>   driver core: Replace dev->dma_coherent with dev_dma_coherent()
>   driver core: Replace dev->of_node_reused with dev_of_node_reused()
>   driver core: Replace dev->offline + ->offline_disabled with accessors
>
>  arch/arc/mm/dma.c                             |   4 +-
>  arch/arm/mach-highbank/highbank.c             |   2 +-
>  arch/arm/mach-mvebu/coherency.c               |   2 +-
>  arch/arm/mm/dma-mapping-nommu.c               |   4 +-
>  arch/arm/mm/dma-mapping.c                     |  28 ++--
>  arch/arm64/kernel/cpufeature.c                |   2 +-
>  arch/arm64/mm/dma-mapping.c                   |   2 +-
>  arch/mips/mm/dma-noncoherent.c                |   2 +-
>  arch/powerpc/kernel/dma-iommu.c               |   8 +-
>  .../platforms/pseries/hotplug-memory.c        |   4 +-
>  arch/riscv/mm/dma-noncoherent.c               |   2 +-
>  drivers/acpi/scan.c                           |   2 +-
>  drivers/base/core.c                           |  53 +++++---
>  drivers/base/cpu.c                            |   4 +-
>  drivers/base/dd.c                             |  28 ++--
>  drivers/base/memory.c                         |   2 +-
>  drivers/base/pinctrl.c                        |   2 +-
>  drivers/base/platform.c                       |   2 +-
>  drivers/dma/ti/k3-udma-glue.c                 |   6 +-
>  drivers/dma/ti/k3-udma.c                      |   6 +-
>  drivers/iommu/dma-iommu.c                     |   9 +-
>  drivers/iommu/iommu.c                         |   5 +-
>  drivers/net/pcs/pcs-xpcs-plat.c               |   2 +-
>  drivers/of/device.c                           |   6 +-
>  drivers/pci/of.c                              |   2 +-
>  drivers/pci/pwrctrl/core.c                    |   2 +-
>  drivers/regulator/bq257xx-regulator.c         |   2 +-
>  drivers/regulator/rk808-regulator.c           |   2 +-
>  drivers/tty/serial/serial_base_bus.c          |   2 +-
>  drivers/usb/gadget/udc/aspeed-vhub/dev.c      |   2 +-
>  include/linux/device.h                        | 120 ++++++++++++------
>  include/linux/dma-map-ops.h                   |   6 +-
>  include/linux/dma-mapping.h                   |   2 +-
>  include/linux/iommu-dma.h                     |   3 +-
>  kernel/cpu.c                                  |   4 +-
>  kernel/dma/mapping.c                          |  12 +-
>  mm/hmm.c                                      |   2 +-
>  37 files changed, 206 insertions(+), 142 deletions(-)
>
> --

For the whole set

Reviewed-by: Rafael J. Wysocki (Intel) <rafael@kernel.org>

^ permalink raw reply

* Re: [PATCH V2 0/8] PCI: imx6: Integrate pwrctrl API and update device trees
From: Manivannan Sadhasivam @ 2026-04-04 17:10 UTC (permalink / raw)
  To: Sherry Sun
  Cc: robh, krzk+dt, conor+dt, Frank.Li, s.hauer, kernel, festevam,
	lpieralisi, kwilczynski, bhelgaas, hongxing.zhu, l.stach, imx,
	linux-pci, linux-arm-kernel, devicetree, linux-kernel
In-Reply-To: <20260402101007.208419-1-sherry.sun@nxp.com>

On Thu, Apr 02, 2026 at 06:09:59PM +0800, Sherry Sun wrote:
> Note: This patch set depends on my previous patch set [1] which adds
> Root Port device tree nodes and support parsing the reset property in
> new Root Port binding in pci-imx6 driver.
> 
> This series integrates the PCI pwrctrl framework into the pci-imx6
> driver and updates i.MX EVK board device trees to support it.
> 
> Patches 2-8 update device trees for i.MX EVK boards which maintained
> by NXP to move power supply properties from the PCIe controller node
> to the Root Port child node, which is required for pwrctrl framework.
> Affected boards:
> - i.MX6Q/DL SABRESD
> - i.MX6SX SDB
> - i.MX8MM EVK
> - i.MX8MP EVK
> - i.MX8MQ EVK
> - i.MX8DXL/QM/QXP EVK
> - i.MX95 15x15/19x19 EVK
> 
> The driver maintains legacy regulator handling for device trees that
> haven't been updated yet. Both old and new device tree structures are
> supported.
> 

Thanks for the work! Due to some recently merged patches, this series (Patch 1)
doesn't apply on top of pci/controller/dwc-imx6 branch. Please rebase and
resend!

- Mani

> [1] https://lore.kernel.org/all/20260318062916.2747472-1-sherry.sun@nxp.com/
> 
> Signed-off-by: Sherry Sun <sherry.sun@nxp.com>
> ---
> Changes in V2:
> 1. After commit 2d8c5098b847 ("PCI/pwrctrl: Do not power off on pwrctrl
>    device removal"), the pwrctrl drivers no longer power off devices
>    during removal. Update pci-imx6 driver's shutdown callback in patch#1
>    to explicitly call pci_pwrctrl_power_off_devices() before 
>    pci_pwrctrl_destroy_devices() to ensure devices are properly powered
>    off.
> ---
> 
> Sherry Sun (8):
>   PCI: imx6: Integrate new pwrctrl API for pci-imx6
>   arm: dts: imx6qdl-sabresd: Move power supply property to Root Port
>     node
>   arm: dts: imx6sx-sdb: Move power supply property to Root Port node
>   arm64: dts: imx8mm-evk: Move power supply property to Root Port node
>   arm64: dts: imx8mp-evk: Move power supply properties to Root Port node
>   arm64: dts: imx8mq-evk: Move power supply properties to Root Port node
>   arm64: dts: imx8dxl/qm/qxp: Move power supply properties to Root Port
>     node
>   arm64: dts: imx95: Move power supply properties to Root Port node
> 
>  .../arm/boot/dts/nxp/imx/imx6qdl-sabresd.dtsi |  2 +-
>  arch/arm/boot/dts/nxp/imx/imx6sx-sdb.dtsi     |  2 +-
>  arch/arm64/boot/dts/freescale/imx8dxl-evk.dts |  4 ++--
>  arch/arm64/boot/dts/freescale/imx8mm-evk.dtsi |  2 +-
>  arch/arm64/boot/dts/freescale/imx8mp-evk.dts  |  4 ++--
>  arch/arm64/boot/dts/freescale/imx8mq-evk.dts  |  4 ++--
>  arch/arm64/boot/dts/freescale/imx8qm-mek.dts  |  4 ++--
>  arch/arm64/boot/dts/freescale/imx8qxp-mek.dts |  4 ++--
>  .../boot/dts/freescale/imx95-15x15-evk.dts    |  4 ++--
>  .../boot/dts/freescale/imx95-19x19-evk.dts    |  8 +++----
>  drivers/pci/controller/dwc/Kconfig            |  1 +
>  drivers/pci/controller/dwc/pci-imx6.c         | 24 ++++++++++++++++++-
>  12 files changed, 43 insertions(+), 20 deletions(-)
> 
> -- 
> 2.37.1
> 

-- 
மணிவண்ணன் சதாசிவம்

^ permalink raw reply

* Re: [PATCH 0/3] Move adis16203 inclinometer driver out of staging
From: David Lechner @ 2026-04-04 16:29 UTC (permalink / raw)
  To: Sheng Kun Chang, jic23
  Cc: lars, Michael.Hennerich, nuno.sa, andy, gregkh, robh, krzk+dt,
	conor+dt, linux-iio, linux-staging, devicetree, linux-kernel
In-Reply-To: <20260401162458.88110-1-nothingchang@mirrorstack.ai>

On 4/1/26 11:24 AM, Sheng Kun Chang wrote:
> This series moves the ADIS16203 Programmable 360 Degrees Inclinometer
> driver out of staging and into drivers/iio/accel/.
> 
> The driver already uses standard IIO channel interfaces and devm
> managed APIs. The only missing piece was devicetree binding
> documentation, which is added in patch 1.
> 
> Patch 1: Add devicetree binding documentation
> Patch 2: Fix MODULE_LICENSE to match SPDX identifier
> Patch 3: Move the driver from staging to drivers/iio/accel/
> 
> Sheng Kun Chang (3):
>   dt-bindings: iio: accel: add binding for adi,adis16203
>   staging: iio: adis16203: align MODULE_LICENSE with SPDX identifier
>   iio: accel: move adis16203 out of staging
> 
>  .../bindings/iio/accel/adi,adis16203.yaml     | 52 +++++++++++++++++++
>  drivers/iio/accel/Kconfig                     | 12 +++++
>  drivers/iio/accel/Makefile                    |  1 +
>  drivers/{staging => }/iio/accel/adis16203.c   |  2 +-
>  drivers/staging/iio/Kconfig                   |  1 -
>  drivers/staging/iio/Makefile                  |  1 -
>  drivers/staging/iio/accel/Kconfig             | 19 -------
>  drivers/staging/iio/accel/Makefile            |  6 ---
>  8 files changed, 66 insertions(+), 28 deletions(-)
>  create mode 100644 Documentation/devicetree/bindings/iio/accel/adi,adis16203.yaml
>  rename drivers/{staging => }/iio/accel/adis16203.c (99%)
>  delete mode 100644 drivers/staging/iio/accel/Kconfig
>  delete mode 100644 drivers/staging/iio/accel/Makefile
> 

Quite a few people have attempted this already [1]. It is likely not as simple
as renaming the file. Please review and give a summary of all of the previous
attempts so that all of us reviewers don't have to go read all of those messages.

[1]: https://lore.kernel.org/linux-iio/?q=adis16203

And when reading those messages, you should find that the rename patch needs
to have the full diff (I think this is the --no-renames option to git
format-patch). This way we can review the driver in it's current state one
more time to make sure we didn't miss any cleanups that are needed first.

^ permalink raw reply

* Re: [PATCH v2 2/3] arm64: dts: qcom: kaanpaali: Add USB support for MTP platform
From: Akhil P Oommen @ 2026-04-04 16:11 UTC (permalink / raw)
  To: Dmitry Baryshkov, Krishna Kurapati
  Cc: linux-arm-msm, devicetree, linux-kernel, Ronak Raheja,
	Jingyi Wang, Konrad Dybcio, Bjorn Andersson, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
In-Reply-To: <n5pxrfwgqdg62p5s7mgxmvx64o4mon3wlys3nxzjlcq5v4o6bh@3gl5dtfctmvp>

On 4/4/2026 1:50 AM, Dmitry Baryshkov wrote:
> On Sat, Apr 04, 2026 at 01:39:50AM +0530, Akhil P Oommen wrote:
>> On 3/29/2026 11:22 PM, Krishna Kurapati wrote:
>>> From: Ronak Raheja <ronak.raheja@oss.qualcomm.com>
>>>
>>> Enable USB support on Kaanapali MTP variant. Enable USB controller in
>>> device mode till glink node is added.
>>>
>>> Signed-off-by: Ronak Raheja <ronak.raheja@oss.qualcomm.com>
>>> Signed-off-by: Jingyi Wang <jingyi.wang@oss.qualcomm.com>
>>> Signed-off-by: Krishna Kurapati <krishna.kurapati@oss.qualcomm.com>
>>> ---
>>>  arch/arm64/boot/dts/qcom/kaanapali-mtp.dts | 27 ++++++++++++++++++++++
>>>  1 file changed, 27 insertions(+)
>>>
>>> +
>>> +&usb {
>>> +	dr_mode = "peripheral";
>>
>> I can see that the usb port in the MTP support 'host' mode too. Should
>> this be 'otg'?
> 
> It's stated in the commit message: OTG requires glink, which is not
> available yet.

If the issue is with switching between host vs device mode, isn't it
more useful to use "host" here? 'Host' mode does work on this device and
I use that to connect an ethernet dongle to ssh.

-Akhil.

> 
> 


^ permalink raw reply

* Re: [PATCH v6 4/4] iio: adc: ad4691: add SPI offload support
From: David Lechner @ 2026-04-04 15:57 UTC (permalink / raw)
  To: radu.sabau, Lars-Peter Clausen, Michael Hennerich,
	Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
	Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
	Philipp Zabel, Jonathan Corbet, Shuah Khan
  Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
	linux-doc
In-Reply-To: <20260403-ad4692-multichannel-sar-adc-driver-v6-4-fa2a01a57c4e@analog.com>

On 4/3/26 6:03 AM, Radu Sabau via B4 Relay wrote:
> From: Radu Sabau <radu.sabau@analog.com>
> 
> Add SPI offload support to enable DMA-based, CPU-independent data
> acquisition using the SPI Engine offload framework.
> 
> When an SPI offload is available (devm_spi_offload_get() succeeds),
> the driver registers a DMA engine IIO buffer and uses dedicated buffer
> setup operations. If no offload is available the existing software
> triggered buffer path is used unchanged.
> 
> Both CNV Burst Mode and Manual Mode support offload, but use different
> trigger mechanisms:
> 
> CNV Burst Mode: the SPI Engine is triggered by the ADC's DATA_READY
> signal on the GP pin specified by the trigger-source consumer reference
> in the device tree (one cell = GP pin number 0-3). For this mode the
> driver acts as both an SPI offload consumer (DMA RX stream, message
> optimization) and a trigger source provider: it registers the
> GP/DATA_READY output via devm_spi_offload_trigger_register() so the
> offload framework can match the '#trigger-source-cells' phandle and
> automatically fire the SPI Engine DMA transfer at end-of-conversion.
> 
> Manual Mode: the SPI Engine is triggered by a periodic trigger at
> the configured sampling frequency. The pre-built SPI message uses
> the pipelined CNV-on-CS protocol: N+1 4-byte transfers are issued
> for N active channels (the first result is discarded as garbage from
> the pipeline flush) and the remaining N results are captured by DMA.
> 
> All offload transfers use 32-bit frames (bits_per_word=32, len=4) for
> DMA word alignment. This patch promotes the channel scan_type from
> storagebits=16 (triggered-buffer path) to storagebits=32 to match the
> DMA word size; the triggered-buffer paths are updated to the same layout
> for consistency. CNV Burst Mode channel data arrives in the lower 16
> bits of the 32-bit word (shift=0); Manual Mode data arrives in the upper
> 16 bits (shift=16), matching the 4-byte SPI transfer layout
> [data_hi, data_lo, 0, 0]. A separate ad4691_manual_channels[] array
> encodes the shift=16 scan type for manual mode.
> 
> Add driver documentation under Documentation/iio/ad4691.rst covering
> operating modes, oversampling, reference voltage, SPI offload paths,
> and buffer data layout; register in MAINTAINERS and index.rst
> 
> Kconfig gains a dependency on IIO_BUFFER_DMAENGINE.
> 
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
> ---
>  Documentation/iio/ad4691.rst | 259 ++++++++++++++++++++++++++
>  Documentation/iio/index.rst  |   1 +
>  MAINTAINERS                  |   1 +
>  drivers/iio/adc/Kconfig      |   1 +
>  drivers/iio/adc/ad4691.c     | 422 ++++++++++++++++++++++++++++++++++++++++++-
>  5 files changed, 676 insertions(+), 8 deletions(-)
> 

...

>  ANALOG DEVICES INC AD4695 DRIVER
> diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
> index d498f16c0816..93f090e9a562 100644
> --- a/drivers/iio/adc/Kconfig
> +++ b/drivers/iio/adc/Kconfig
> @@ -144,6 +144,7 @@ config AD4691
>  	depends on SPI
>  	select IIO_BUFFER
>  	select IIO_TRIGGERED_BUFFER
> +	select IIO_BUFFER_DMAENGINE
>  	select REGMAP
>  	help
>  	  Say yes here to build support for Analog Devices AD4691 Family MuxSAR
> diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
> index f2a7273e43b9..cc2138e47feb 100644
> --- a/drivers/iio/adc/ad4691.c
> +++ b/drivers/iio/adc/ad4691.c
> @@ -11,6 +11,7 @@
>  #include <linux/delay.h>
>  #include <linux/dev_printk.h>
>  #include <linux/device/devres.h>
> +#include <linux/dmaengine.h>
>  #include <linux/err.h>
>  #include <linux/interrupt.h>
>  #include <linux/math.h>
> @@ -22,10 +23,14 @@
>  #include <linux/regulator/consumer.h>
>  #include <linux/reset.h>
>  #include <linux/spi/spi.h>
> +#include <linux/spi/offload/consumer.h>
> +#include <linux/spi/offload/provider.h>
>  #include <linux/units.h>
>  #include <linux/unaligned.h>
>  
>  #include <linux/iio/buffer.h>
> +#include <linux/iio/buffer-dma.h>
> +#include <linux/iio/buffer-dmaengine.h>
>  #include <linux/iio/iio.h>
>  #include <linux/iio/sysfs.h>
>  #include <linux/iio/trigger.h>
> @@ -40,6 +45,7 @@
>  #define AD4691_VREF_4P096_uV_MAX		4500000
>  
>  #define AD4691_CNV_DUTY_CYCLE_NS		380
> +#define AD4691_CNV_HIGH_TIME_NS			430
>  
>  #define AD4691_SPI_CONFIG_A_REG			0x000
>  #define AD4691_SW_RESET				(BIT(7) | BIT(0))
> @@ -92,6 +98,8 @@
>  #define AD4691_ACC_IN(n)			(0x252 + (3 * (n)))
>  #define AD4691_ACC_STS_DATA(n)			(0x283 + (4 * (n)))
>  
> +#define AD4691_OFFLOAD_BITS_PER_WORD		32
> +
>  static const char * const ad4691_supplies[] = { "avdd", "vio" };
>  
>  enum ad4691_ref_ctrl {
> @@ -109,6 +117,31 @@ struct ad4691_chip_info {
>  	unsigned int max_rate;
>  };
>  
> +enum {
> +	AD4691_SCAN_TYPE_NORMAL,         /* triggered buffer:  storagebits=16, shift=0  */
> +	AD4691_SCAN_TYPE_OFFLOAD_CNV,    /* CNV burst offload: storagebits=32, shift=0  */
> +	AD4691_SCAN_TYPE_OFFLOAD_MANUAL, /* manual offload:    storagebits=32, shift=16 */
> +};
> +
> +static const struct iio_scan_type ad4691_scan_types[] = {
> +	[AD4691_SCAN_TYPE_NORMAL] = {
> +		.sign = 'u',
> +		.realbits = 16,
> +		.storagebits = 16,
> +	},
> +	[AD4691_SCAN_TYPE_OFFLOAD_CNV] = {
> +		.sign = 'u',
> +		.realbits = 16,
> +		.storagebits = 32,
> +	},
> +	[AD4691_SCAN_TYPE_OFFLOAD_MANUAL] = {
> +		.sign = 'u',
> +		.realbits = 16,
> +		.storagebits = 32,
> +		.shift = 16,
> +	},
> +};
> +
>  #define AD4691_CHANNEL(ch)						\
>  	{								\
>  		.type = IIO_VOLTAGE,					\
> @@ -122,11 +155,9 @@ struct ad4691_chip_info {
>  		.info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE),	\
>  		.channel = ch,						\
>  		.scan_index = ch,					\
> -		.scan_type = {						\
> -			.sign = 'u',					\
> -			.realbits = 16,					\
> -			.storagebits = 16,				\
> -		},							\
> +		.has_ext_scan_type = 1,					\
> +		.ext_scan_type = ad4691_scan_types,			\
> +		.num_ext_scan_type = ARRAY_SIZE(ad4691_scan_types),	\

Usually, we just make two separte ad4691_chip_info structs for offload
vs. not offload.

ext_scan_type is generally only used when the scan type can change
dynamically after probe.

>  	}
>  
>  static const struct iio_chan_spec ad4691_channels[] = {
> @@ -221,6 +252,17 @@ static const struct ad4691_chip_info ad4694_chip_info = {
>  	.max_rate = 1 * HZ_PER_MHZ,
>  };
>  
> +struct ad4691_offload_state {
> +	struct spi_offload *spi;
> +	struct spi_offload_trigger *trigger;
> +	u64 trigger_hz;
> +	struct spi_message msg;
> +	/* Max 16 channel xfers + 1 state-reset or NOOP */
> +	struct spi_transfer xfer[17];
> +	u8 tx_cmd[17][4];
> +	u8 tx_reset[4];
> +};
> +
>  struct ad4691_state {
>  	const struct ad4691_chip_info *info;
>  	struct regmap *regmap;
> @@ -251,6 +293,8 @@ struct ad4691_state {
>  	struct spi_transfer *scan_xfers;
>  	__be16 *scan_tx;
>  	__be16 *scan_rx;
> +	/* NULL when no SPI offload hardware is present */
> +	struct ad4691_offload_state *offload;
>  	/* Scan buffer: one slot per channel plus timestamp */
>  	struct {
>  		u16 vals[16];
> @@ -273,6 +317,46 @@ static int ad4691_gpio_setup(struct ad4691_state *st, unsigned int gp_num)
>  				  AD4691_GP_MODE_DATA_READY << shift);
>  }
>  
> +static const struct spi_offload_config ad4691_offload_config = {
> +	.capability_flags = SPI_OFFLOAD_CAP_TRIGGER |
> +			    SPI_OFFLOAD_CAP_RX_STREAM_DMA,
> +};
> +
> +static bool ad4691_offload_trigger_match(struct spi_offload_trigger *trigger,
> +					 enum spi_offload_trigger_type type,
> +					 u64 *args, u32 nargs)
> +{
> +	return type == SPI_OFFLOAD_TRIGGER_DATA_READY &&
> +	       nargs == 1 && args[0] <= 3;
> +}
> +
> +static int ad4691_offload_trigger_request(struct spi_offload_trigger *trigger,
> +					  enum spi_offload_trigger_type type,
> +					  u64 *args, u32 nargs)
> +{
> +	struct ad4691_state *st = spi_offload_trigger_get_priv(trigger);
> +
> +	if (nargs != 1)
> +		return -EINVAL;
> +
> +	return ad4691_gpio_setup(st, (unsigned int)args[0]);

Should be fine to leave out the cast here.

> +}
> +
> +static int ad4691_offload_trigger_validate(struct spi_offload_trigger *trigger,
> +					   struct spi_offload_trigger_config *config)
> +{
> +	if (config->type != SPI_OFFLOAD_TRIGGER_DATA_READY)
> +		return -EINVAL;
> +
> +	return 0;
> +}
> +
> +static const struct spi_offload_trigger_ops ad4691_offload_trigger_ops = {
> +	.match    = ad4691_offload_trigger_match,
> +	.request  = ad4691_offload_trigger_request,
> +	.validate = ad4691_offload_trigger_validate,
> +};
> +
>  static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
>  {
>  	struct spi_device *spi = context;
> @@ -553,10 +637,17 @@ static int ad4691_read_raw(struct iio_dev *indio_dev,
>  	case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
>  		*val = st->osr[chan->scan_index];
>  		return IIO_VAL_INT;
> -	case IIO_CHAN_INFO_SCALE:
> +	case IIO_CHAN_INFO_SCALE: {
> +		const struct iio_scan_type *scan_type;
> +
> +		scan_type = iio_get_current_scan_type(indio_dev, chan);
> +		if (IS_ERR(scan_type))
> +			return PTR_ERR(scan_type);
> +
>  		*val = st->vref_uV / (MICRO / MILLI);
> -		*val2 = chan->scan_type.realbits;
> +		*val2 = scan_type->realbits;
>  		return IIO_VAL_FRACTIONAL_LOG2;
> +	}
>  	default:
>  		return -EINVAL;
>  	}
> @@ -856,6 +947,213 @@ static const struct iio_buffer_setup_ops ad4691_cnv_burst_buffer_setup_ops = {
>  	.postdisable = &ad4691_cnv_burst_buffer_postdisable,
>  };
>  
> +static int ad4691_manual_offload_buffer_postenable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct ad4691_offload_state *offload = st->offload;
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct spi_device *spi = to_spi_device(dev);
> +	struct spi_offload_trigger_config config = {
> +		.type = SPI_OFFLOAD_TRIGGER_PERIODIC,
> +	};
> +	unsigned int bit, k;
> +	int ret;
> +
> +	ret = ad4691_enter_conversion_mode(st);
> +	if (ret)
> +		return ret;
> +
> +	memset(offload->xfer, 0, sizeof(offload->xfer));
> +
> +	/*
> +	 * N+1 transfers for N channels. Each CS-low period triggers
> +	 * a conversion AND returns the previous result (pipelined).
> +	 *   TX: [AD4691_ADC_CHAN(n), 0x00, 0x00, 0x00]
> +	 *   RX: [data_hi, data_lo, 0x00, 0x00]   (shift=16)
> +	 * Transfer 0 RX is garbage; transfers 1..N carry real data.
> +	 */
> +	k = 0;
> +	iio_for_each_active_channel(indio_dev, bit) {
> +		offload->tx_cmd[k][0] = AD4691_ADC_CHAN(bit);
> +		offload->xfer[k].tx_buf = offload->tx_cmd[k];
> +		offload->xfer[k].len = sizeof(offload->tx_cmd[k]);
> +		offload->xfer[k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
> +		offload->xfer[k].cs_change = 1;
> +		offload->xfer[k].cs_change_delay.value = AD4691_CNV_HIGH_TIME_NS;
> +		offload->xfer[k].cs_change_delay.unit = SPI_DELAY_UNIT_NSECS;
> +		/* First transfer RX is garbage — skip it. */
> +		if (k > 0)
> +			offload->xfer[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
> +		k++;
> +	}
> +
> +	/* Final NOOP to flush pipeline and capture last channel. */
> +	offload->tx_cmd[k][0] = AD4691_NOOP;
> +	offload->xfer[k].tx_buf = offload->tx_cmd[k];
> +	offload->xfer[k].len = sizeof(offload->tx_cmd[k]);
> +	offload->xfer[k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
> +	offload->xfer[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
> +	k++;
> +
> +	spi_message_init_with_transfers(&offload->msg, offload->xfer, k);
> +	offload->msg.offload = offload->spi;
> +
> +	ret = spi_optimize_message(spi, &offload->msg);
> +	if (ret)
> +		goto err_exit_conversion;
> +
> +	config.periodic.frequency_hz = offload->trigger_hz;
> +	ret = spi_offload_trigger_enable(offload->spi, offload->trigger, &config);
> +	if (ret)
> +		goto err_unoptimize;
> +
> +	return 0;
> +
> +err_unoptimize:
> +	spi_unoptimize_message(&offload->msg);
> +err_exit_conversion:
> +	ad4691_exit_conversion_mode(st);
> +	return ret;
> +}
> +
> +static int ad4691_manual_offload_buffer_predisable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct ad4691_offload_state *offload = st->offload;
> +
> +	spi_offload_trigger_disable(offload->spi, offload->trigger);
> +	spi_unoptimize_message(&offload->msg);
> +
> +	return ad4691_exit_conversion_mode(st);
> +}
> +
> +static const struct iio_buffer_setup_ops ad4691_manual_offload_buffer_setup_ops = {
> +	.postenable = &ad4691_manual_offload_buffer_postenable,
> +	.predisable = &ad4691_manual_offload_buffer_predisable,
> +};
> +
> +static int ad4691_cnv_burst_offload_buffer_postenable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct ad4691_offload_state *offload = st->offload;
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct spi_device *spi = to_spi_device(dev);
> +	struct spi_offload_trigger_config config = {
> +		.type = SPI_OFFLOAD_TRIGGER_DATA_READY,
> +	};
> +	unsigned int n_active = bitmap_weight(indio_dev->active_scan_mask,
> +					      iio_get_masklength(indio_dev));
> +	unsigned int bit, k;
> +	int ret;
> +
> +	ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
> +			   bitmap_read(indio_dev->active_scan_mask, 0,
> +				       iio_get_masklength(indio_dev)));
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
> +			   ~bitmap_read(indio_dev->active_scan_mask, 0,
> +				iio_get_masklength(indio_dev)) & GENMASK(15, 0));
> +	if (ret)
> +		return ret;
> +
> +	iio_for_each_active_channel(indio_dev, bit) {
> +		ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(bit),
> +				   st->osr[bit]);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	ret = ad4691_enter_conversion_mode(st);
> +	if (ret)
> +		return ret;
> +
> +	memset(offload->xfer, 0, sizeof(offload->xfer));
> +
> +	/*
> +	 * N transfers to read N AVG_IN registers plus one state-reset
> +	 * transfer (no RX) to re-arm DATA_READY.
> +	 *   TX: [reg_hi | 0x80, reg_lo, 0x00, 0x00]
> +	 *   RX: [0x00, 0x00, data_hi, data_lo]   (shift=0)
> +	 */
> +	k = 0;
> +	iio_for_each_active_channel(indio_dev, bit) {
> +		unsigned int reg = AD4691_AVG_IN(bit);
> +
> +		offload->tx_cmd[k][0] = (reg >> 8) | 0x80;
> +		offload->tx_cmd[k][1] = reg & 0xFF;

Can we use put_unaligned_be16()?

> +		offload->xfer[k].tx_buf = offload->tx_cmd[k];
> +		offload->xfer[k].len = sizeof(offload->tx_cmd[k]);
> +		offload->xfer[k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
> +		offload->xfer[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
> +		if (k < n_active - 1)
> +			offload->xfer[k].cs_change = 1;
> +		k++;
> +	}
> +
> +	/* State reset to re-arm DATA_READY for the next scan. */
> +	offload->tx_reset[0] = AD4691_STATE_RESET_REG >> 8;
> +	offload->tx_reset[1] = AD4691_STATE_RESET_REG & 0xFF;

ditto.

> +	offload->tx_reset[2] = AD4691_STATE_RESET_ALL;
> +	offload->xfer[k].tx_buf = offload->tx_reset;
> +	offload->xfer[k].len = sizeof(offload->tx_reset);
> +	offload->xfer[k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
> +	k++;
> +
> +	spi_message_init_with_transfers(&offload->msg, offload->xfer, k);
> +	offload->msg.offload = offload->spi;
> +
> +	ret = spi_optimize_message(spi, &offload->msg);
> +	if (ret)
> +		goto err_exit_conversion;
> +
> +	ret = ad4691_sampling_enable(st, true);
> +	if (ret)
> +		goto err_unoptimize;
> +
> +	ret = spi_offload_trigger_enable(offload->spi, offload->trigger, &config);
> +	if (ret)
> +		goto err_sampling_disable;
> +
> +	return 0;
> +
> +err_sampling_disable:
> +	ad4691_sampling_enable(st, false);
> +err_unoptimize:
> +	spi_unoptimize_message(&offload->msg);
> +err_exit_conversion:
> +	ad4691_exit_conversion_mode(st);
> +	return ret;
> +}
> +
> +static int ad4691_cnv_burst_offload_buffer_predisable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct ad4691_offload_state *offload = st->offload;
> +	int ret;
> +
> +	spi_offload_trigger_disable(offload->spi, offload->trigger);
> +
> +	ret = ad4691_sampling_enable(st, false);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
> +			   AD4691_SEQ_ALL_CHANNELS_OFF);
> +	if (ret)
> +		return ret;
> +
> +	spi_unoptimize_message(&offload->msg);
> +
> +	return ad4691_exit_conversion_mode(st);
> +}
> +
> +static const struct iio_buffer_setup_ops ad4691_cnv_burst_offload_buffer_setup_ops = {
> +	.postenable = &ad4691_cnv_burst_offload_buffer_postenable,
> +	.predisable = &ad4691_cnv_burst_offload_buffer_predisable,
> +};
> +
>  static ssize_t sampling_frequency_show(struct device *dev,
>  				       struct device_attribute *attr,
>  				       char *buf)
> @@ -863,6 +1161,9 @@ static ssize_t sampling_frequency_show(struct device *dev,
>  	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
>  	struct ad4691_state *st = iio_priv(indio_dev);
>  
> +	if (st->manual_mode && st->offload)
> +		return sysfs_emit(buf, "%llu\n", st->offload->trigger_hz);
> +
>  	return sysfs_emit(buf, "%u\n", (u32)(NSEC_PER_SEC / st->cnv_period_ns));
>  }
>  
> @@ -883,6 +1184,20 @@ static ssize_t sampling_frequency_store(struct device *dev,
>  	if (iio_buffer_enabled(indio_dev))
>  		return -EBUSY;
>  
> +	if (st->manual_mode && st->offload) {
> +		struct spi_offload_trigger_config config = {
> +			.type = SPI_OFFLOAD_TRIGGER_PERIODIC,
> +			.periodic = { .frequency_hz = freq },
> +		};

Same comment as other patches. This needs to account for oversampling ratio.

> +
> +		ret = spi_offload_trigger_validate(st->offload->trigger, &config);
> +		if (ret)
> +			return ret;
> +
> +		st->offload->trigger_hz = config.periodic.frequency_hz;
> +		return len;
> +	}
> +
>  	ret = ad4691_set_pwm_freq(st, freq);
>  	if (ret)
>  		return ret;
> @@ -968,10 +1283,23 @@ static irqreturn_t ad4691_trigger_handler(int irq, void *p)
>  	return IRQ_HANDLED;
>  }
>  
> +static int ad4691_get_current_scan_type(const struct iio_dev *indio_dev,
> +					 const struct iio_chan_spec *chan)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +
> +	if (!st->offload)
> +		return AD4691_SCAN_TYPE_NORMAL;
> +	if (st->manual_mode)
> +		return AD4691_SCAN_TYPE_OFFLOAD_MANUAL;
> +	return AD4691_SCAN_TYPE_OFFLOAD_CNV;
> +}
> +
>  static const struct iio_info ad4691_info = {
>  	.read_raw = &ad4691_read_raw,
>  	.write_raw = &ad4691_write_raw,
>  	.read_avail = &ad4691_read_avail,
> +	.get_current_scan_type = &ad4691_get_current_scan_type,
>  	.debugfs_reg_access = &ad4691_reg_access,
>  };
>  
> @@ -1195,9 +1523,75 @@ static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
>  					       &ad4691_manual_buffer_setup_ops);
>  }
>  
> +static int ad4691_setup_offload(struct iio_dev *indio_dev,
> +				struct ad4691_state *st,
> +				struct spi_offload *spi_offload)
> +{
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct ad4691_offload_state *offload;
> +	struct dma_chan *rx_dma;
> +	int ret;
> +
> +	offload = devm_kzalloc(dev, sizeof(*offload), GFP_KERNEL);
> +	if (!offload)
> +		return -ENOMEM;
> +
> +	offload->spi = spi_offload;
> +	st->offload = offload;
> +
> +	if (st->manual_mode) {
> +		offload->trigger =
> +			devm_spi_offload_trigger_get(dev, offload->spi,
> +						     SPI_OFFLOAD_TRIGGER_PERIODIC);
> +		if (IS_ERR(offload->trigger))
> +			return dev_err_probe(dev, PTR_ERR(offload->trigger),
> +					     "Failed to get periodic offload trigger\n");
> +
> +		offload->trigger_hz = st->info->max_rate;

I think I mentioned this elsewhere, but can we really get max_rate in manual mode
due to the extra SPI overhead? Probably safer to start with a lower rate.

> +	} else {
> +		struct spi_offload_trigger_info trigger_info = {
> +			.fwnode = dev_fwnode(dev),
> +			.ops    = &ad4691_offload_trigger_ops,
> +			.priv   = st,
> +		};
> +
> +		ret = devm_spi_offload_trigger_register(dev, &trigger_info);
> +		if (ret)
> +			return dev_err_probe(dev, ret,
> +					     "Failed to register offload trigger\n");
> +
> +		offload->trigger =
> +			devm_spi_offload_trigger_get(dev, offload->spi,
> +						     SPI_OFFLOAD_TRIGGER_DATA_READY);
> +		if (IS_ERR(offload->trigger))
> +			return dev_err_probe(dev, PTR_ERR(offload->trigger),
> +					     "Failed to get DATA_READY offload trigger\n");
> +	}
> +
> +	rx_dma = devm_spi_offload_rx_stream_request_dma_chan(dev, offload->spi);
> +	if (IS_ERR(rx_dma))
> +		return dev_err_probe(dev, PTR_ERR(rx_dma),
> +				     "Failed to get offload RX DMA channel\n");
> +
> +	if (st->manual_mode)
> +		indio_dev->setup_ops = &ad4691_manual_offload_buffer_setup_ops;
> +	else
> +		indio_dev->setup_ops = &ad4691_cnv_burst_offload_buffer_setup_ops;
> +
> +	ret = devm_iio_dmaengine_buffer_setup_with_handle(dev, indio_dev, rx_dma,
> +							  IIO_BUFFER_DIRECTION_IN);
> +	if (ret)
> +		return ret;
> +
> +	indio_dev->buffer->attrs = ad4691_buffer_attrs;

Should including ad4691_buffer_attrs depend on st->manual_mode?

I thought it was only used when PWM is connected to CNV.

> +
> +	return 0;
> +}
> +
>  static int ad4691_probe(struct spi_device *spi)
>  {
>  	struct device *dev = &spi->dev;
> +	struct spi_offload *spi_offload;
>  	struct iio_dev *indio_dev;
>  	struct ad4691_state *st;
>  	int ret;
> @@ -1232,6 +1626,13 @@ static int ad4691_probe(struct spi_device *spi)
>  	if (ret)
>  		return ret;
>  
> +	spi_offload = devm_spi_offload_get(dev, spi, &ad4691_offload_config);
> +	ret = PTR_ERR_OR_ZERO(spi_offload);
> +	if (ret == -ENODEV)
> +		spi_offload = NULL;
> +	else if (ret)
> +		return dev_err_probe(dev, ret, "Failed to get SPI offload\n");
> +
>  	indio_dev->name = st->info->name;
>  	indio_dev->info = &ad4691_info;
>  	indio_dev->modes = INDIO_DIRECT_MODE;
> @@ -1239,7 +1640,10 @@ static int ad4691_probe(struct spi_device *spi)
>  	indio_dev->channels = st->info->channels;
>  	indio_dev->num_channels = st->info->num_channels;

As mentioned earlier, we generally want separate channel structs
for SPI offload. These will also have different num_channels because
there is no timestamp channel in SPI offload.

>  
> -	ret = ad4691_setup_triggered_buffer(indio_dev, st);
> +	if (spi_offload)
> +		ret = ad4691_setup_offload(indio_dev, st, spi_offload);
> +	else
> +		ret = ad4691_setup_triggered_buffer(indio_dev, st);
>  	if (ret)
>  		return ret;
>  
> @@ -1277,3 +1681,5 @@ module_spi_driver(ad4691_driver);
>  MODULE_AUTHOR("Radu Sabau <radu.sabau@analog.com>");
>  MODULE_DESCRIPTION("Analog Devices AD4691 Family ADC Driver");
>  MODULE_LICENSE("GPL");
> +MODULE_IMPORT_NS("IIO_DMA_BUFFER");
> +MODULE_IMPORT_NS("IIO_DMAENGINE_BUFFER");
> 


^ permalink raw reply

* Re: [PATCH v6 4/4] iio: adc: ad4691: add SPI offload support
From: David Lechner @ 2026-04-04 15:34 UTC (permalink / raw)
  To: radu.sabau, Lars-Peter Clausen, Michael Hennerich,
	Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
	Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
	Philipp Zabel, Jonathan Corbet, Shuah Khan
  Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
	linux-doc
In-Reply-To: <20260403-ad4692-multichannel-sar-adc-driver-v6-4-fa2a01a57c4e@analog.com>

On 4/3/26 6:03 AM, Radu Sabau via B4 Relay wrote:
> From: Radu Sabau <radu.sabau@analog.com>
> 
> Add SPI offload support to enable DMA-based, CPU-independent data
> acquisition using the SPI Engine offload framework.
> 
> When an SPI offload is available (devm_spi_offload_get() succeeds),
> the driver registers a DMA engine IIO buffer and uses dedicated buffer
> setup operations. If no offload is available the existing software
> triggered buffer path is used unchanged.
> 
> Both CNV Burst Mode and Manual Mode support offload, but use different
> trigger mechanisms:
> 
> CNV Burst Mode: the SPI Engine is triggered by the ADC's DATA_READY
> signal on the GP pin specified by the trigger-source consumer reference
> in the device tree (one cell = GP pin number 0-3). For this mode the
> driver acts as both an SPI offload consumer (DMA RX stream, message
> optimization) and a trigger source provider: it registers the
> GP/DATA_READY output via devm_spi_offload_trigger_register() so the
> offload framework can match the '#trigger-source-cells' phandle and
> automatically fire the SPI Engine DMA transfer at end-of-conversion.
> 
> Manual Mode: the SPI Engine is triggered by a periodic trigger at
> the configured sampling frequency. The pre-built SPI message uses
> the pipelined CNV-on-CS protocol: N+1 4-byte transfers are issued
> for N active channels (the first result is discarded as garbage from
> the pipeline flush) and the remaining N results are captured by DMA.
> 
> All offload transfers use 32-bit frames (bits_per_word=32, len=4) for
> DMA word alignment. This patch promotes the channel scan_type from
> storagebits=16 (triggered-buffer path) to storagebits=32 to match the
> DMA word size; the triggered-buffer paths are updated to the same layout
> for consistency. CNV Burst Mode channel data arrives in the lower 16
> bits of the 32-bit word (shift=0); Manual Mode data arrives in the upper
> 16 bits (shift=16), matching the 4-byte SPI transfer layout
> [data_hi, data_lo, 0, 0]. A separate ad4691_manual_channels[] array
> encodes the shift=16 scan type for manual mode.
> 
> Add driver documentation under Documentation/iio/ad4691.rst covering
> operating modes, oversampling, reference voltage, SPI offload paths,
> and buffer data layout; register in MAINTAINERS and index.rst

Documentation should be separate patch. It covers more than just SPI
offload.

> 
> Kconfig gains a dependency on IIO_BUFFER_DMAENGINE.
> 
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
> ---
>  Documentation/iio/ad4691.rst | 259 ++++++++++++++++++++++++++
>  Documentation/iio/index.rst  |   1 +
>  MAINTAINERS                  |   1 +
>  drivers/iio/adc/Kconfig      |   1 +
>  drivers/iio/adc/ad4691.c     | 422 ++++++++++++++++++++++++++++++++++++++++++-
>  5 files changed, 676 insertions(+), 8 deletions(-)
> 
> diff --git a/Documentation/iio/ad4691.rst b/Documentation/iio/ad4691.rst
> new file mode 100644
> index 000000000000..36f0c841605a
> --- /dev/null
> +++ b/Documentation/iio/ad4691.rst
> @@ -0,0 +1,259 @@
> +.. SPDX-License-Identifier: GPL-2.0-only
> +
> +=============
> +AD4691 driver
> +=============
> +
> +ADC driver for Analog Devices Inc. AD4691 family of multichannel SAR ADCs.
> +The module name is ``ad4691``.
> +
> +
> +Supported devices
> +=================
> +
> +The following chips are supported by this driver:
> +
> +* `AD4691 <https://www.analog.com/en/products/ad4691.html>`_ — 16-channel, 500 kSPS
> +* `AD4692 <https://www.analog.com/en/products/ad4692.html>`_ — 16-channel, 1 MSPS
> +* `AD4693 <https://www.analog.com/en/products/ad4693.html>`_ — 8-channel, 500 kSPS
> +* `AD4694 <https://www.analog.com/en/products/ad4694.html>`_ — 8-channel, 1 MSPS
> +
> +
> +IIO channels
> +============
> +
> +Each physical ADC input maps to one IIO voltage channel. The AD4691 and AD4692
> +expose 16 channels (``voltage0`` through ``voltage15``); the AD4693 and AD4694
> +expose 8 channels (``voltage0`` through ``voltage7``).
> +
> +All channels share a common scale (``in_voltage_scale``), derived from the
> +reference voltage. Each channel independently exposes:
> +
> +* ``in_voltageN_raw`` — single-shot ADC result
> +* ``in_voltageN_sampling_frequency`` — internal oscillator frequency used for

As mentioned in another patch, sampling_frquency != osciallator frequency when
oversampling ratio != 1. So this needs to be changed to reflect that.

> +  single-shot reads and CNV Burst Mode buffered captures
> +* ``in_voltageN_sampling_frequency_available`` — list of valid oscillator
> +  frequencies
> +* ``in_voltageN_oversampling_ratio`` — per-channel hardware accumulation depth
> +* ``in_voltageN_oversampling_ratio_available`` — list of valid ratios
> +
> +
> +Operating modes
> +===============
> +
> +The driver supports two operating modes, auto-detected from the device tree at
> +probe time. Both modes transition to and from an internal Autonomous Mode idle
> +state when the IIO buffer is enabled and disabled.
> +
> +Manual Mode
> +-----------
> +
> +Selected when no ``pwms`` property is present in the device tree. The CNV pin
> +is tied to the SPI chip-select: every CS assertion both triggers a new
> +conversion and returns the result of the previous one (pipelined N+1 scheme).
> +
> +To read N channels the driver issues N+1 SPI transfers in a single optimised
> +message:
> +
> +* Transfers 0 to N-1 each carry ``AD4691_ADC_CHAN(n)`` in the TX byte to
> +  select the next channel; the RX byte of transfer ``k+1`` contains the result
> +  of the channel selected in transfer ``k``.
> +* Transfer N is a NOOP (0x00) to flush the last conversion result out of the
> +  pipeline.
> +
> +The external IIO trigger (``pollfunc_store_time``) drives the trigger handler,

I'm not sure "external" is the best word to describe this. I would say a "user-
defined IIO triger (e.g. hrtimer trigger)".

> +which executes the pre-built SPI message and pushes the scan to the buffer.
> +
> +CNV Burst Mode
> +--------------
> +
> +Selected when a ``pwms`` property is present in the device tree. The PWM drives
> +the CNV pin independently of SPI at the configured conversion rate, and a GP
> +pin (identified by ``interrupt-names``) asserts DATA_READY at end-of-burst to
> +signal that the AVG_IN result registers are ready to be read.
> +
> +The IRQ handler stops the PWM, fires the IIO trigger, and the trigger handler

If we stop the PWM after an IRQ, then we don't get a consistent sample rate.
Ideally, we would leave the PWM running and just pick a rate slow enough that
there is plenty of time to read the data. Otherwise, this mode doesn't seem
particularly useful.

> +reads all active ``AVG_IN(n)`` registers in a single optimised SPI message and
> +pushes the scan to the buffer.
> +
> +The buffer sampling frequency (i.e. the PWM rate) is controlled by the
> +``sampling_frequency`` attribute on the IIO buffer. Valid values span from the
> +chip's minimum oscillator rate up to its maximum conversion rate
> +(500 kSPS for AD4691/AD4693, 1 MSPS for AD4692/AD4694).

Valid, but not usable without SPI offload.

> +
> +Autonomous Mode (idle / single-shot)
> +-------------------------------------
> +
> +The chip idles in Autonomous Mode whenever the IIO buffer is disabled. In this
> +state, ``read_raw`` requests (``in_voltageN_raw``) use the internal oscillator
> +to perform a single conversion on the requested channel and read back the
> +result from the ``AVG_IN(N)`` register. The oscillator is started and stopped
> +for each read to save power.
> +
> +
> +Oversampling
> +============
> +
> +Each channel has an independent hardware accumulator (ACC_DEPTH_IN) that
> +averages a configurable number of successive conversions before DATA_READY
> +asserts. The result is always returned as a 16-bit mean from the ``AVG_IN``
> +register, so the IIO ``realbits`` and ``storagebits`` are unaffected by the
> +oversampling ratio.
> +
> +Valid ratios are 1, 2, 4, 8, 16 and 32. The default is 1 (no averaging).
> +
> +.. code-block:: bash
> +
> +    # Set oversampling ratio to 16 on channel 0
> +    echo 16 > /sys/bus/iio/devices/iio:device0/in_voltage0_oversampling_ratio
> +
> +When OSR > 1 the effective conversion rate for ``read_raw`` is reduced
> +accordingly, since the driver waits for 2 × OSR oscillator periods before
> +reading the result.
> +
> +
> +Reference voltage
> +=================
> +
> +The driver supports two reference configurations, mutually exclusive:
> +
> +* **External reference** (``ref-supply``): a voltage between 2.4 V and 5.25 V
> +  supplied externally. The internal reference buffer is disabled.
> +* **Buffered internal reference** (``refin-supply``): An internal reference
> +  buffer is used. The driver enables ``REFBUF_EN`` in the REF_CTRL register
> +  when this supply is used.
> +
> +Exactly one of ``ref-supply`` or ``refin-supply`` must be present in the
> +device tree.
> +
> +The reference voltage determines the full-scale range:
> +
> +.. code-block::
> +
> +    full-scale = Vref / 2^16  (per LSB)
> +
> +
> +LDO supply
> +==========
> +
> +The chip contains an internal LDO that powers part of the analog front-end.
> +The LDO input can be driven externally via the ``ldo-in-supply`` regulator. If
> +that supply is absent, the driver enables the internal LDO path (``LDO_EN``
> +bit in DEVICE_SETUP).
> +
> +
> +Reset
> +=====
> +
> +The driver supports two reset mechanisms:
> +
> +* **Hardware reset** (``reset-gpios`` in device tree): the GPIO is already
> +  asserted at driver probe by the reset controller framework. The driver waits
> +  for the required 300 µs reset pulse width and then deasserts.
> +* **Software reset** (fallback when ``reset-gpios`` is absent): the driver
> +  writes the software-reset pattern to the SPI_CONFIG_A register.
> +
> +
> +GP pins and interrupts
> +======================
> +
> +The chip exposes up to four general-purpose (GP) pins that can be configured as
> +interrupt outputs. In CNV Burst Mode (non-offload), one GP pin must be wired to

Or trigger sources.

> +an interrupt-capable SoC input and declared in the device tree using the
> +``interrupts`` and ``interrupt-names`` properties.
> +
> +The ``interrupt-names`` value identifies which GP pin is used (``"gp0"``
> +through ``"gp3"``). The driver configures that pin as a DATA_READY output in
> +the GPIO_MODE register.
> +
> +Example device tree fragment::
> +
> +    adc@0 {
> +        compatible = "adi,ad4692";
> +        ...
> +        interrupts = <17 IRQ_TYPE_LEVEL_HIGH>;
> +        interrupt-parent = <&gpio0>;
> +        interrupt-names = "gp0";
> +    };
> +
> +
> +SPI offload support
> +===================
> +
> +When a SPI offload engine (e.g. the AXI SPI Engine) is present, the driver
> +uses DMA-backed transfers for CPU-independent, high-throughput data capture.
> +SPI offload is detected automatically at probe via ``devm_spi_offload_get()``;
> +if no offload hardware is available the driver falls back to the software
> +triggered-buffer path.
> +
> +Two SPI offload sub-modes exist, corresponding to the two operating modes:
> +
> +CNV Burst offload
> +-----------------
> +
> +Used when a ``pwms`` property is present and SPI offload is available.
> +
> +The PWM drives CNV at the configured rate. On DATA_READY the SPI offload
> +engine automatically executes a pre-built message that reads all active
> +``AVG_IN`` registers and streams the data directly to an IIO DMA buffer with
> +no CPU involvement. A final state-reset transfer re-arms DATA_READY for the
> +next burst.
> +
> +The GP pin used as DATA_READY trigger is supplied by the trigger-source
> +consumer (via ``#trigger-source-cells``) at buffer enable time; no
> +``interrupt-names`` entry is required in this path.
> +
> +The buffer sampling frequency is controlled by the ``sampling_frequency``
> +attribute on the IIO buffer (same as the non-offload CNV Burst path).
> +
> +Manual offload
> +--------------
> +
> +Used when no ``pwms`` property is present and SPI offload is available.
> +
> +A periodic SPI offload trigger controls the conversion rate. On each trigger
> +period, the SPI engine executes an N+1 transfer message (same pipelined scheme

How does this work with oversampling?

> +as software Manual Mode) and streams the data directly to the IIO DMA buffer.
> +
> +The ``sampling_frequency`` attribute on the IIO buffer controls the trigger
> +rate (in Hz). The default is the chip's maximum conversion rate.
> +
> +
> +Buffer data format
> +==================
> +
> +The IIO buffer data format (``in_voltageN_type``) depends on the active path:
> +
> ++-------------------------+-------------+-------------+-------+
> +| Path                    | storagebits | realbits    | shift |
> ++=========================+=============+=============+=======+
> +| Triggered buffer        | 16          | 16          | 0     |
> ++-------------------------+-------------+-------------+-------+
> +| CNV Burst offload (DMA) | 32          | 16          | 0     |
> ++-------------------------+-------------+-------------+-------+
> +| Manual offload (DMA)    | 32          | 16          | 16    |
> ++-------------------------+-------------+-------------+-------+
> +
> +In the triggered-buffer path the driver unpacks the 16-bit result in software
> +before pushing to the buffer, so ``storagebits`` is 16.
> +
> +In the DMA offload paths the DMA engine writes 32-bit words directly into the
> +IIO DMA buffer:
> +
> +* **CNV Burst offload**: the SPI engine reads AVG_IN registers with a 2-byte
> +  address phase followed by a 2-byte data phase; the 16-bit result lands in
> +  the lower half of the 32-bit word (``shift=0``).
> +* **Manual offload**: each 32-bit SPI word carries the channel byte in the
> +  first byte; the 16-bit result is returned in the upper half of the 32-bit

I would expect the "first" byte to be in the "upper half" of the 32-bits as
well. This layout could be explained better.

Also, since extra data has to be read in this mode, does this affect the max
conversion rate?

> +  word (``shift=16``).
> +
> +The ``in_voltageN_type`` sysfs attribute reflects the active scan type.
> +
> +
> +Unimplemented features
> +======================
> +
> +* GPIO controller functionality of the GP pins
> +* Clamp status and overrange events
> +* Raw accumulator (ACC_IN) and accumulator status registers
> +* ADC_BUSY and overrun status interrupts

^ permalink raw reply

* Re: [PATCH v6 3/4] iio: adc: ad4691: add triggered buffer support
From: David Lechner @ 2026-04-04 15:12 UTC (permalink / raw)
  To: radu.sabau, Lars-Peter Clausen, Michael Hennerich,
	Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
	Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
	Philipp Zabel, Jonathan Corbet, Shuah Khan
  Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
	linux-doc
In-Reply-To: <20260403-ad4692-multichannel-sar-adc-driver-v6-3-fa2a01a57c4e@analog.com>

On 4/3/26 6:03 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 u16 slot per channel at its scan_index position, followed by a
> timestamp — to the IIO buffer via iio_push_to_buffers_with_ts().
> 
> The CNV Burst Mode sampling frequency (PWM period) is exposed as a
> buffer-level attribute via IIO_DEVICE_ATTR.
> 
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
> ---
>  drivers/iio/adc/Kconfig  |   2 +
>  drivers/iio/adc/ad4691.c | 592 ++++++++++++++++++++++++++++++++++++++++++++++-
>  2 files changed, 592 insertions(+), 2 deletions(-)
> 
> diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
> index 3685a03aa8dc..d498f16c0816 100644
> --- a/drivers/iio/adc/Kconfig
> +++ b/drivers/iio/adc/Kconfig
> @@ -142,6 +142,8 @@ config AD4170_4
>  config AD4691
>  	tristate "Analog Devices AD4691 Family ADC Driver"
>  	depends on SPI
> +	select IIO_BUFFER
> +	select IIO_TRIGGERED_BUFFER
>  	select REGMAP
>  	help
>  	  Say yes here to build support for Analog Devices AD4691 Family MuxSAR
> diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
> index 43bd408c3d11..f2a7273e43b9 100644
> --- a/drivers/iio/adc/ad4691.c
> +++ b/drivers/iio/adc/ad4691.c
> @@ -5,15 +5,19 @@
>   */
>  #include <linux/array_size.h>
>  #include <linux/bitfield.h>
> +#include <linux/bitmap.h>
>  #include <linux/bitops.h>
>  #include <linux/cleanup.h>
>  #include <linux/delay.h>
>  #include <linux/dev_printk.h>
>  #include <linux/device/devres.h>
>  #include <linux/err.h>
> +#include <linux/interrupt.h>
>  #include <linux/math.h>
>  #include <linux/module.h>
>  #include <linux/mod_devicetable.h>
> +#include <linux/property.h>
> +#include <linux/pwm.h>
>  #include <linux/regmap.h>
>  #include <linux/regulator/consumer.h>
>  #include <linux/reset.h>
> @@ -21,7 +25,12 @@
>  #include <linux/units.h>
>  #include <linux/unaligned.h>
>  
> +#include <linux/iio/buffer.h>
>  #include <linux/iio/iio.h>
> +#include <linux/iio/sysfs.h>
> +#include <linux/iio/trigger.h>
> +#include <linux/iio/triggered_buffer.h>
> +#include <linux/iio/trigger_consumer.h>
>  
>  #define AD4691_VREF_uV_MIN			2400000
>  #define AD4691_VREF_uV_MAX			5250000
> @@ -30,6 +39,8 @@
>  #define AD4691_VREF_3P3_uV_MAX			3750000
>  #define AD4691_VREF_4P096_uV_MAX		4500000
>  
> +#define AD4691_CNV_DUTY_CYCLE_NS		380
> +
>  #define AD4691_SPI_CONFIG_A_REG			0x000
>  #define AD4691_SW_RESET				(BIT(7) | BIT(0))
>  
> @@ -37,6 +48,7 @@
>  #define AD4691_CLAMP_STATUS1_REG		0x01A
>  #define AD4691_CLAMP_STATUS2_REG		0x01B
>  #define AD4691_DEVICE_SETUP			0x020
> +#define AD4691_MANUAL_MODE			BIT(2)
>  #define AD4691_LDO_EN				BIT(4)
>  #define AD4691_REF_CTRL				0x021
>  #define AD4691_REF_CTRL_MASK			GENMASK(4, 2)
> @@ -44,13 +56,18 @@
>  #define AD4691_OSC_FREQ_REG			0x023
>  #define AD4691_OSC_FREQ_MASK			GENMASK(3, 0)
>  #define AD4691_STD_SEQ_CONFIG			0x025
> +#define AD4691_SEQ_ALL_CHANNELS_OFF		0x00
>  #define AD4691_SPARE_CONTROL			0x02A
>  
> +#define AD4691_NOOP				0x00
> +#define AD4691_ADC_CHAN(ch)			((0x10 + (ch)) << 3)
> +
>  #define AD4691_OSC_EN_REG			0x180
>  #define AD4691_STATE_RESET_REG			0x181
>  #define AD4691_STATE_RESET_ALL			0x01
>  #define AD4691_ADC_SETUP			0x182
>  #define AD4691_ADC_MODE_MASK			GENMASK(1, 0)
> +#define AD4691_CNV_BURST_MODE			0x01
>  #define AD4691_AUTONOMOUS_MODE			0x02
>  /*
>   * ACC_MASK_REG covers both mask bytes via ADDR_DESCENDING SPI: writing a
> @@ -60,6 +77,8 @@
>  #define AD4691_ACC_DEPTH_IN(n)			(0x186 + (n))
>  #define AD4691_GPIO_MODE1_REG			0x196
>  #define AD4691_GPIO_MODE2_REG			0x197
> +#define AD4691_GP_MODE_MASK			GENMASK(3, 0)
> +#define AD4691_GP_MODE_DATA_READY		0x06
>  #define AD4691_GPIO_READ			0x1A0
>  #define AD4691_ACC_STATUS_FULL1_REG		0x1B0
>  #define AD4691_ACC_STATUS_FULL2_REG		0x1B1
> @@ -95,9 +114,11 @@ struct ad4691_chip_info {
>  		.type = IIO_VOLTAGE,					\
>  		.indexed = 1,						\
>  		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW)		\
> -				    | BIT(IIO_CHAN_INFO_SAMP_FREQ),	\
> +				    | BIT(IIO_CHAN_INFO_SAMP_FREQ)	\
> +				    | BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),	\
>  		.info_mask_separate_available =				\
> -				      BIT(IIO_CHAN_INFO_SAMP_FREQ),	\
> +				      BIT(IIO_CHAN_INFO_SAMP_FREQ)	\
> +				    | BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),	\
>  		.info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE),	\
>  		.channel = ch,						\
>  		.scan_index = ch,					\
> @@ -125,6 +146,7 @@ static const struct iio_chan_spec ad4691_channels[] = {
>  	AD4691_CHANNEL(13),
>  	AD4691_CHANNEL(14),
>  	AD4691_CHANNEL(15),
> +	IIO_CHAN_SOFT_TIMESTAMP(16),
>  };
>  
>  static const struct iio_chan_spec ad4693_channels[] = {
> @@ -136,6 +158,7 @@ static const struct iio_chan_spec ad4693_channels[] = {
>  	AD4691_CHANNEL(5),
>  	AD4691_CHANNEL(6),
>  	AD4691_CHANNEL(7),
> +	IIO_CHAN_SOFT_TIMESTAMP(8),
>  };
>  
>  /*
> @@ -162,6 +185,14 @@ static const int ad4691_osc_freqs_Hz[] = {
>  	[0xF] = 1250,
>  };
>  
> +static const char * const ad4691_gp_names[] = { "gp0", "gp1", "gp2", "gp3" };
> +
> +/*
> + * Valid ACC_DEPTH values where the effective divisor equals the count.
> + * From Table 13: ACC_DEPTH = 2^N yields right-shift = N, divisor = 2^N.
> + */
> +static const int ad4691_oversampling_ratios[] = { 1, 2, 4, 8, 16, 32 };

It would be nice to add oversampling in a separate commit as that is a
separate feature.

Oversampling also affects sampling frequency. When there isn't oversampling,
sample rate == conversion rate. However, with oversampling, sample rate ==
conversion rate / oversampling ratio (because each sample involves #OSR
conversions).

So more code will be required to make IIO_CHAN_INFO_SAMP_FREQ attributes
(both read/write_raw and read_avail) adjust the values based on the current
oversampling ratio.

> +
>  static const struct ad4691_chip_info ad4691_chip_info = {
>  	.channels = ad4691_channels,
>  	.name = "ad4691",
> @@ -193,16 +224,55 @@ static const struct ad4691_chip_info ad4694_chip_info = {
>  struct ad4691_state {
>  	const struct ad4691_chip_info *info;
>  	struct regmap *regmap;
> +
> +	struct pwm_device *conv_trigger;
> +	int irq;
> +
> +	bool manual_mode;
> +
>  	int vref_uV;
> +	u8 osr[16];
>  	bool refbuf_en;
>  	bool ldo_en;
> +	u32 cnv_period_ns;
>  	/*
>  	 * Synchronize access to members of the driver state, and ensure
>  	 * atomicity of consecutive SPI operations.
>  	 */
>  	struct mutex lock;
> +	/*
> +	 * Per-buffer-enable lifetime resources:
> +	 * Manual Mode - a pre-built SPI message that clocks out N+1
> +	 *		 transfers in one go.
> +	 * CNV Burst Mode - a pre-built SPI message that clocks out 2*N
> +	 *		    transfers in one go.
> +	 */
> +	struct spi_message scan_msg;
> +	struct spi_transfer *scan_xfers;
> +	__be16 *scan_tx;
> +	__be16 *scan_rx;

Why not embed these arrays here? Then we don't have to deal with
alloc/free later.

> +	/* Scan buffer: one slot per channel plus timestamp */
> +	struct {
> +		u16 vals[16];
> +		aligned_s64 ts;
> +	} scan __aligned(IIO_DMA_MINALIGN);

Better would be IIO_DECLARE_BUFFER_WITH_TS() since we don't always
use all vals.

Also, current usage doesn't need to be DMA-safe because scan_tx
is being used for the actual SPI xfer.

>  };
>  
> +/*
> + * Configure the given GP pin (0-3) as DATA_READY output.
> + * GP0/GP1 → GPIO_MODE1_REG, GP2/GP3 → GPIO_MODE2_REG.
> + * Even pins occupy bits [3:0], odd pins bits [7:4].
> + */
> +static int ad4691_gpio_setup(struct ad4691_state *st, unsigned int gp_num)
> +{
> +	unsigned int shift = 4 * (gp_num % 2);
> +
> +	return regmap_update_bits(st->regmap,
> +				  AD4691_GPIO_MODE1_REG + gp_num / 2,
> +				  AD4691_GP_MODE_MASK << shift,
> +				  AD4691_GP_MODE_DATA_READY << shift);
> +}
> +
>  static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
>  {
>  	struct spi_device *spi = context;
> @@ -362,6 +432,24 @@ static int ad4691_set_sampling_freq(struct iio_dev *indio_dev, int freq)
>  	return -EINVAL;
>  }
>  
> +static int ad4691_set_oversampling_ratio(struct iio_dev *indio_dev,
> +					 const struct iio_chan_spec *chan,
> +					 int osr)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +
> +	if (osr < 1 || osr > 32 || !is_power_of_2(osr))
> +		return -EINVAL;
> +
> +	IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
> +	if (IIO_DEV_ACQUIRE_FAILED(claim))
> +		return -EBUSY;
> +
> +	st->osr[chan->scan_index] = osr;
> +	return regmap_write(st->regmap,
> +			    AD4691_ACC_DEPTH_IN(chan->scan_index), osr);
> +}
> +
>  static int ad4691_read_avail(struct iio_dev *indio_dev,
>  			     struct iio_chan_spec const *chan,
>  			     const int **vals, int *type,
> @@ -376,6 +464,11 @@ static int ad4691_read_avail(struct iio_dev *indio_dev,
>  		*type = IIO_VAL_INT;
>  		*length = ARRAY_SIZE(ad4691_osc_freqs_Hz) - start;
>  		return IIO_AVAIL_LIST;
> +	case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
> +		*vals = ad4691_oversampling_ratios;
> +		*type = IIO_VAL_INT;
> +		*length = ARRAY_SIZE(ad4691_oversampling_ratios);
> +		return IIO_AVAIL_LIST;
>  	default:
>  		return -EINVAL;
>  	}
> @@ -406,6 +499,11 @@ static int ad4691_single_shot_read(struct iio_dev *indio_dev,
>  	if (ret)
>  		return ret;
>  
> +	ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(chan->scan_index),
> +			   st->osr[chan->scan_index]);
> +	if (ret)
> +		return ret;
> +
>  	ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, &reg_val);
>  	if (ret)
>  		return ret;
> @@ -452,6 +550,9 @@ static int ad4691_read_raw(struct iio_dev *indio_dev,
>  	}
>  	case IIO_CHAN_INFO_SAMP_FREQ:
>  		return ad4691_get_sampling_freq(st, val);
> +	case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
> +		*val = st->osr[chan->scan_index];
> +		return IIO_VAL_INT;
>  	case IIO_CHAN_INFO_SCALE:
>  		*val = st->vref_uV / (MICRO / MILLI);
>  		*val2 = chan->scan_type.realbits;
> @@ -468,6 +569,8 @@ static int ad4691_write_raw(struct iio_dev *indio_dev,
>  	switch (mask) {
>  	case IIO_CHAN_INFO_SAMP_FREQ:
>  		return ad4691_set_sampling_freq(indio_dev, val);
> +	case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
> +		return ad4691_set_oversampling_ratio(indio_dev, chan, val);
>  	default:
>  		return -EINVAL;
>  	}
> @@ -486,6 +589,385 @@ static int ad4691_reg_access(struct iio_dev *indio_dev, unsigned int reg,
>  	return regmap_write(st->regmap, reg, writeval);
>  }
>  
> +static int ad4691_set_pwm_freq(struct ad4691_state *st, int freq)
> +{
> +	if (!freq)
> +		return -EINVAL;
> +
> +	st->cnv_period_ns = DIV_ROUND_UP(NSEC_PER_SEC, freq);
> +	return 0;
> +}
> +
> +static int ad4691_sampling_enable(struct ad4691_state *st, bool enable)
> +{
> +	struct pwm_state conv_state = {
> +		.period     = st->cnv_period_ns,
> +		.duty_cycle = AD4691_CNV_DUTY_CYCLE_NS,
> +		.polarity   = PWM_POLARITY_NORMAL,
> +		.enabled    = enable,
> +	};
> +
> +	return pwm_apply_might_sleep(st->conv_trigger, &conv_state);
> +}
> +
> +/*
> + * ad4691_enter_conversion_mode - Switch the chip to its buffer conversion mode.
> + *
> + * Configures the ADC hardware registers for the mode selected at probe
> + * (CNV_BURST or MANUAL). Called from buffer preenable before starting
> + * sampling. The chip is in AUTONOMOUS mode during idle (for read_raw).
> + */
> +static int ad4691_enter_conversion_mode(struct ad4691_state *st)
> +{
> +	int ret;
> +
> +	if (st->manual_mode)
> +		return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
> +					  AD4691_MANUAL_MODE, AD4691_MANUAL_MODE);
> +
> +	ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
> +				 AD4691_ADC_MODE_MASK, AD4691_CNV_BURST_MODE);
> +	if (ret)
> +		return ret;
> +
> +	return regmap_write(st->regmap, AD4691_STATE_RESET_REG,
> +			    AD4691_STATE_RESET_ALL);
> +}
> +
> +/*
> + * ad4691_exit_conversion_mode - Return the chip to AUTONOMOUS mode.
> + *
> + * Called from buffer postdisable to restore the chip to the
> + * idle state used by read_raw. Clears the sequencer and resets state.
> + */
> +static int ad4691_exit_conversion_mode(struct ad4691_state *st)
> +{
> +	if (st->manual_mode)
> +		return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
> +					  AD4691_MANUAL_MODE, 0);
> +
> +	return regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
> +				  AD4691_ADC_MODE_MASK, AD4691_AUTONOMOUS_MODE);
> +}
> +
> +static void ad4691_free_scan_bufs(struct ad4691_state *st)
> +{
> +	kfree(st->scan_xfers);
> +	kfree(st->scan_tx);
> +	kfree(st->scan_rx);
> +}
> +
> +static int ad4691_manual_buffer_preenable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct spi_device *spi = to_spi_device(dev);
> +	unsigned int n_active = bitmap_weight(indio_dev->active_scan_mask,
> +					      iio_get_masklength(indio_dev));
> +	unsigned int n_xfers = n_active + 1;
> +	unsigned int k, i;
> +	int ret;
> +
> +	st->scan_xfers = kcalloc(n_xfers, sizeof(*st->scan_xfers), GFP_KERNEL);

Usually, we make st->scan_xfers a fixed array with the max number of possible
xfers. Then we don't have to deal with alloc/free.

> +	if (!st->scan_xfers)
> +		return -ENOMEM;
> +
> +	st->scan_tx = kcalloc(n_xfers, sizeof(*st->scan_tx), GFP_KERNEL);
> +	if (!st->scan_tx) {
> +		kfree(st->scan_xfers);
> +		return -ENOMEM;
> +	}
> +
> +	st->scan_rx = kcalloc(n_xfers, sizeof(*st->scan_rx), GFP_KERNEL);
> +	if (!st->scan_rx) {
> +		kfree(st->scan_tx);
> +		kfree(st->scan_xfers);
> +		return -ENOMEM;
> +	}
> +
> +	spi_message_init(&st->scan_msg);
> +
> +	k = 0;
> +	iio_for_each_active_channel(indio_dev, i) {
> +		st->scan_tx[k] = cpu_to_be16(AD4691_ADC_CHAN(i));
> +		st->scan_xfers[k].tx_buf = &st->scan_tx[k];
> +		st->scan_xfers[k].rx_buf = &st->scan_rx[k];
> +		st->scan_xfers[k].len = sizeof(__be16);
> +		st->scan_xfers[k].cs_change = 1;
> +		spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
> +		k++;
> +	}
> +
> +	/* Final NOOP transfer to retrieve last channel's result. */
> +	st->scan_tx[k] = cpu_to_be16(AD4691_NOOP);
> +	st->scan_xfers[k].tx_buf = &st->scan_tx[k];
> +	st->scan_xfers[k].rx_buf = &st->scan_rx[k];
> +	st->scan_xfers[k].len = sizeof(__be16);
> +	spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
> +
> +	st->scan_msg.spi = spi;

This isn't how the SPI framework is intended to be used. We should
have st->spi = spi in probe instead.

> +
> +	ret = spi_optimize_message(spi, &st->scan_msg);
> +	if (ret) {
> +		ad4691_free_scan_bufs(st);
> +		return ret;
> +	}
> +
> +	ret = ad4691_enter_conversion_mode(st);
> +	if (ret) {
> +		spi_unoptimize_message(&st->scan_msg);
> +		ad4691_free_scan_bufs(st);
> +		return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +static int ad4691_manual_buffer_postdisable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	int ret;
> +
> +	ret = ad4691_exit_conversion_mode(st);
> +	spi_unoptimize_message(&st->scan_msg);
> +	ad4691_free_scan_bufs(st);
> +	return ret;
> +}
> +
> +static const struct iio_buffer_setup_ops ad4691_manual_buffer_setup_ops = {
> +	.preenable = &ad4691_manual_buffer_preenable,
> +	.postdisable = &ad4691_manual_buffer_postdisable,
> +};
> +
> +static int ad4691_cnv_burst_buffer_preenable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct spi_device *spi = to_spi_device(dev);
> +	unsigned int n_active = bitmap_weight(indio_dev->active_scan_mask,
> +					      iio_get_masklength(indio_dev));
> +	unsigned int bit, k, i;
> +	int ret;
> +
> +	st->scan_xfers = kcalloc(2 * n_active, sizeof(*st->scan_xfers), GFP_KERNEL);
> +	if (!st->scan_xfers)
> +		return -ENOMEM;
> +
> +	st->scan_tx = kcalloc(n_active, sizeof(*st->scan_tx), GFP_KERNEL);
> +	if (!st->scan_tx) {
> +		kfree(st->scan_xfers);
> +		return -ENOMEM;
> +	}
> +
> +	st->scan_rx = kcalloc(n_active, sizeof(*st->scan_rx), GFP_KERNEL);
> +	if (!st->scan_rx) {
> +		kfree(st->scan_tx);
> +		kfree(st->scan_xfers);
> +		return -ENOMEM;
> +	}
> +
> +	spi_message_init(&st->scan_msg);
> +
> +	/*
> +	 * Each AVG_IN read needs two transfers: a 2-byte address write phase
> +	 * followed by a 2-byte data read phase. CS toggles between channels
> +	 * (cs_change=1 on the read phase of all but the last channel).
> +	 */
> +	k = 0;
> +	iio_for_each_active_channel(indio_dev, i) {
> +		st->scan_tx[k] = cpu_to_be16(0x8000 | AD4691_AVG_IN(i));
> +		st->scan_xfers[2 * k].tx_buf = &st->scan_tx[k];
> +		st->scan_xfers[2 * k].len = sizeof(__be16);
> +		spi_message_add_tail(&st->scan_xfers[2 * k], &st->scan_msg);
> +		st->scan_xfers[2 * k + 1].rx_buf = &st->scan_rx[k];
> +		st->scan_xfers[2 * k + 1].len = sizeof(__be16);
> +		if (k < n_active - 1)
> +			st->scan_xfers[2 * k + 1].cs_change = 1;
> +		spi_message_add_tail(&st->scan_xfers[2 * k + 1], &st->scan_msg);
> +		k++;
> +	}
> +
> +	st->scan_msg.spi = spi;
> +
> +	ret = spi_optimize_message(spi, &st->scan_msg);
> +	if (ret)
> +		goto err_free_bufs;
> +
> +	ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
> +			   bitmap_read(indio_dev->active_scan_mask, 0,
> +				       iio_get_masklength(indio_dev)));
> +	if (ret)
> +		goto err;
> +
> +	ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
> +			   ~bitmap_read(indio_dev->active_scan_mask, 0,
> +				iio_get_masklength(indio_dev)) & GENMASK(15, 0));
> +	if (ret)
> +		goto err;
> +
> +	iio_for_each_active_channel(indio_dev, bit) {
> +		ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(bit),
> +				   st->osr[bit]);
> +		if (ret)
> +			goto err;
> +	}
> +
> +	ret = ad4691_enter_conversion_mode(st);
> +	if (ret)
> +		goto err;
> +
> +	ret = ad4691_sampling_enable(st, true);
> +	if (ret)
> +		goto err;

Do we need to do something to exit conversion mode on error here?

> +
> +	enable_irq(st->irq);
> +	return 0;
> +err:
> +	spi_unoptimize_message(&st->scan_msg);
> +err_free_bufs:
> +	ad4691_free_scan_bufs(st);
> +	return ret;
> +}
> +
> +static int ad4691_cnv_burst_buffer_postdisable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	int ret;
> +
> +	disable_irq(st->irq);
> +
> +	ret = ad4691_sampling_enable(st, false);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
> +			   AD4691_SEQ_ALL_CHANNELS_OFF);
> +	if (ret)
> +		return ret;
> +

This order of unwinding is not the exact reverse of how it was
set up. So either the order needs to be fixed or a comment added
explaining why this order is needed instead.

> +	ret = ad4691_exit_conversion_mode(st);
> +	spi_unoptimize_message(&st->scan_msg);
> +	ad4691_free_scan_bufs(st);
> +	return ret;
> +}
> +
> +static const struct iio_buffer_setup_ops ad4691_cnv_burst_buffer_setup_ops = {
> +	.preenable = &ad4691_cnv_burst_buffer_preenable,
> +	.postdisable = &ad4691_cnv_burst_buffer_postdisable,
> +};
> +
> +static ssize_t sampling_frequency_show(struct device *dev,
> +				       struct device_attribute *attr,
> +				       char *buf)
> +{
> +	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +
> +	return sysfs_emit(buf, "%u\n", (u32)(NSEC_PER_SEC / st->cnv_period_ns));
> +}
> +
> +static ssize_t sampling_frequency_store(struct device *dev,
> +					struct device_attribute *attr,
> +					const char *buf, size_t len)
> +{
> +	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	int freq, ret;
> +
> +	ret = kstrtoint(buf, 10, &freq);
> +	if (ret)
> +		return ret;
> +
> +	guard(mutex)(&st->lock);
> +
> +	if (iio_buffer_enabled(indio_dev))

This should be using iio_device_claim_direct(), otherwise
it is racy.

> +		return -EBUSY;
> +
> +	ret = ad4691_set_pwm_freq(st, freq);
> +	if (ret)
> +		return ret;
> +
> +	return len;
> +}
> +
> +static IIO_DEVICE_ATTR(sampling_frequency, 0644,
> +		       sampling_frequency_show,
> +		       sampling_frequency_store, 0);
> +
> +static const struct iio_dev_attr *ad4691_buffer_attrs[] = {
> +	&iio_dev_attr_sampling_frequency,
> +	NULL
> +};
> +
> +static irqreturn_t ad4691_irq(int irq, void *private)
> +{
> +	struct iio_dev *indio_dev = private;
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +
> +	/*
> +	 * GPx has asserted: stop conversions before reading so the

Does this happen per-channel or only once per complete sequence?

> +	 * accumulator does not continue sampling while the trigger handler
> +	 * processes the data. Then fire the IIO trigger to push the sample
> +	 * to the buffer.
> +	 */
> +	ad4691_sampling_enable(st, false);
> +	iio_trigger_poll(indio_dev->trig);
> +
> +	return IRQ_HANDLED;
> +}
> +
> +static const struct iio_trigger_ops ad4691_trigger_ops = {
> +	.validate_device = iio_trigger_validate_own_device,
> +};
> +
> +static int ad4691_read_scan(struct iio_dev *indio_dev, s64 timestamp)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	unsigned int i, k = 0;
> +	int ret;
> +
> +	guard(mutex)(&st->lock);
> +
> +	ret = spi_sync(st->scan_msg.spi, &st->scan_msg);
> +	if (ret)
> +		return ret;
> +
> +	if (st->manual_mode) {
> +		iio_for_each_active_channel(indio_dev, i) {
> +			st->scan.vals[i] = be16_to_cpu(st->scan_rx[k + 1]);
> +			k++;
> +		}
> +	} else {
> +		iio_for_each_active_channel(indio_dev, i) {
> +			st->scan.vals[i] = be16_to_cpu(st->scan_rx[k]);
> +			k++;
> +		}

I suppose this is fine, but we usually try to avoid extra copiying and
byte swapping of bufferes like this if we can. It seems completly doable
in both modes. Manual mode will just one extra two-byte buffer for the
throw-away conversion on the first read xfer (or just write to the same
element twice).

> +
> +		ret = regmap_write(st->regmap, AD4691_STATE_RESET_REG,
> +				   AD4691_STATE_RESET_ALL);
> +		if (ret)
> +			return ret;
> +
> +		ret = ad4691_sampling_enable(st, true);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	iio_push_to_buffers_with_ts(indio_dev, &st->scan, sizeof(st->scan),
> +				    timestamp);
> +	return 0;
> +}
> +
> +static irqreturn_t ad4691_trigger_handler(int irq, void *p)
> +{
> +	struct iio_poll_func *pf = p;
> +	struct iio_dev *indio_dev = pf->indio_dev;
> +
> +	ad4691_read_scan(indio_dev, pf->timestamp);
> +	iio_trigger_notify_done(indio_dev->trig);
> +	return IRQ_HANDLED;
> +}
> +
>  static const struct iio_info ad4691_info = {
>  	.read_raw = &ad4691_read_raw,
>  	.write_raw = &ad4691_write_raw,
> @@ -493,6 +975,18 @@ static const struct iio_info ad4691_info = {
>  	.debugfs_reg_access = &ad4691_reg_access,
>  };
>  
> +static int ad4691_pwm_setup(struct ad4691_state *st)
> +{
> +	struct device *dev = regmap_get_device(st->regmap);
> +
> +	st->conv_trigger = devm_pwm_get(dev, "cnv");
> +	if (IS_ERR(st->conv_trigger))
> +		return dev_err_probe(dev, PTR_ERR(st->conv_trigger),
> +				     "Failed to get cnv pwm\n");
> +
> +	return ad4691_set_pwm_freq(st, st->info->max_rate);
> +}
> +
>  static int ad4691_regulator_setup(struct ad4691_state *st)
>  {
>  	struct device *dev = regmap_get_device(st->regmap);
> @@ -558,6 +1052,22 @@ static int ad4691_config(struct ad4691_state *st)
>  	unsigned int val;
>  	int ret;
>  
> +	/*
> +	 * Determine buffer conversion mode from DT: if a PWM is provided it
> +	 * drives the CNV pin (CNV_BURST_MODE); otherwise CNV is tied to CS
> +	 * and each SPI transfer triggers a conversion (MANUAL_MODE).
> +	 * Both modes idle in AUTONOMOUS mode so that read_raw can use the
> +	 * internal oscillator without disturbing the hardware configuration.
> +	 */
> +	if (device_property_present(dev, "pwms")) {
> +		st->manual_mode = false;
> +		ret = ad4691_pwm_setup(st);
> +		if (ret)
> +			return ret;
> +	} else {
> +		st->manual_mode = true;
> +	}
> +
>  	switch (st->vref_uV) {
>  	case AD4691_VREF_uV_MIN ... AD4691_VREF_2P5_uV_MAX:
>  		ref_val = AD4691_VREF_2P5;
> @@ -613,6 +1123,78 @@ static int ad4691_config(struct ad4691_state *st)
>  	return 0;
>  }
>  
> +static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
> +					 struct ad4691_state *st)
> +{
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct iio_trigger *trig;
> +	unsigned int i;
> +	int irq, ret;
> +
> +	trig = devm_iio_trigger_alloc(dev, "%s-dev%d",
> +				      indio_dev->name,
> +				      iio_device_id(indio_dev));
> +	if (!trig)
> +		return -ENOMEM;
> +
> +	trig->ops = &ad4691_trigger_ops;
> +	iio_trigger_set_drvdata(trig, st);
> +
> +	ret = devm_iio_trigger_register(dev, trig);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "IIO trigger register failed\n");
> +
> +	indio_dev->trig = iio_trigger_get(trig);
> +
> +	if (!st->manual_mode) {

I would invert the if since the other case is shorter.

> +		/*
> +		 * The GP pin named in interrupt-names asserts at end-of-conversion.
> +		 * The IRQ handler stops conversions and fires the IIO trigger so
> +		 * the trigger handler can read and push the sample to the buffer.
> +		 * The IRQ is kept disabled until the buffer is enabled.
> +		 */
> +		irq = -ENODEV;
> +		for (i = 0; i < ARRAY_SIZE(ad4691_gp_names); i++) {
> +			irq = fwnode_irq_get_byname(dev_fwnode(dev),
> +						    ad4691_gp_names[i]);
> +			if (irq > 0)
> +				break;
> +		}
> +		if (irq <= 0)
> +			return dev_err_probe(dev, irq < 0 ? irq : -ENODEV,
> +					     "failed to get GP interrupt\n");

Usually we would usually just use spi->irq since it already
has been looked up. But I guess it is OK to do it like this.

> +
> +		st->irq = irq;
> +
> +		ret = ad4691_gpio_setup(st, i);
> +		if (ret)
> +			return ret;
> +
> +		/*
> +		 * IRQ is kept disabled until the buffer is enabled to prevent
> +		 * spurious DATA_READY events before the SPI message is set up.
> +		 */
> +		ret = devm_request_threaded_irq(dev, irq, NULL,
> +						&ad4691_irq,
> +						IRQF_ONESHOT | IRQF_NO_AUTOEN,
> +						indio_dev->name, indio_dev);
> +		if (ret)
> +			return ret;
> +
> +		return devm_iio_triggered_buffer_setup_ext(dev, indio_dev,
> +							   &iio_pollfunc_store_time,
> +							   &ad4691_trigger_handler,
> +							   IIO_BUFFER_DIRECTION_IN,
> +							   &ad4691_cnv_burst_buffer_setup_ops,
> +							   ad4691_buffer_attrs);
> +	}
> +
> +	return devm_iio_triggered_buffer_setup(dev, indio_dev,
> +					       &iio_pollfunc_store_time,
> +					       &ad4691_trigger_handler,
> +					       &ad4691_manual_buffer_setup_ops);
> +}
> +

^ permalink raw reply

* Re: [PATCH 0/2] Add USB Ethernet controller (LAN7500) node in VAR-SOM-OM44 devicetree
From: Andrew Lunn @ 2026-04-04 14:59 UTC (permalink / raw)
  To: Jakub Kicinski
  Cc: Nicolai Buchwitz, Thomas Richard, Andrew Lunn, David S. Miller,
	Eric Dumazet, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Aaro Koskinen, Andreas Kemnade, Kevin Hilman,
	Roger Quadros, Tony Lindgren, Thomas Petazzoni, netdev,
	devicetree, linux-kernel, linux-omap
In-Reply-To: <20260403151500.5225681d@kernel.org>

On Fri, Apr 03, 2026 at 03:15:00PM -0700, Jakub Kicinski wrote:
> On Fri, 03 Apr 2026 22:33:26 +0200 Nicolai Buchwitz wrote:
> > On 3.4.2026 22:18, Jakub Kicinski wrote:
> > > On Fri, 03 Apr 2026 21:02:22 +0200 Thomas Richard wrote:  
> > >> The goal of this series is to add the USB Ethernet controller node in 
> > >> the
> > >> VAR-SOM-OM44 devicetree in order to allow the bootloader to patch the
> > >> devicetree and pass a MAC address to the smsc75xx driver. This was 
> > >> also a
> > >> good opportunity to create the schema file for LAN75XX devices.  
> > > 
> > > But there's no driver for it yet, right?
> > > IDK what the best practices are here, just unusual to see a schema
> > > without a driver, is all.  
> > 
> > The smsc75xx driver has been in the tree since 2010 (d0cad871703b) and
> > already reads local-mac-address/mac-address from devicetree via
> > platform_get_ethdev_address() in smsc75xx_init_mac_address(), so the
> > binding should be covered on the driver side.
> 
> Curious. So USB core can tie the right USB device to the DT / OF
> information automatically? I was thrown by the fact that there
> are no matches on the compatibles anywhere in the kernel.

The PCI core has similar capabilities. For both cases, there is no
compatibility matching going on, the hardware is enumerable and has
vendor:product information to match the device to the driver.

However the PCI and USB core can see that the device tree matches the
hardware tree, and so maps the device tree node to the driver.

This works well for devices soldered onto the board, but should be
avoided when there is a socket and anything could be plugged into it.

	Andrew

^ permalink raw reply

* Re: [PATCH 3/3] net: dsa: microchip: implement KSZ87xx Module 3 low-loss cable errata
From: Andrew Lunn @ 2026-04-04 14:45 UTC (permalink / raw)
  To: Fidelio LAWSON
  Cc: Woojung Huh, UNGLinuxDriver, Vladimir Oltean, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Marek Vasut, Maxime Chevallier,
	netdev, devicetree, linux-kernel, Fidelio Lawson
In-Reply-To: <fe14415a-52fa-42cc-9e4c-e3876588d513@gmail.com>

On Fri, Apr 03, 2026 at 11:43:24AM +0200, Fidelio LAWSON wrote:
> On 3/26/26 13:18, Andrew Lunn wrote:
> > > +	mutex_lock(&dev->alu_mutex);
> > > +
> > > +	ret = ksz_write8(dev, regs[REG_IND_CTRL_0], 0xA0);
> > > +
> > > +	if (!ret)
> > > +		ret = ksz_write8(dev, 0x6F, indir_reg);
> > > +
> > > +	if (!ret)
> > > +		ret = ksz_write8(dev, regs[REG_IND_BYTE], indir_val);
> > > +
> > > +	mutex_unlock(&dev->alu_mutex);
> > 
> > What address space are these registers in? Normally workarounds for a
> > PHY would be in the PHY driver. But that assumes the registers are
> > accessible from the PHY driver.
> > 
> > 	   Andrew
> 
> Hi Andrew,
> These registers belong to the KSZ87xx switch address space, accessed through
> the switch’s indirect access mechanism. In particular, the offsets used here
> correspond to entries within the TABLE_LINK_MD_V indirect table of the
> KSZ8-family switches.

So this errata is for ksz87xx only?

For this PHY, do all PHY register reads and writes go through

https://elixir.bootlin.com/linux/v6.19.11/source/drivers/net/dsa/microchip/ksz8.c#L957
ksz8_r_phy()

and

https://elixir.bootlin.com/linux/v6.19.11/source/drivers/net/dsa/microchip/ksz8.c#L1221
ksz8_w_phy()?

We already have some "interesting" things going on in these
functions. PHY_REG_LINK_MD and PHY_REG_PHY_CTRL are not standard C22
PHY registers. They take the values 0x1d and 0x1f. The 802.3 standard
defines 0x10-0x1f as vendor specific, so this is O.K.

So you could define 2 bits in say register 0x1c to indicate the errata
mode. You can have a PHY tunable which does reads/writes to these two
bits, and ksz8_w_phy/ksz8_r_phy which translates them to indirect
register accesses?

It is not even really violating the layering.

	 Andrew

^ permalink raw reply

* Re: [PATCH v6 2/4] iio: adc: ad4691: add initial driver for AD4691 family
From: David Lechner @ 2026-04-04 14:25 UTC (permalink / raw)
  To: radu.sabau, Lars-Peter Clausen, Michael Hennerich,
	Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
	Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
	Philipp Zabel, Jonathan Corbet, Shuah Khan
  Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
	linux-doc
In-Reply-To: <20260403-ad4692-multichannel-sar-adc-driver-v6-2-fa2a01a57c4e@analog.com>

On 4/3/26 6:03 AM, Radu Sabau via B4 Relay wrote:
> From: Radu Sabau <radu.sabau@analog.com>
> 
> Add support for the Analog Devices AD4691 family of high-speed,
> low-power multichannel SAR ADCs: AD4691 (16-ch, 500 kSPS),
> AD4692 (16-ch, 1 MSPS), AD4693 (8-ch, 500 kSPS) and
> AD4694 (8-ch, 1 MSPS).
> 
> The driver implements a custom regmap layer over raw SPI to handle the
> device's mixed 1/2/3/4-byte register widths and uses the standard IIO
> read_raw/write_raw interface for single-channel reads.
> 
> The chip idles in Autonomous Mode so that single-shot read_raw can use
> the internal oscillator without disturbing the hardware configuration.
> 
> Three voltage supply domains are managed: avdd (required), vio, and a
> reference supply on either the REF pin (ref-supply, external buffer)
> or the REFIN pin (refin-supply, uses the on-chip reference buffer;
> REFBUF_EN is set accordingly). Hardware reset is performed via
> the reset controller framework; a software reset through SPI_CONFIG_A
> is used as fallback when no hardware reset is available.
> 
> Accumulator channel masking for single-shot reads uses ACC_MASK_REG via
> an ADDR_DESCENDING SPI write, which covers both mask bytes in a single
> 16-bit transfer.
> 
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
> ---
This patch looks in good shape. Although I wouldn't mind using
MEGA/MICRO, etc. more in numbers with more than 3 or 4 zeros.

Reviewed-by: David Lechner <dlechner@baylibre.com>



^ permalink raw reply

* Re: [PATCH 1/3] dt-bindings: dsa: microchip: add KSZ low-loss cable errata properties
From: Andrew Lunn @ 2026-04-04 14:21 UTC (permalink / raw)
  To: Fidelio LAWSON
  Cc: Woojung Huh, UNGLinuxDriver, Vladimir Oltean, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Marek Vasut, Maxime Chevallier,
	netdev, devicetree, linux-kernel, Fidelio Lawson
In-Reply-To: <72c9a165-74bb-42a5-b5fe-67bfa2c8ce2e@gmail.com>

> Regarding the difference between the two workarounds:
> Microchip’s errata does provide some insight into how they behave and when
> each should be used.
> Workaround 1 modifies the PHY equalizer settings by adjusting an indirect
> register (0x3c).
> According to Microchip’s support article:

....


Thanks for the documentation. This needs to go somewhere.

Not sure where yet. If we stay with a DT setting, it should be in the
DT binding. If we make it a PHY tuneable, maybe a comment in the PHY
driver, and in the commit message?

> Regarding the question of whether this should be exposed through a PHY
> tunable:
> I understand your concern. The erratum is indeed linked to cable
> characteristics, not the board itself.
> Since this patch modifies registers that belong to the DSA switch itself,
> and not the PHY driver,

I need to go look at the code, but maybe we can make use of the fact
the PHY is embedded within the switch, rather than being a discrete
device. So we can safely break the layering, even if it is
architecturally wrong.

	Andrew

^ permalink raw reply

* Re: [PATCH 2/2] iio: dac: mcp47feb02: add MCP48FEB02 SPI driver to MCP47FEB02 I2C driver
From: David Lechner @ 2026-04-04 13:49 UTC (permalink / raw)
  To: Ariana Lazar, Jonathan Cameron, Nuno Sá, Andy Shevchenko,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley
  Cc: Jonathan Cameron, Conor Dooley, linux-iio, devicetree,
	linux-kernel
In-Reply-To: <20260403-mcp47feb02-fix2-v1-2-da60c773550e@microchip.com>

On 4/3/26 5:50 AM, Ariana Lazar wrote:
> This is the iio driver for Microchip MCP48FxBy1/2/4/8 series of
> buffered voltage output Digital-to-Analog Converters with nonvolatile or
> volatile memory on top of MCP47FEB02. The families support up to 8
> output channels and have 8-bit, 10-bit or 12-bit resolution.
> 
> The MCP47FEB02 driver was split into three modules: mcp47feb02-core.c,
> mcp47feb02-i2c.c and mcp47feb02-spi.c in order to support both DAC families
> - I2C (MCP47F(E/V)BXX) and SPI (MCP48F(E/V)BXX).
> 
> Fixes: bf394cc80369 ("iio: dac: adding support for Microchip MCP47FEB02")
> Signed-off-by: Ariana Lazar <ariana.lazar@microchip.com>
> Link: https://lore.kernel.org/all/aY4yaVP2TQFRI1E4@smile.fi.intel.com/
> ---
>  MAINTAINERS                       |   4 +
>  drivers/iio/dac/Kconfig           |  29 +-
>  drivers/iio/dac/Makefile          |   3 +
>  drivers/iio/dac/mcp47feb02-core.c | 845 ++++++++++++++++++++++++++++++++++++++
>  drivers/iio/dac/mcp47feb02-i2c.c  | 145 +++++++
>  drivers/iio/dac/mcp47feb02-spi.c  | 145 +++++++
>  drivers/iio/dac/mcp47feb02.h      | 158 +++++++
>  7 files changed, 1328 insertions(+), 1 deletion(-)
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 5997cf04b0732beaf69ac78cb762c42c56e4fcd6..af747c5449681807d3d74014dc11dffea5acc012 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -15850,6 +15850,10 @@ M:	Ariana Lazar <ariana.lazar@microchip.com>
>  L:	linux-iio@vger.kernel.org
>  S:	Supported
>  F:	Documentation/devicetree/bindings/iio/dac/microchip,mcp47feb02.yaml
> +F:	drivers/iio/dac/mcp47feb02-core.c
> +F:	drivers/iio/dac/mcp47feb02-i2c.c
> +F:	drivers/iio/dac/mcp47feb02-spi.c
> +F:	drivers/iio/dac/mcp47feb02.h
>  
>  MCP4821 DAC DRIVER
>  M:	Anshul Dalal <anshulusr@gmail.com>
> diff --git a/drivers/iio/dac/Kconfig b/drivers/iio/dac/Kconfig
> index cd4870b654153e91c3c44860be43d231ee3b5519..5bec52552d263532ffe357666a64a1c6bb968d85 100644
> --- a/drivers/iio/dac/Kconfig
> +++ b/drivers/iio/dac/Kconfig
> @@ -539,8 +539,12 @@ config MCP4728
>  	  will be called mcp4728.
>  
>  config MCP47FEB02
> +	tristate
> +
> +config MCP47FEB02_I2C
>  	tristate "MCP47F(E/V)B01/02/04/08/11/12/14/18/21/22/24/28 DAC driver"
>  	depends on I2C
> +	select MCP47FEB02
>  	help
>  	  Say yes here if you want to build the driver for the Microchip:
>  	  - 8-bit DAC:
> @@ -556,7 +560,30 @@ config MCP47FEB02
>  	  (DAC) with I2C interface.
>  
>  	  To compile this driver as a module, choose M here: the module
> -	  will be called mcp47feb02.
> +	  will be called mcp47feb02_i2c and you will also get
> +	  mcp47feb02_core for the core module.
> +
> +config MCP47FEB02_SPI
> +	tristate "MCP48F(E/V)B01/02/04/08/11/12/14/18/21/22/24/28 DAC driver"
> +	depends on SPI
> +	select MCP47FEB02
> +	help
> +	  Say yes here if you want to build the driver for the Microchip:
> +	  - 8-bit DAC:
> +	    MCP48FEB01, MCP48FEB02, MCP48FEB04, MCP48FEB08,
> +	    MCP48FVB01, MCP48FVB02, MCP48FVB04, MCP48FVB08
> +	  - 10-bit DAC:
> +	    MCP48FEB11, MCP48FEB12, MCP48FEB14, MCP48FEB18,
> +	    MCP48FVB11, MCP48FVB12, MCP48FVB14, MCP48FVB18
> +	  - 12-bit DAC:
> +	    MCP48FEB21, MCP48FEB22, MCP48FEB24, MCP48FEB28,
> +	    MCP48FVB21, MCP48FVB22, MCP48FVB24, MCP48FVB28
> +	  having 1 to 8 channels, 8/10/12-bit digital-to-analog converter
> +	  (DAC) with SPI interface.
> +
> +	  To compile this driver as a module, choose M here: the module
> +	  will be called mcp47feb02_spi and you will also get
> +	  mcp47feb02_core for the core module.
>  
>  config MCP4821
>  	tristate "MCP4801/02/11/12/21/22 DAC driver"
> diff --git a/drivers/iio/dac/Makefile b/drivers/iio/dac/Makefile
> index 2a80bbf4e80ad557da79ed916027cedff286984b..d2a2279b15499e1b43ed0e3e1f180b5b1ff72785 100644
> --- a/drivers/iio/dac/Makefile
> +++ b/drivers/iio/dac/Makefile
> @@ -54,6 +54,9 @@ obj-$(CONFIG_MAX5821) += max5821.o
>  obj-$(CONFIG_MCP4725) += mcp4725.o
>  obj-$(CONFIG_MCP4728) += mcp4728.o
>  obj-$(CONFIG_MCP47FEB02) += mcp47feb02.o

Shouldn't we be removing this old file?

The patch series would be eaiser to understand if it was split into
one commit to split the existing driver into two files and then
another commit to add support for the new parts.


> +mcp47feb02-objs := mcp47feb02-core.o
> +obj-$(CONFIG_MCP47FEB02_I2C) += mcp47feb02-i2c.o
> +obj-$(CONFIG_MCP47FEB02_SPI) += mcp47feb02-spi.o
>  obj-$(CONFIG_MCP4821) += mcp4821.o
>  obj-$(CONFIG_MCP4922) += mcp4922.o
>  obj-$(CONFIG_STM32_DAC_CORE) += stm32-dac-core.o

^ permalink raw reply

* Re: [PATCH 1/2] dt-bindings: iio: dac: add support for Microchip MCP48FEB02 to MCP47FEB02
From: David Lechner @ 2026-04-04 13:43 UTC (permalink / raw)
  To: Ariana Lazar, Jonathan Cameron, Nuno Sá, Andy Shevchenko,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley
  Cc: Jonathan Cameron, Conor Dooley, linux-iio, devicetree,
	linux-kernel
In-Reply-To: <20260403-mcp47feb02-fix2-v1-1-da60c773550e@microchip.com>

On 4/3/26 5:50 AM, Ariana Lazar wrote:
> This is the device tree schema for iio driver for Microchip
> MCP48FxBy1/2/4/8 series of buffered voltage output Digital-to-Analog
> Converters with nonvolatile or volatile memory on top of MCP47FEB02.
> The families support up to 8 output channels and have 8-bit, 10-bit or
> 12-bit resolution.
> 

...

>  examples:
>    - |
> @@ -280,7 +398,7 @@ examples:
>  
>          #address-cells = <1>;
>          #size-cells = <0>;
> -        dac@0 {
> +        dac@60 {
>            compatible = "microchip,mcp47feb02";
>            reg = <0>;

Need to change reg to 0x60 also.

>            vdd-supply = <&vdac_vdd>;
> @@ -299,4 +417,29 @@ examples:
>            };
>        };
>      };

^ permalink raw reply

* [PATCH v2 4/4] arm64: dts: qcom: x1e80100-dell-xps13-9345: introduce EC
From: Aleksandrs Vinarskis @ 2026-04-04 12:55 UTC (permalink / raw)
  To: Bjorn Andersson, Konrad Dybcio, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Hans de Goede, Ilpo Järvinen,
	Bryan O'Donoghue
  Cc: linux-arm-msm, devicetree, linux-kernel, platform-driver-x86,
	laurentiu.tudor1, Abel Vesa, Tobias Heider, Val Packett
In-Reply-To: <20260404-dell-xps-9345-ec-v2-0-c977c3caa81f@vinarskis.com>

Describe embedded controller, its interrupt and required thermal zones.
Add EC's reset GPIO to reserved range, as triggering it during device
operation leads to unrecoverable and unusable state.

Signed-off-by: Aleksandrs Vinarskis <alex@vinarskis.com>
---
 .../boot/dts/qcom/x1e80100-dell-xps13-9345.dts     | 94 +++++++++++++++++++++-
 1 file changed, 92 insertions(+), 2 deletions(-)

diff --git a/arch/arm64/boot/dts/qcom/x1e80100-dell-xps13-9345.dts b/arch/arm64/boot/dts/qcom/x1e80100-dell-xps13-9345.dts
index ce7b10ea89b6dcb2a4a65c114037f4c90a4b0c6d..fe7e069f0ef56c6fdc3b495dd78dacd1b96c1c95 100644
--- a/arch/arm64/boot/dts/qcom/x1e80100-dell-xps13-9345.dts
+++ b/arch/arm64/boot/dts/qcom/x1e80100-dell-xps13-9345.dts
@@ -7,6 +7,7 @@
 /dts-v1/;
 
 #include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/iio/qcom,spmi-adc7-pm8350.h>
 #include <dt-bindings/input/gpio-keys.h>
 #include <dt-bindings/input/input.h>
 #include <dt-bindings/leds/common.h>
@@ -759,8 +760,32 @@ retimer_ss0_con_sbu_out: endpoint {
 
 &i2c5 {
 	clock-frequency = <100000>;
-	status = "disabled";
-	/* EC @0x3b */
+	status = "okay";
+
+	embedded-controller@3b {
+		compatible = "dell,xps13-9345-ec";
+		reg = <0x3b>;
+
+		interrupts-extended = <&tlmm 66 IRQ_TYPE_LEVEL_LOW>;
+
+		pinctrl-0 = <&ec_int_n_default>;
+		pinctrl-names = "default";
+
+		io-channels = <&pmk8550_vadc PM8350_ADC7_GPIO3_100K_PU(1)>,
+			      <&pmk8550_vadc PM8350_ADC7_GPIO4_100K_PU(1)>,
+			      <&pmk8550_vadc PM8350_ADC7_AMUX_THM1_100K_PU(1)>,
+			      <&pmk8550_vadc PM8350_ADC7_AMUX_THM2_100K_PU(1)>,
+			      <&pmk8550_vadc PM8350_ADC7_AMUX_THM3_100K_PU(1)>,
+			      <&pmk8550_vadc PM8350_ADC7_AMUX_THM4_100K_PU(1)>,
+			      <&pmk8550_vadc PM8350_ADC7_AMUX_THM5_100K_PU(1)>;
+		io-channel-names = "sys_therm0",
+				   "sys_therm1",
+				   "sys_therm2",
+				   "sys_therm3",
+				   "sys_therm4",
+				   "sys_therm5",
+				   "sys_therm6";
+	};
 };
 
 &i2c7 {
@@ -1025,6 +1050,64 @@ rtmr0_1p8_reg_en: rtmr0-1p8-reg-en-state {
 	};
 };
 
+&pmk8550_vadc {
+	/* Around DRAM */
+	channel@14c {
+		reg = <PM8350_ADC7_GPIO3_100K_PU(1)>;
+		qcom,hw-settle-time = <200>;
+		qcom,ratiometric;
+		label = "sys_therm0";
+	};
+
+	/* Around left Type-C charging controller */
+	channel@14d {
+		reg = <PM8350_ADC7_GPIO4_100K_PU(1)>;
+		qcom,hw-settle-time = <200>;
+		qcom,ratiometric;
+		label = "sys_therm1";
+	};
+
+	/* Around upper-left side of motherboard */
+	channel@144 {
+		reg = <PM8350_ADC7_AMUX_THM1_100K_PU(1)>;
+		qcom,hw-settle-time = <200>;
+		qcom,ratiometric;
+		label = "sys_therm2";
+	};
+
+	/* Around right Type-C charging controller */
+	channel@145 {
+		reg = <PM8350_ADC7_AMUX_THM2_100K_PU(1)>;
+		qcom,hw-settle-time = <200>;
+		qcom,ratiometric;
+		label = "sys_therm3";
+	};
+
+	/* Around SSD connector */
+	channel@146 {
+		reg = <PM8350_ADC7_AMUX_THM3_100K_PU(1)>;
+		qcom,hw-settle-time = <200>;
+		qcom,ratiometric;
+		label = "sys_therm4";
+	};
+
+	/* Around battery charging circuit */
+	channel@147 {
+		reg = <PM8350_ADC7_AMUX_THM4_100K_PU(1)>;
+		qcom,hw-settle-time = <200>;
+		qcom,ratiometric;
+		label = "sys_therm5";
+	};
+
+	/* Around keyboard */
+	channel@148 {
+		reg = <PM8350_ADC7_AMUX_THM5_100K_PU(1)>;
+		qcom,hw-settle-time = <200>;
+		qcom,ratiometric;
+		label = "sys_therm6";
+	};
+};
+
 &qupv3_0 {
 	status = "okay";
 };
@@ -1071,6 +1154,7 @@ &smb2360_1_eusb2_repeater {
 
 &tlmm {
 	gpio-reserved-ranges = <44 4>,  /* SPI11 (TPM) */
+			       <65 1>,  /* EC Reset, accessible but yields system unusable */
 			       <76 4>,  /* SPI19 (TZ Protected) */
 			       <238 1>; /* UFS Reset */
 
@@ -1081,6 +1165,12 @@ cam_indicator_en: cam-indicator-en-state {
 		bias-disable;
 	};
 
+	ec_int_n_default: ec-int-n-state {
+		pins = "gpio66";
+		function = "gpio";
+		bias-disable;
+	};
+
 	edp_bl_en: edp-bl-en-state {
 		pins = "gpio74";
 		function = "gpio";

-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 3/4] arm64: dts: qcom: hamoa-pmics: define VADC for pmk8550
From: Aleksandrs Vinarskis @ 2026-04-04 12:55 UTC (permalink / raw)
  To: Bjorn Andersson, Konrad Dybcio, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Hans de Goede, Ilpo Järvinen,
	Bryan O'Donoghue
  Cc: linux-arm-msm, devicetree, linux-kernel, platform-driver-x86,
	laurentiu.tudor1, Abel Vesa, Tobias Heider, Val Packett
In-Reply-To: <20260404-dell-xps-9345-ec-v2-0-c977c3caa81f@vinarskis.com>

Follow pattern of pmk8350 to add missing pmk8550 VADC to hamoa.
Register address of 0x9000 matches example schema for spmi-adc5-gen3.

Signed-off-by: Aleksandrs Vinarskis <alex@vinarskis.com>
---
 arch/arm64/boot/dts/qcom/hamoa-pmics.dtsi | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/arch/arm64/boot/dts/qcom/hamoa-pmics.dtsi b/arch/arm64/boot/dts/qcom/hamoa-pmics.dtsi
index 6a31a0adf8be472badea502a916cdbc9477e9f2b..cc69d299bc356d90aa1483f347f5eee43b853e45 100644
--- a/arch/arm64/boot/dts/qcom/hamoa-pmics.dtsi
+++ b/arch/arm64/boot/dts/qcom/hamoa-pmics.dtsi
@@ -218,6 +218,32 @@ pon_resin: resin {
 			};
 		};
 
+		pmk8550_vadc: adc@9000 {
+			compatible = "qcom,spmi-adc5-gen3";
+			reg = <0x9000>, <0x9100>;
+			#address-cells = <1>;
+			#size-cells = <0>;
+			interrupts = <0x0 0x90 0x1 IRQ_TYPE_EDGE_RISING>,
+				     <0x0 0x91 0x1 IRQ_TYPE_EDGE_RISING>;
+			#io-channel-cells = <1>;
+			#thermal-sensor-cells = <1>;
+
+			channel@3 {
+				reg = <0x3>;
+				label = "pmk8550_die_temp";
+				qcom,pre-scaling = <1 1>;
+			};
+
+			channel@44 {
+				reg = <0x44>;
+				label = "pmk8550_xo_therm";
+				qcom,pre-scaling = <1 1>;
+				qcom,ratiometric;
+				qcom,hw-settle-time = <200>;
+				qcom,adc-tm;
+			};
+		};
+
 		pmk8550_rtc: rtc@6100 {
 			compatible = "qcom,pmk8350-rtc";
 			reg = <0x6100>, <0x6200>;

-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 2/4] platform: arm64: dell-xps-ec: new driver
From: Aleksandrs Vinarskis @ 2026-04-04 12:55 UTC (permalink / raw)
  To: Bjorn Andersson, Konrad Dybcio, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Hans de Goede, Ilpo Järvinen,
	Bryan O'Donoghue
  Cc: linux-arm-msm, devicetree, linux-kernel, platform-driver-x86,
	laurentiu.tudor1, Abel Vesa, Tobias Heider, Val Packett
In-Reply-To: <20260404-dell-xps-9345-ec-v2-0-c977c3caa81f@vinarskis.com>

Introduce EC driver for Dell XPS 13 9345 (codename 'tributo') which may
partially of fully compatible with Snapdragon-based Dell Latitude,
Inspiron ('thena'). Primary function of this driver is unblock EC's
thermal management, specifically to provide it with necessary
information to control device fans, peripherals power.

The driver was developed primarily by analyzing ACPI DSDT's _DSM and
i2c dumps of communication between SoC and EC. Changes to Windows
driver's behavior include increasing temperature feed loop from ~50ms
to 100ms here.

While Xps's EC is rather complex and controls practically all device
peripherals including touch row's brightness and special keys such as
mic mute, these do not go over this particular i2c interface.

Not yet implemented features:
- On lid-close IRQ event is registered. Windows performs what to
  appears to be thermistor constants readout, though its not obvious
  what it used for.
- According to ACPI's _DSM there is a method to readout fans' RPM.
- Initial thermistor constants were sniffed from Windows, these can be
  likely fine tuned for better cooling performance.
- There is additional temperature reading that Windows sents to EC but
  more rare than others, likely SoC T_j / TZ98 or TZ4. This is the only
  thermal zone who's reading can exceed 115C without triggering thermal
  shutdown.
- Given similarities between 'tributo' and 'thena' platforms, including
  EC i2c address, driver can be potentially extended to support both.

Signed-off-by: Aleksandrs Vinarskis <alex@vinarskis.com>
---
 MAINTAINERS                          |   1 +
 drivers/platform/arm64/Kconfig       |  12 ++
 drivers/platform/arm64/Makefile      |   1 +
 drivers/platform/arm64/dell-xps-ec.c | 267 +++++++++++++++++++++++++++++++++++
 4 files changed, 281 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index a5d175559f4468dfe363b319a1b08d3425f4d712..c150f57b60706224e5b24b0dfb3d8a9b81f36398 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7240,6 +7240,7 @@ DELL XPS EMBEDDED CONTROLLER DRIVER
 M:	Aleksandrs Vinarskis <alex@vinarskis.com>
 S:	Maintained
 F:	Documentation/devicetree/bindings/embedded-controller/dell,xps13-9345-ec.yaml
+F:	drivers/platform/arm64/dell-xps-ec.c
 
 DELTA AHE-50DC FAN CONTROL MODULE DRIVER
 M:	Zev Weiss <zev@bewilderbeest.net>
diff --git a/drivers/platform/arm64/Kconfig b/drivers/platform/arm64/Kconfig
index 10f905d7d6bfa5fad30a0689d3a20481268c781e..0bc8f016032bb05cb3a7cc50bdf1092da04153bc 100644
--- a/drivers/platform/arm64/Kconfig
+++ b/drivers/platform/arm64/Kconfig
@@ -33,6 +33,18 @@ config EC_ACER_ASPIRE1
 	  laptop where this information is not properly exposed via the
 	  standard ACPI devices.
 
+config EC_DELL_XPS
+	tristate "Dell XPS 9345 Embedded Controller driver"
+	depends on ARCH_QCOM || COMPILE_TEST
+	depends on I2C
+	depends on IIO
+	help
+	  Driver for the Embedded Controller in the Qualcomm Snapdragon-based
+	  Dell XPS 13 9345, which handles thermal management and fan speed
+	  control.
+
+	  Say M or Y here to include this support.
+
 config EC_HUAWEI_GAOKUN
 	tristate "Huawei Matebook E Go Embedded Controller driver"
 	depends on ARCH_QCOM || COMPILE_TEST
diff --git a/drivers/platform/arm64/Makefile b/drivers/platform/arm64/Makefile
index 60c131cff6a15bb51a49c9edab95badf513ee0f6..6768dc6c2310837374e67381cfc729bed1fdaaef 100644
--- a/drivers/platform/arm64/Makefile
+++ b/drivers/platform/arm64/Makefile
@@ -6,6 +6,7 @@
 #
 
 obj-$(CONFIG_EC_ACER_ASPIRE1)	+= acer-aspire1-ec.o
+obj-$(CONFIG_EC_DELL_XPS)	+= dell-xps-ec.o
 obj-$(CONFIG_EC_HUAWEI_GAOKUN)	+= huawei-gaokun-ec.o
 obj-$(CONFIG_EC_LENOVO_YOGA_C630) += lenovo-yoga-c630.o
 obj-$(CONFIG_EC_LENOVO_THINKPAD_T14S) += lenovo-thinkpad-t14s.o
diff --git a/drivers/platform/arm64/dell-xps-ec.c b/drivers/platform/arm64/dell-xps-ec.c
new file mode 100644
index 0000000000000000000000000000000000000000..bf1495fbe473ccdb82b95a66b56e8525f782cc8e
--- /dev/null
+++ b/drivers/platform/arm64/dell-xps-ec.c
@@ -0,0 +1,267 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, Aleksandrs Vinarskis <alex@vinarskis.com>
+ */
+
+#include <linux/array_size.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/devm-helpers.h>
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/iio/consumer.h>
+#include <linux/interrupt.h>
+#include <linux/jiffies.h>
+#include <linux/module.h>
+#include <linux/pm.h>
+#include <linux/unaligned.h>
+#include <linux/workqueue.h>
+
+#define DELL_XPS_EC_SUSPEND_CMD		0xb9
+#define DELL_XPS_EC_SUSPEND_MSG_LEN	64
+
+#define DELL_XPS_EC_TEMP_CMD0		0xfb
+#define DELL_XPS_EC_TEMP_CMD1		0x20
+#define DELL_XPS_EC_TEMP_CMD3		0x02
+#define DELL_XPS_EC_TEMP_MSG_LEN	6
+#define DELL_XPS_EC_TEMP_POLL_JIFFIES	msecs_to_jiffies(100)
+
+/*
+ * Format:
+ * - header/unknown (2 bytes)
+ * - per-thermistor entries (3 bytes): thermistor_id, param1, param2
+ */
+static const u8 dell_xps_ec_thermistor_profile[] = {
+	0xff, 0x54,
+	0x01, 0x00, 0x2b,	/* sys_therm0 */
+	0x02, 0x44, 0x2a,	/* sys_therm1 */
+	0x03, 0x44, 0x2b,	/* sys_therm2 */
+	0x04, 0x44, 0x28,	/* sys_therm3 */
+	0x05, 0x55, 0x2a,	/* sys_therm4 */
+	0x06, 0x44, 0x26,	/* sys_therm5 */
+	0x07, 0x44, 0x2b,	/* sys_therm6 */
+};
+
+/*
+ * Mapping from IIO channel name to EC command byte
+ */
+static const struct {
+	const char *name;
+	u8 cmd;
+} dell_xps_ec_therms[] = {
+	/* TODO: 0x01 is sent only occasionally, likely TZ98 or TZ4 */
+	{ "sys_therm0", 0x02 },
+	{ "sys_therm1", 0x03 },
+	{ "sys_therm2", 0x04 },
+	{ "sys_therm3", 0x05 },
+	{ "sys_therm4", 0x06 },
+	{ "sys_therm5", 0x07 },
+	{ "sys_therm6", 0x08 },
+};
+
+struct dell_xps_ec {
+	struct device *dev;
+	struct i2c_client *client;
+	struct iio_channel *therm_channels[ARRAY_SIZE(dell_xps_ec_therms)];
+	struct delayed_work temp_work;
+};
+
+static int dell_xps_ec_suspend_cmd(struct dell_xps_ec *ec, bool suspend)
+{
+	u8 buf[DELL_XPS_EC_SUSPEND_MSG_LEN] = {};
+	int ret;
+
+	buf[0] = DELL_XPS_EC_SUSPEND_CMD;
+	buf[1] = suspend ? 0x01 : 0x00;
+	/* bytes 2..63 remain zero */
+
+	ret = i2c_master_send(ec->client, buf, sizeof(buf));
+	if (ret < 0)
+		return ret;
+
+	return 0;
+}
+
+static int dell_xps_ec_send_temp(struct dell_xps_ec *ec, u8 cmd_byte,
+				 int milli_celsius)
+{
+	u8 buf[DELL_XPS_EC_TEMP_MSG_LEN];
+	u16 deci_celsius;
+	int ret;
+
+	/* Convert milli-Celsius to deci-Celsius (Celsius * 10) */
+	deci_celsius = milli_celsius / 100;
+
+	buf[0] = DELL_XPS_EC_TEMP_CMD0;
+	buf[1] = DELL_XPS_EC_TEMP_CMD1;
+	buf[2] = cmd_byte;
+	buf[3] = DELL_XPS_EC_TEMP_CMD3;
+	put_unaligned_le16(deci_celsius, &buf[4]);
+
+	ret = i2c_master_send(ec->client, buf, sizeof(buf));
+	if (ret < 0)
+		return ret;
+
+	return 0;
+}
+
+static void dell_xps_ec_temp_work_fn(struct work_struct *work)
+{
+	struct dell_xps_ec *ec = container_of(work, struct dell_xps_ec,
+					      temp_work.work);
+	int val, ret, i;
+
+	for (i = 0; i < ARRAY_SIZE(dell_xps_ec_therms); i++) {
+		if (!ec->therm_channels[i])
+			continue;
+
+		ret = iio_read_channel_processed(ec->therm_channels[i], &val);
+		if (ret < 0) {
+			dev_err_ratelimited(ec->dev,
+					    "Failed to read thermistor %s: %d\n",
+					    dell_xps_ec_therms[i].name, ret);
+			continue;
+		}
+
+		ret = dell_xps_ec_send_temp(ec, dell_xps_ec_therms[i].cmd, val);
+		if (ret < 0) {
+			dev_err_ratelimited(ec->dev,
+					    "Failed to send temp for %s: %d\n",
+					    dell_xps_ec_therms[i].name, ret);
+		}
+	}
+
+	schedule_delayed_work(&ec->temp_work, DELL_XPS_EC_TEMP_POLL_JIFFIES);
+}
+
+static irqreturn_t dell_xps_ec_irq_handler(int irq, void *data)
+{
+	struct dell_xps_ec *ec = data;
+
+	/*
+	 * TODO: IRQ is fired on lid-close. Follow Windows example to read out
+	 *       the thermistor thresholds and potentially fan speeds.
+	 */
+	dev_info_ratelimited(ec->dev, "IRQ triggered! (irq=%d)\n", irq);
+
+	return IRQ_HANDLED;
+}
+
+static int dell_xps_ec_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct dell_xps_ec *ec;
+	int ret, i;
+
+	ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL);
+	if (!ec)
+		return -ENOMEM;
+
+	ec->dev = dev;
+	ec->client = client;
+	i2c_set_clientdata(client, ec);
+
+	/* Set default thermistor profile */
+	ret = i2c_master_send(client, dell_xps_ec_thermistor_profile,
+			      sizeof(dell_xps_ec_thermistor_profile));
+	if (ret < 0)
+		return dev_err_probe(dev, ret, "Failed to set thermistor profile\n");
+
+	/* Get IIO channels for thermistors */
+	for (i = 0; i < ARRAY_SIZE(dell_xps_ec_therms); i++) {
+		ec->therm_channels[i] =
+			devm_iio_channel_get(dev, dell_xps_ec_therms[i].name);
+		if (IS_ERR(ec->therm_channels[i])) {
+			ret = PTR_ERR(ec->therm_channels[i]);
+			ec->therm_channels[i] = NULL;
+			if (ret == -EPROBE_DEFER)
+				return ret;
+			dev_warn(dev, "Thermistor %s not available: %d\n",
+				 dell_xps_ec_therms[i].name, ret);
+		}
+	}
+
+	/* Start periodic temperature reporting */
+	ret = devm_delayed_work_autocancel(dev, &ec->temp_work,
+					   dell_xps_ec_temp_work_fn);
+	if (ret)
+		return ret;
+	schedule_delayed_work(&ec->temp_work, DELL_XPS_EC_TEMP_POLL_JIFFIES);
+	dev_dbg(dev, "Started periodic temperature reporting to EC every %d ms\n",
+		jiffies_to_msecs(DELL_XPS_EC_TEMP_POLL_JIFFIES));
+
+	/* Request IRQ for EC events */
+	ret = devm_request_threaded_irq(dev, client->irq, NULL,
+					dell_xps_ec_irq_handler,
+					IRQF_ONESHOT, dev_name(dev), ec);
+	if (ret < 0)
+		return dev_err_probe(dev, ret, "Failed to request IRQ\n");
+
+	return 0;
+}
+
+/*
+ * Notify EC of suspend
+ *
+ * This will:
+ * - Cut power to display/trackpad/keyboard/touchrow, wake-up source still works
+ */
+static int dell_xps_ec_suspend(struct device *dev)
+{
+	struct dell_xps_ec *ec = dev_get_drvdata(dev);
+
+	cancel_delayed_work_sync(&ec->temp_work);
+
+	return dell_xps_ec_suspend_cmd(ec, true);
+}
+
+/*
+ * Notify EC of resume
+ *
+ * This will undo the suspend actions
+ * Without the resume signal, device would wake up but be forced back into
+ * suspend by EC within seconds
+ */
+static int dell_xps_ec_resume(struct device *dev)
+{
+	struct dell_xps_ec *ec = dev_get_drvdata(dev);
+	int ret;
+
+	ret = dell_xps_ec_suspend_cmd(ec, false);
+	if (ret)
+		return ret;
+
+	schedule_delayed_work(&ec->temp_work, DELL_XPS_EC_TEMP_POLL_JIFFIES);
+	return 0;
+}
+
+static const struct of_device_id dell_xps_ec_of_match[] = {
+	{ .compatible = "dell,xps13-9345-ec" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, dell_xps_ec_of_match);
+
+static const struct i2c_device_id dell_xps_ec_i2c_id[] = {
+	{ "dell-xps-ec" },
+	{}
+};
+MODULE_DEVICE_TABLE(i2c, dell_xps_ec_i2c_id);
+
+static const struct dev_pm_ops dell_xps_ec_pm_ops = {
+	SYSTEM_SLEEP_PM_OPS(dell_xps_ec_suspend, dell_xps_ec_resume)
+};
+
+static struct i2c_driver dell_xps_ec_driver = {
+	.driver = {
+		.name = "dell-xps-ec",
+		.of_match_table = dell_xps_ec_of_match,
+		.pm = &dell_xps_ec_pm_ops,
+	},
+	.probe = dell_xps_ec_probe,
+	.id_table = dell_xps_ec_i2c_id,
+};
+module_i2c_driver(dell_xps_ec_driver);
+
+MODULE_AUTHOR("Aleksandrs Vinarskis <alex@vinarskis.com>");
+MODULE_DESCRIPTION("Dell XPS 13 9345 Embedded Controller");
+MODULE_LICENSE("GPL");

-- 
2.53.0


^ permalink raw reply related


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