From coiaprant at gmail.com  Sat Aug  1 07:22:24 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:24 +0800
Subject: [PATCH v2 00/10] net-next: add basic support for RK3568 XPCS
Message-ID: <20260801142244.679437-2-coiaprant@gmail.com>

Changes since v1:

Patch 2 (dt-bindings: phy: naneng-combphy)
- Changed `rockchip,sgmii-mac-sel` default from 0 to 1 to match
  hardware reset value (GMAC1)
- Added `if-then` conditional in `allOf` to restrict the property
  to `rockchip,rk3568-naneng-combphy` only
- Updated commit message to reflect hardware default

Patch 3 (phy: rockchip: naneng-combphy)
- Changed `priv->sgmii_mac_sel` initial value from 0 to 1 to match
  hardware default
- Updated commit message to align with binding changes

Patch 4 (dt-bindings: net: pcs: rockchip-dwxpcs)
- Fixed `reg` format in example to pass `dt_binding_check`
- Added missing `#include <dt-bindings/phy/phy.h>` in example

Patch 5 (arm64: dts: rockchip: rk3568)
- Clarified commit message: "all nodes disabled" refers to XPCS
  device nodes, not fixed-clock nodes
- Noted that `assigned-clock-parents` are applied at board level
  (Patch 9), not in SoC dtsi

Patch 6 (net: pcs: xpcs: add ANRESTART support for SGMII link recovery)
- Split the original RFC patch into two: the SGMII state reading fix was
  sent separately to the net tree and has been merged. This patch now
  only adds the ANRESTART functionality as a new feature for net-next.
- Removed unintentional `BMCR_ANRESTART` change in c37_1000basex
  (this was not tested and should not have been included)
- Kept SGMII path changes for RK3568 only

Patch 7 (net: pcs: xpcs: add Rockchip RK3568 platform glue driver)
- Added `.remove` callback with `pm_runtime_force_suspend()` to fix
  potential CSR clock leak on device removal
- Added compatible string validation in `xpcs_rk_create()` before
  dereferencing drvdata (using `of_match_node(xpcs_rk_of_ids, pcs_np)`)
- Removed `__maybe_unused` from PM runtime callbacks
- Add `depends on PM` in Kconfig to ensure `CONFIG_PM` is enabled for 
  this driver

Patch 8 (net: stmmac: dwmac-rk)
- Moved `gmac_clk_enable()` call before SGMII check to ensure clocks are
  enabled before register access in SGMII mode
- Added unified error handling (`clk_disable` label) for all failure paths
- Added error handling for default (unhandled interface) case

===

This series adds proper SGMII support for the Rockchip RK3568 SoC
using the integrated Synopsys DesignWare XPCS, along with necessary
fixes and refactoring in the stmmac core and XPCS driver.

Motivation
==========
The RK3568 integrates a DW XPCS accessed via APB3 and connected to
a Naneng Combo SerDes PHY.  Several boards (e.g., Ariaboard
Photonicat) use this interface for Gigabit Ethernet.  However, the
current upstream stmmac driver does not support this configuration,
and the XPCS driver has issues in SGMII poll mode that cause the
link to be reported incorrectly.

This series addresses these issues by:
- Fixing the XPCS driver's SGMII AN state handling
- Refactoring stmmac PCS lifetime management to allow platform drivers
  full control over PCS creation/destruction
- Adding a Rockchip XPCS platform glue driver and wiring it up in
  dwmac-rk

Series overview
===============

Generic:
  Patch 1: move XPCS lifetime management to platform drivers
           (introduces pcs_init/pcs_exit callbacks)

PHY:
  Patch 2: DT binding for Naneng Combo PHY SGMII MAC selection
  Patch 3: implement the PHY SGMII MAC selection in driver

RK3568 XPCS/SGMII:
  Patch 4: DT binding for Rockchip RK3568 XPCS
  Patch 5: add XPCS and fixed-clock nodes to rk3568.dtsi
  Patch 6: add ANRESTART support for SGMII link recovery
  Patch 7: implement the Rockchip XPCS platform glue driver
  Patch 8: wire up SGMII support in dwmac-rk
  Patch 10: update MAINTAINERS

Board enablement:
  Patch 9: enable SGMII LAN port on Photonicat board

Key design decisions
====================
- The stmmac core now delegates XPCS creation entirely to platform
  drivers via pcs_init/pcs_exit.  This is necessary because the
  generic XPCS creation logic would override any XPCS set up by the
  platform driver.

- The Rockchip XPCS driver creates a virtual MDIO bus over the APB3
  registers and implements address remapping.  The generic XPCS core
  handles all PCS configuration via phylink_pcs_ops.

- On RK3568 in SGMII mode, the MAC clock is fixed at 125 MHz and
  cannot be dynamically changed.  In-band mode is used, and the
  generic stmmac set_clk_tx_rate callback is disabled to prevent
  incorrect clock updates that would break RX.

- The SerDes and power domain are attached to the XPCS device tree
  node rather than the MAC node. This reflects the actual hardware
  topology and simplifies the dwmac-rk driver by keeping all PCS-related
  resources self-contained. It also prepares for possible future QSGMII
  support, where a single SerDes serves multiple MACs and would be
  more naturally managed under the XPCS node.

Testing
=======
Board: Ariaboard Photonicat (RK3568)
OS: Armbian (trixie)
Kernel: 6.18 (backports)
Result: The SGMII interface obtains an IP address, SSH works, and
        ping traffic passes without loss.

Notes
=====
- When testing out-band mode with set_clk_tx_rate, only 1000Mbps
  works on both TX/RX; 10/100Mbps only works on TX side.

Dependencies
============
None. All patches apply cleanly on top of torvalds master tree (v7.2).

Acknowledgments
===============
This work was inspired by and builds upon the excellent work of others:
- Serge Semin's Synopsys DesignWare XPCS platform driver (pcs-xpcs-plat.c)
- Cl?ment L?ger's Renesas MIIC driver (pcs-rzn1-miic.c)
- The Rockchip TRM and downstream OEM drivers

Thanks in advance,
Coia Prant
---
Coia Prant (10):
  net: stmmac: move XPCS lifetime management to platform drivers
  dt-bindings: phy: rockchip: naneng-combphy: add rockchip,sgmii-mac-sel
    property
  phy: rockchip: naneng-combphy: add SGMII MAC selection for RK3568
  dt-bindings: net: pcs: add rockchip,rk3568-xpcs binding
  arm64: dts: rockchip: rk3568: add XPCS and fixed-clock nodes
  net: pcs: xpcs: add ANRESTART support for SGMII link recovery
  net: pcs: xpcs: add Rockchip RK3568 platform glue driver
  net: stmmac: dwmac-rk: add SGMII support for RK3568
  arm64: dts: rockchip: rk3568-photonicat: enable SGMII LAN port
  MAINTAINERS: add entry for Rockchip XPCS driver

 .../bindings/net/pcs/rockchip-dwxpcs.yaml     | 127 +++++
 .../phy/phy-rockchip-naneng-combphy.yaml      |  13 +
 MAINTAINERS                                   |   9 +
 .../boot/dts/rockchip/rk3568-photonicat.dts   |  77 ++-
 arch/arm64/boot/dts/rockchip/rk3568.dtsi      |  45 ++
 drivers/net/ethernet/stmicro/stmmac/Kconfig   |   1 +
 .../net/ethernet/stmicro/stmmac/dwmac-intel.c |  44 +-
 .../stmicro/stmmac/dwmac-renesas-gbeth.c      |   7 +-
 .../net/ethernet/stmicro/stmmac/dwmac-rk.c    | 121 +++-
 .../net/ethernet/stmicro/stmmac/dwmac-rzn1.c  |   7 +-
 .../ethernet/stmicro/stmmac/dwmac-socfpga.c   |   7 +-
 .../net/ethernet/stmicro/stmmac/stmmac_mdio.c |  37 +-
 drivers/net/pcs/Kconfig                       |  23 +
 drivers/net/pcs/Makefile                      |   7 +-
 drivers/net/pcs/pcs-xpcs-rk.c                 | 538 ++++++++++++++++++
 drivers/net/pcs/pcs-xpcs.c                    |  17 +-
 .../rockchip/phy-rockchip-naneng-combphy.c    |   8 +
 include/linux/pcs/pcs-xpcs-rk.h               |  11 +
 18 files changed, 1030 insertions(+), 69 deletions(-)
 create mode 100644 Documentation/devicetree/bindings/net/pcs/rockchip-dwxpcs.yaml
 create mode 100644 drivers/net/pcs/pcs-xpcs-rk.c
 create mode 100644 include/linux/pcs/pcs-xpcs-rk.h

-- 
2.47.3



From coiaprant at gmail.com  Sat Aug  1 07:22:25 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:25 +0800
Subject: [PATCH v2 01/10] net: stmmac: move XPCS lifetime management to platform drivers
In-Reply-To: <20260801142244.679437-2-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
Message-ID: <20260801142244.679437-3-coiaprant@gmail.com>

The current XPCS creation logic in stmmac_pcs_setup() is problematic
for several reasons.

First, if a device tree specifies a "pcs-handle" but no select_pcs()
callback is provided by the platform driver, the created XPCS is never
used. The phylink framework requires select_pcs() to actually return
the PCS to the core, so the pcs-handle property becomes effectively
useless without the matching callback. This is confusing for developers
who expect that specifying a pcs-handle in their device tree should be
sufficient to enable the PCS.

Second, and more critically, when stmmac_pcs_setup() fails to create
an XPCS (either because no pcs-handle is present and no pcs_mask is
configured), it falls through to the else branch and leaves
priv->hw->xpcs as NULL. This will silently override any XPCS that a
platform driver may have already set up during its own initialization,
for example in a pcs_init() callback or during probe. The platform
driver has no way to prevent this override because the common code
runs unconditionally after the platform-specific initialization.

After commit 93f84152e4ae ("net: stmmac: clean up
stmmac_mac_select_pcs()"), the common code no longer falls back to
priv->hw->phylink_pcs if select_pcs() is not set. This change
reinforces that each platform must manage its own PCS life cycle
explicitly, but the XPCS creation code in stmmac_pcs_setup() was not
updated to match this new expectation, leaving a gap where platform
drivers have no clean way to take control of XPCS creation.

Address all of these issues by introducing pcs_init() and pcs_exit()
callbacks in plat_stmmacenet_data. These callbacks give platform
drivers full control over when and how the XPCS is created, configured,
and destroyed. The common stmmac_pcs_setup() and stmmac_pcs_clean()
functions are simplified to just call these callbacks, removing the
confusing and error-prone XPCS creation logic from the common code.

Platforms that do not need an XPCS simply leave the callbacks as NULL
and no change in behavior occurs. Platforms that do need an XPCS can
now create it with the exact configuration they require, including
wrapping it with custom phylink_pcs_ops when necessary.

Existing platform drivers (intel, rzn1, socfpga) are updated to use
the new callbacks by moving their XPCS creation and cleanup logic into
pcs_init() and pcs_exit(). In their pcs_exit() implementations, the
pointer to the destroyed PCS is explicitly set to NULL to avoid
dangling pointer references.

Signed-off-by: Coia Prant <coiaprant at gmail.com>
---
 .../net/ethernet/stmicro/stmmac/dwmac-intel.c | 44 +++++++++++++++++--
 .../stmicro/stmmac/dwmac-renesas-gbeth.c      |  7 ++-
 .../net/ethernet/stmicro/stmmac/dwmac-rzn1.c  |  7 ++-
 .../ethernet/stmicro/stmmac/dwmac-socfpga.c   |  7 ++-
 .../net/ethernet/stmicro/stmmac/stmmac_mdio.c | 37 +++-------------
 5 files changed, 61 insertions(+), 41 deletions(-)

diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-intel.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-intel.c
index 4d207f41a43b3..221b3203a81ed 100644
--- a/drivers/net/ethernet/stmicro/stmmac/dwmac-intel.c
+++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-intel.c
@@ -603,13 +603,47 @@ static void common_default_data(struct plat_stmmacenet_data *plat)
 	plat->mdio_bus_data->needs_reset = true;
 }
 
+static int intel_mgbe_pcs_init(struct stmmac_priv *priv)
+{
+	struct fwnode_handle *devnode, *pcsnode;
+	struct dw_xpcs *xpcs = NULL;
+	int addr;
+
+	devnode = dev_fwnode(priv->device);
+
+	if (fwnode_property_present(devnode, "pcs-handle")) {
+		pcsnode = fwnode_find_reference(devnode, "pcs-handle", 0);
+		xpcs = xpcs_create_fwnode(pcsnode);
+		fwnode_handle_put(pcsnode);
+	} else {
+		addr = ffs(priv->plat->mdio_bus_data->pcs_mask) - 1;
+		xpcs = xpcs_create_mdiodev(priv->mii, addr);
+	}
+
+	if (IS_ERR(xpcs))
+		return PTR_ERR(xpcs);
+
+	xpcs_config_eee_mult_fact(xpcs, priv->plat->mult_fact_100ns);
+
+	priv->hw->xpcs = xpcs;
+	return 0;
+}
+
+static void intel_mgbe_pcs_exit(struct stmmac_priv *priv)
+{
+	if (!priv->hw->xpcs)
+		return;
+
+	xpcs_destroy(priv->hw->xpcs);
+	priv->hw->xpcs = NULL;
+}
+
 static struct phylink_pcs *intel_mgbe_select_pcs(struct stmmac_priv *priv,
 						 phy_interface_t interface)
 {
-	/* plat->mdio_bus_data->has_xpcs has been set true, so there
-	 * should always be an XPCS. The original code would always
-	 * return this if present.
-	 */
+	if (!priv->hw->xpcs)
+		return NULL;
+
 	return xpcs_to_phylink_pcs(priv->hw->xpcs);
 }
 
@@ -733,6 +767,8 @@ static int intel_mgbe_common_data(struct pci_dev *pdev,
 	    plat->phy_interface == PHY_INTERFACE_MODE_1000BASEX) {
 		plat->mdio_bus_data->pcs_mask = BIT_U32(INTEL_MGBE_XPCS_ADDR);
 		plat->default_an_inband = true;
+		plat->pcs_init = intel_mgbe_pcs_init;
+		plat->pcs_exit = intel_mgbe_pcs_exit;
 		plat->select_pcs = intel_mgbe_select_pcs;
 	}
 
diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-renesas-gbeth.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-renesas-gbeth.c
index 19f34e18bfef2..9af32c26f9c14 100644
--- a/drivers/net/ethernet/stmicro/stmmac/dwmac-renesas-gbeth.c
+++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-renesas-gbeth.c
@@ -81,8 +81,11 @@ static int renesas_gmac_pcs_init(struct stmmac_priv *priv)
 
 static void renesas_gmac_pcs_exit(struct stmmac_priv *priv)
 {
-	if (priv->hw->phylink_pcs)
-		miic_destroy(priv->hw->phylink_pcs);
+	if (!priv->hw->phylink_pcs)
+		return;
+
+	miic_destroy(priv->hw->phylink_pcs);
+	priv->hw->phylink_pcs = NULL;
 }
 
 static struct phylink_pcs *renesas_gmac_select_pcs(struct stmmac_priv *priv,
diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-rzn1.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-rzn1.c
index 13634965bc19a..01df4776edb3f 100644
--- a/drivers/net/ethernet/stmicro/stmmac/dwmac-rzn1.c
+++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-rzn1.c
@@ -35,8 +35,11 @@ static int rzn1_dwmac_pcs_init(struct stmmac_priv *priv)
 
 static void rzn1_dwmac_pcs_exit(struct stmmac_priv *priv)
 {
-	if (priv->hw->phylink_pcs)
-		miic_destroy(priv->hw->phylink_pcs);
+	if (!priv->hw->phylink_pcs)
+		return;
+
+	miic_destroy(priv->hw->phylink_pcs);
+	priv->hw->phylink_pcs = NULL;
 }
 
 static struct phylink_pcs *rzn1_dwmac_select_pcs(struct stmmac_priv *priv,
diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-socfpga.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-socfpga.c
index 1d7f0a57d2889..6d4bc1fe8f751 100644
--- a/drivers/net/ethernet/stmicro/stmmac/dwmac-socfpga.c
+++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-socfpga.c
@@ -539,8 +539,11 @@ static int socfpga_dwmac_pcs_init(struct stmmac_priv *priv)
 
 static void socfpga_dwmac_pcs_exit(struct stmmac_priv *priv)
 {
-	if (priv->hw->phylink_pcs)
-		lynx_pcs_destroy(priv->hw->phylink_pcs);
+	if (!priv->hw->phylink_pcs)
+		return;
+
+	lynx_pcs_destroy(priv->hw->phylink_pcs);
+	priv->hw->phylink_pcs = NULL;
 }
 
 static struct phylink_pcs *socfpga_dwmac_select_pcs(struct stmmac_priv *priv,
diff --git a/drivers/net/ethernet/stmicro/stmmac/stmmac_mdio.c b/drivers/net/ethernet/stmicro/stmmac/stmmac_mdio.c
index afe98ff5bdcb0..d2f77f0c223a7 100644
--- a/drivers/net/ethernet/stmicro/stmmac/stmmac_mdio.c
+++ b/drivers/net/ethernet/stmicro/stmmac/stmmac_mdio.c
@@ -426,36 +426,15 @@ int stmmac_mdio_reset(struct mii_bus *bus)
 int stmmac_pcs_setup(struct net_device *ndev)
 {
 	struct stmmac_priv *priv = netdev_priv(ndev);
-	struct fwnode_handle *devnode, *pcsnode;
-	struct dw_xpcs *xpcs = NULL;
-	int addr, ret;
-
-	devnode = dev_fwnode(priv->device);
-
-	if (priv->plat->pcs_init) {
-		ret = priv->plat->pcs_init(priv);
-	} else if (fwnode_property_present(devnode, "pcs-handle")) {
-		pcsnode = fwnode_find_reference(devnode, "pcs-handle", 0);
-		xpcs = xpcs_create_fwnode(pcsnode);
-		fwnode_handle_put(pcsnode);
-		ret = PTR_ERR_OR_ZERO(xpcs);
-	} else if (priv->plat->mdio_bus_data &&
-		   priv->plat->mdio_bus_data->pcs_mask) {
-		addr = ffs(priv->plat->mdio_bus_data->pcs_mask) - 1;
-		xpcs = xpcs_create_mdiodev(priv->mii, addr);
-		ret = PTR_ERR_OR_ZERO(xpcs);
-	} else {
+	int ret;
+
+	if (!priv->plat->pcs_init)
 		return 0;
-	}
 
+	ret = priv->plat->pcs_init(priv);
 	if (ret)
 		return dev_err_probe(priv->device, ret, "No xPCS found\n");
 
-	if (xpcs)
-		xpcs_config_eee_mult_fact(xpcs, priv->plat->mult_fact_100ns);
-
-	priv->hw->xpcs = xpcs;
-
 	return 0;
 }
 
@@ -463,14 +442,10 @@ void stmmac_pcs_clean(struct net_device *ndev)
 {
 	struct stmmac_priv *priv = netdev_priv(ndev);
 
-	if (priv->plat->pcs_exit)
-		priv->plat->pcs_exit(priv);
-
-	if (!priv->hw->xpcs)
+	if (!priv->plat->pcs_exit)
 		return;
 
-	xpcs_destroy(priv->hw->xpcs);
-	priv->hw->xpcs = NULL;
+	priv->plat->pcs_exit(priv);
 }
 
 struct stmmac_clk_rate {
-- 
2.47.3



From coiaprant at gmail.com  Sat Aug  1 07:22:26 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:26 +0800
Subject: [PATCH v2 02/10] dt-bindings: phy: rockchip: naneng-combphy: add rockchip,sgmii-mac-sel property
In-Reply-To: <20260801142244.679437-2-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
Message-ID: <20260801142244.679437-4-coiaprant@gmail.com>

On RK3568, the SGMII interface can be routed to either GMAC0 or
GMAC1 via the pipe_sgmii_mac_sel bit in the pipe GRF registers.

Add the optional "rockchip,sgmii-mac-sel" property to allow the
device tree to select which GMAC controller is used for SGMII.

The property takes a value of 0 (GMAC0) or 1 (GMAC1). The hardware
reset value is 1 (GMAC1), but this can be overridden by setting the
property to 0 for boards where SGMII is connected to GMAC0.

This is necessary for boards such as the Ariaboard Photonicat, where
the SGMII interface is connected to GMAC0 and needs to be explicitly
configured.

Signed-off-by: Coia Prant <coiaprant at gmail.com>
---
 .../bindings/phy/phy-rockchip-naneng-combphy.yaml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/Documentation/devicetree/bindings/phy/phy-rockchip-naneng-combphy.yaml b/Documentation/devicetree/bindings/phy/phy-rockchip-naneng-combphy.yaml
index 379b08bd9e97a..8e898bce9af73 100644
--- a/Documentation/devicetree/bindings/phy/phy-rockchip-naneng-combphy.yaml
+++ b/Documentation/devicetree/bindings/phy/phy-rockchip-naneng-combphy.yaml
@@ -80,6 +80,15 @@ properties:
     description:
       Some additional pipe settings are accessed through GRF regs.
 
+  rockchip,sgmii-mac-sel:
+    $ref: /schemas/types.yaml#/definitions/uint32
+    enum: [0, 1]
+    default: 1
+    description:
+      Select gmac0 or gmac1 to be used as SGMII controller.
+      The hardware reset value is GMAC1 (1). Set this to 0 to route
+      SGMII to GMAC0.
+
   "#phy-cells":
     const: 1
 
@@ -105,6 +114,10 @@ allOf:
           maxItems: 1
         reset-names:
           maxItems: 1
+        rockchip,sgmii-mac-sel: true
+    else:
+      properties:
+        rockchip,sgmii-mac-sel: false
   - if:
       properties:
         compatible:
-- 
2.47.3



From coiaprant at gmail.com  Sat Aug  1 07:22:27 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:27 +0800
Subject: [PATCH v2 03/10] phy: rockchip: naneng-combphy: add SGMII MAC selection for RK3568
In-Reply-To: <20260801142244.679437-2-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
Message-ID: <20260801142244.679437-5-coiaprant@gmail.com>

On RK3568, the SGMII interface can be routed to either GMAC0 or
GMAC1 via the GRF register pipe_sgmii_mac_sel.

Add support for this selection by introducing
the "rockchip,sgmii-mac-sel" DT property.

The hardware reset value is GMAC1 (1). If the property is set to 0,
the driver routes SGMII to GMAC0; if set to 1 (or omitted), it
remains at GMAC1.

This is necessary for boards such as the Ariaboard Photonicat, which
uses the SGMII interface connected to GMAC0.

Link: https://dl.radxa.com/rock3/docs/hw/datasheet/Rockchip%20RK3568%20TRM%20Part1%20V1.1-20210301.pdf (Page 229)
Signed-off-by: Coia Prant <coiaprant at gmail.com>
---
 drivers/phy/rockchip/phy-rockchip-naneng-combphy.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/drivers/phy/rockchip/phy-rockchip-naneng-combphy.c b/drivers/phy/rockchip/phy-rockchip-naneng-combphy.c
index 2b0f152f54709..539acd1459948 100644
--- a/drivers/phy/rockchip/phy-rockchip-naneng-combphy.c
+++ b/drivers/phy/rockchip/phy-rockchip-naneng-combphy.c
@@ -186,6 +186,7 @@ struct rockchip_combphy_grfcfg {
 	struct combphy_reg pipe_xpcs_phy_ready;
 	struct combphy_reg pipe_pcie1l0_sel;
 	struct combphy_reg pipe_pcie1l1_sel;
+	struct combphy_reg pipe_sgmii_mac_sel;
 	struct combphy_reg u3otg0_port_en;
 	struct combphy_reg u3otg1_port_en;
 };
@@ -212,6 +213,7 @@ struct rockchip_combphy_priv {
 	bool enable_ssc;
 	bool ext_refclk;
 	struct clk *refclk;
+	u32 sgmii_mac_sel;
 };
 
 static void rockchip_combphy_updatel(struct rockchip_combphy_priv *priv,
@@ -375,6 +377,9 @@ static int rockchip_combphy_parse_dt(struct device *dev, struct rockchip_combphy
 
 	priv->ext_refclk = device_property_present(dev, "rockchip,ext-refclk");
 
+	priv->sgmii_mac_sel = 1;
+	device_property_read_u32(dev, "rockchip,sgmii-mac-sel", &priv->sgmii_mac_sel);
+
 	priv->phy_rst = devm_reset_control_get_exclusive(dev, "phy");
 	/* fallback to old behaviour */
 	if (PTR_ERR(priv->phy_rst) == -ENOENT)
@@ -873,6 +878,8 @@ static int rk3568_combphy_cfg(struct rockchip_combphy_priv *priv)
 		break;
 
 	case PHY_TYPE_SGMII:
+		rockchip_combphy_param_write(priv->pipe_grf, &cfg->pipe_sgmii_mac_sel,
+					priv->sgmii_mac_sel > 0);
 		rockchip_combphy_param_write(priv->pipe_grf, &cfg->pipe_xpcs_phy_ready, true);
 		rockchip_combphy_param_write(priv->phy_grf, &cfg->pipe_phymode_sel, true);
 		rockchip_combphy_param_write(priv->phy_grf, &cfg->pipe_sel_qsgmii, true);
@@ -984,6 +991,7 @@ static const struct rockchip_combphy_grfcfg rk3568_combphy_grfcfgs = {
 	.con3_for_sata		= { 0x000c, 15, 0, 0x00, 0x4407 },
 	/* pipe-grf */
 	.pipe_con0_for_sata	= { 0x0000, 15, 0, 0x00, 0x2220 },
+	.pipe_sgmii_mac_sel	= { 0x0040, 1, 1, 0x00, 0x01 },
 	.pipe_xpcs_phy_ready	= { 0x0040, 2, 2, 0x00, 0x01 },
 	.u3otg0_port_en		= { 0x0104, 15, 0, 0x0181, 0x1100 },
 	.u3otg1_port_en		= { 0x0144, 15, 0, 0x0181, 0x1100 },
-- 
2.47.3



From coiaprant at gmail.com  Sat Aug  1 07:22:28 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:28 +0800
Subject: [PATCH v2 04/10] dt-bindings: net: pcs: add rockchip,rk3568-xpcs binding
In-Reply-To: <20260801142244.679437-2-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
Message-ID: <20260801142244.679437-6-coiaprant@gmail.com>

Add device tree binding documentation for the Synopsys DesignWare
XPCS integrated on the Rockchip RK3568 SoC.

The XPCS is accessed over the APB3 bus and internally connected to
a Naneng Combo SerDes PHY.  It supports 1000BASE-X, SGMII, and
QSGMII modes, with four MII ports.

The binding describes:
- Required properties: compatible, reg, clocks, clock-names
- Optional properties: phys, phy-names, power-domains
- pcs-mii sub-nodes for each MII port (reg 0..3)

Signed-off-by: Coia Prant <coiaprant at gmail.com>
---
 .../bindings/net/pcs/rockchip-dwxpcs.yaml     | 127 ++++++++++++++++++
 1 file changed, 127 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/net/pcs/rockchip-dwxpcs.yaml

diff --git a/Documentation/devicetree/bindings/net/pcs/rockchip-dwxpcs.yaml b/Documentation/devicetree/bindings/net/pcs/rockchip-dwxpcs.yaml
new file mode 100644
index 0000000000000..3e3f3a388d822
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/pcs/rockchip-dwxpcs.yaml
@@ -0,0 +1,127 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/pcs/rockchip-dwxpcs.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Rockchip RK3568 Synopsys DesignWare Ethernet PCS
+
+maintainers:
+  - Coia Prant <coiaprant at gmail.com>
+
+description: |
+  Rockchip RK3568 SoC integrates a Synopsys DesignWare Ethernet Physical
+  Coding Sublayer (XPCS).
+  The PCS provides an interface between the Media Access Control (MAC)
+  and the Physical Medium Attachment (PMA) sublayer through a Media
+  Independent Interface (GMII).
+
+  The XPCS is accessed over the APB3 bus and internally connected to a
+  Naneng Combo SerDes PHY.
+  It supports 1000BASE-X, SGMII and QSGMII modes.
+
+  The block contains four MII ports (pcs-mii at 0..3) that can be
+  individually enabled and routed to one of the Ethernet GMAC controllers
+  via the pcs-handle property in the MAC device tree node.
+
+properties:
+  compatible:
+    const: rockchip,rk3568-xpcs
+
+  '#address-cells':
+    const: 1
+
+  '#size-cells':
+    const: 0
+
+  reg:
+    description: |
+      Base address and size of the XPCS register space mapped over the
+      APB3 bus.
+    maxItems: 1
+
+  clocks:
+    description: |
+      Clock sources for the XPCS:
+      - csr: APB3 bus interface clock (clk_csr_i), required for register
+        access.
+      - eee: EEE clock (clk_eee_i), required for Energy Efficient
+        Ethernet (EEE) operation.
+    minItems: 2
+    maxItems: 2
+
+  clock-names:
+    items:
+      - const: csr
+      - const: eee
+
+  phys:
+    description: |
+      The phandle of SerDes PHY (Naneng Combo PHY) that provides
+      the serial lanes for 1000BASE-X / SGMII / QSGMII.
+      The SerDes must be powered on and initialised before any XPCS
+      register access.
+    maxItems: 1
+
+  phy-names:
+    const: serdes
+
+  power-domains:
+    description: |
+      Power domain for the XPCS.
+      On RK3568 this is typically the PD_PIPE power domain, which also
+      supplies the SerDes PHY.
+    maxItems: 1
+
+patternProperties:
+  "^pcs-mii@[0-3]$":
+    type: object
+    description: |
+      One of the four MII ports of the XPCS.
+      The port number is specified by the reg property (0..3).
+      The port is linked to an Ethernet MAC controller via the
+      pcs-handle property in the MAC's device tree node.
+
+    properties:
+      reg:
+        minimum: 0
+        maximum: 3
+        description: |
+          MII port number of PCS.
+
+      status: true
+
+    required:
+      - reg
+
+    additionalProperties: false
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - clock-names
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/rk3568-cru.h>
+    #include <dt-bindings/power/rk3568-power.h>
+    #include <dt-bindings/phy/phy.h>
+
+    pcs at fda00000 {
+      compatible = "rockchip,rk3568-xpcs";
+      #address-cells = <1>;
+      #size-cells = <0>;
+      reg = <0xfda00000 0x200000>;
+      clocks = <&cru PCLK_XPCS>, <&cru CLK_XPCS_EEE>;
+      clock-names = "csr", "eee";
+      phys = <&combphy2 PHY_TYPE_SGMII>;
+      phy-names = "serdes";
+      power-domains = <&power RK3568_PD_PIPE>;
+
+      pcs-mii at 0 {
+        reg = <0>;
+      };
+    };
-- 
2.47.3



From coiaprant at gmail.com  Sat Aug  1 07:22:29 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:29 +0800
Subject: [PATCH v2 05/10] arm64: dts: rockchip: rk3568: add XPCS and fixed-clock nodes
In-Reply-To: <20260801142244.679437-2-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
Message-ID: <20260801142244.679437-7-coiaprant@gmail.com>

The RK3568 SoC integrates a Synopsys DesignWare XPCS that provides
the Physical Coding Sublayer for 1000BASE-X, SGMII, and QSGMII
interfaces via its four MII ports.  Add the XPCS device node and
its pcs-mii sub-nodes to the SoC device tree.

The XPCS device is accessed via the APB3 bus at 0xfda00000 and
requires the CSR clock (PCLK_XPCS) for register access and the EEE
clock (CLK_XPCS_EEE) for Energy Efficient Ethernet operation.  The
PD_PIPE power domain must be enabled before any register access.

Also add two fixed-clock nodes (xpcs_gmac0_clk and xpcs_gmac1_clk)
providing the 125 MHz reference clock for the GMACs when operating
with XPCS.  These clocks are used as the assigned-clock-parents
for the respective GMAC nodes in board-level device trees.

The XPCS node and its pcs-mii sub-nodes are disabled by default and
must be enabled at the board level when 1000BASE-X/SGMII/QSGMII is
in use.  The fixed-clock nodes are always present and do not have a
status property, as they are static clock sources.

The XPCS node also requires a reference to the appropriate Naneng
Combo PHY via the phys property at the board level.

Signed-off-by: Coia Prant <coiaprant at gmail.com>
---
 arch/arm64/boot/dts/rockchip/rk3568.dtsi | 45 ++++++++++++++++++++++++
 1 file changed, 45 insertions(+)

diff --git a/arch/arm64/boot/dts/rockchip/rk3568.dtsi b/arch/arm64/boot/dts/rockchip/rk3568.dtsi
index 3bc653f027f1f..989e164c0eb39 100644
--- a/arch/arm64/boot/dts/rockchip/rk3568.dtsi
+++ b/arch/arm64/boot/dts/rockchip/rk3568.dtsi
@@ -110,6 +110,51 @@ sata0: sata at fc000000 {
 		status = "disabled";
 	};
 
+	xpcs: pcs at fda00000 {
+		compatible = "rockchip,rk3568-xpcs";
+		#address-cells = <1>;
+		#size-cells = <0>;
+		reg = <0x0 0xfda00000 0x0 0x200000>;
+		clocks = <&cru PCLK_XPCS>, <&cru CLK_XPCS_EEE>;
+		clock-names = "csr", "eee";
+		power-domains = <&power RK3568_PD_PIPE>;
+		status = "disabled";
+
+		xpcs_mii0: pcs-mii at 0 {
+			reg = <0>;
+			status = "disabled";
+		};
+
+		xpcs_mii1: pcs-mii at 1 {
+			reg = <1>;
+			status = "disabled";
+		};
+
+		xpcs_mii2: pcs-mii at 2 {
+			reg = <2>;
+			status = "disabled";
+		};
+
+		xpcs_mii3: pcs-mii at 3 {
+			reg = <3>;
+			status = "disabled";
+		};
+	};
+
+	xpcs_gmac0_clk: xpcs-gmac0-clock {
+		compatible = "fixed-clock";
+		clock-frequency = <125000000>;
+		clock-output-names = "clk_gmac0_xpcs_mii";
+		#clock-cells = <0>;
+	};
+
+	xpcs_gmac1_clk: xpcs-gmac1-clock {
+		compatible = "fixed-clock";
+		clock-frequency = <125000000>;
+		clock-output-names = "clk_gmac1_xpcs_mii";
+		#clock-cells = <0>;
+	};
+
 	pipe_phy_grf0: syscon at fdc70000 {
 		compatible = "rockchip,rk3568-pipe-phy-grf", "syscon";
 		reg = <0x0 0xfdc70000 0x0 0x1000>;
-- 
2.47.3



From coiaprant at gmail.com  Sat Aug  1 07:22:30 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:30 +0800
Subject: [PATCH v2 06/10] net: pcs: xpcs: add ANRESTART support for SGMII link recovery
In-Reply-To: <20260801142244.679437-2-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
Message-ID: <20260801142244.679437-8-coiaprant@gmail.com>

On some hardware using the DesignWare XPCS IP (e.g., RK3568 MAC side
SGMII), the PCS does not automatically restart auto-negotiation when the
link goes down and comes back up. Without an explicit ANRESTART, the link
stays down forever.

Add BMCR_ANRESTART in two places:
1. In xpcs_config_aneg_c37_sgmii(), when starting AN, set ANRESTART
   alongside ANENABLE to initiate a fresh negotiation.
2. In xpcs_get_state_c37_sgmii(), when link is down and AN completion is
   detected, clear the interrupt and trigger ANRESTART to restart the
   negotiation process.

This change has been verified on:
- RK3568 (Photonicat board)
- Wangxun NICs (no regression)

Tested-by: Jiawen Wu <jiawenwu at trustnetic.com>
Signed-off-by: Coia Prant <coiaprant at gmail.com>
---
 drivers/net/pcs/pcs-xpcs.c | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/drivers/net/pcs/pcs-xpcs.c b/drivers/net/pcs/pcs-xpcs.c
index 0337e2bcc0125..28e98449e8ac2 100644
--- a/drivers/net/pcs/pcs-xpcs.c
+++ b/drivers/net/pcs/pcs-xpcs.c
@@ -816,9 +816,14 @@ static int xpcs_config_aneg_c37_sgmii(struct dw_xpcs *xpcs,
 	if (ret < 0)
 		return ret;
 
+	/* Clear CL37 AN complete status */
+	ret = xpcs_write(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_INTR_STS, 0);
+	if (ret < 0)
+		return ret;
+
 	if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
 		ret = xpcs_write(xpcs, MDIO_MMD_VEND2, MII_BMCR,
-				 mdio_ctrl | BMCR_ANENABLE);
+				 mdio_ctrl | BMCR_ANENABLE | BMCR_ANRESTART);
 
 	return ret;
 }
@@ -1089,14 +1094,18 @@ static int xpcs_get_state_c37_sgmii(struct dw_xpcs *xpcs,
 			state->duplex = DUPLEX_FULL;
 		else
 			state->duplex = DUPLEX_HALF;
-
 		return 0;
 	}
 
-	/* Clear AN complete status or interrupt */
-	if (state->an_complete)
+	if (state->an_complete) {
+		/* Clear AN complete status or interrupt */
 		xpcs_write(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_INTR_STS, 0);
 
+		/* Initiate the next round of AN */
+		xpcs_modify(xpcs, MDIO_MMD_VEND2, MII_BMCR, BMCR_ANRESTART,
+				    BMCR_ANRESTART);
+	}
+
 	return 0;
 }
 
-- 
2.47.3



From coiaprant at gmail.com  Sat Aug  1 07:22:31 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:31 +0800
Subject: [PATCH v2 07/10] net: pcs: xpcs: add Rockchip RK3568 platform glue driver
In-Reply-To: <20260801142244.679437-2-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
Message-ID: <20260801142244.679437-9-coiaprant@gmail.com>

The RK3568 SoC integrates a Synopsys DesignWare XPCS that is accessed
via APB3 memory-mapped registers.
This driver provides the glue logic to make the XPCS accessible to
the generic pcs-xpcs core.

The XPCS block contains four MII ports (0..3), each of which can be
routed to GMAC0 or GMAC1 via the pcs-handle property in the MAC node.
The hardware maps these ports to different MMDs:
  - port 0: MMD 7 (ROCKCHIP_MMD_MII)
  - port 1: MMD 2 (ROCKCHIP_MMD_MII1)
  - port 2: MMD 3 (ROCKCHIP_MMD_MII2)
  - port 3: MMD 4 (ROCKCHIP_MMD_MII3)

This driver creates a virtual MDIO bus that translates MDIO operations
to APB3 register accesses, with proper address remapping for each port.
The generic xpcs driver then creates a phylink_pcs instance on top of
this bus, allowing the MAC to use the PCS via the standard phylink API.

Link: https://dl.radxa.com/rock3/docs/hw/datasheet/Rockchip%20RK3568%20TRM%20Part2%20V1.1-20210301.pdf (Page 2078)
Signed-off-by: Coia Prant <coiaprant at gmail.com>
---
 drivers/net/pcs/Kconfig         |  23 ++
 drivers/net/pcs/Makefile        |   7 +-
 drivers/net/pcs/pcs-xpcs-rk.c   | 538 ++++++++++++++++++++++++++++++++
 include/linux/pcs/pcs-xpcs-rk.h |  11 +
 4 files changed, 575 insertions(+), 4 deletions(-)
 create mode 100644 drivers/net/pcs/pcs-xpcs-rk.c
 create mode 100644 include/linux/pcs/pcs-xpcs-rk.h

diff --git a/drivers/net/pcs/Kconfig b/drivers/net/pcs/Kconfig
index e417fd66f660a..500400e3f4808 100644
--- a/drivers/net/pcs/Kconfig
+++ b/drivers/net/pcs/Kconfig
@@ -12,6 +12,29 @@ config PCS_XPCS
 	  This module provides a driver and helper functions for Synopsys
 	  DesignWare XPCS controllers.
 
+if PCS_XPCS
+
+config PCS_XPCS_PLATFORM
+	tristate "Generic XPCS controller support"
+	default PCS_XPCS
+	help
+	  Generic DWXPCS driver for platforms that don't require any
+	  platform specific code to function or is using platform
+	  data for setup.
+
+	  If you have a controller with this interface, say Y or M here.
+
+config PCS_XPCS_ROCKCHIP
+	tristate "Rockchip XPCS controller support"
+	depends on OF && (ARCH_ROCKCHIP || COMPILE_TEST)
+	depends on PM || COMPILE_TEST
+	help
+	  Support for XPCS controller on Rockchip RK356x SoC.
+
+	  If you have a Rockchip SoC with this interface, say Y or M here.
+
+endif # PCS_XPCS
+
 config PCS_LYNX
 	tristate
 	help
diff --git a/drivers/net/pcs/Makefile b/drivers/net/pcs/Makefile
index 4f7920618b900..c809b7f942a51 100644
--- a/drivers/net/pcs/Makefile
+++ b/drivers/net/pcs/Makefile
@@ -1,10 +1,9 @@
 # SPDX-License-Identifier: GPL-2.0
 # Makefile for Linux PCS drivers
 
-pcs_xpcs-$(CONFIG_PCS_XPCS)	:= pcs-xpcs.o pcs-xpcs-plat.o \
-				   pcs-xpcs-nxp.o pcs-xpcs-wx.o
-
-obj-$(CONFIG_PCS_XPCS)		+= pcs_xpcs.o
+obj-$(CONFIG_PCS_XPCS) += pcs-xpcs.o pcs-xpcs-nxp.o pcs-xpcs-wx.o
+obj-$(CONFIG_PCS_XPCS_PLATFORM) += pcs-xpcs-plat.o
+obj-$(CONFIG_PCS_XPCS_ROCKCHIP) += pcs-xpcs-rk.o
 obj-$(CONFIG_PCS_LYNX)		+= pcs-lynx.o
 obj-$(CONFIG_PCS_MTK_LYNXI)	+= pcs-mtk-lynxi.o
 obj-$(CONFIG_PCS_RZN1_MIIC)	+= pcs-rzn1-miic.o
diff --git a/drivers/net/pcs/pcs-xpcs-rk.c b/drivers/net/pcs/pcs-xpcs-rk.c
new file mode 100644
index 0000000000000..36c1557b83367
--- /dev/null
+++ b/drivers/net/pcs/pcs-xpcs-rk.c
@@ -0,0 +1,538 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Rockchip XPCS platform device driver
+ *
+ * Based on the Synopsys DesignWare XPCS platform driver.
+ * Copyright (C) 2024 Serge Semin
+ *
+ * Adapted for Rockchip SoCs, with reference to the Rockchip OEM driver.
+ * Copyright (C) 2026 Coia Prant
+ */
+
+#include <linux/atomic.h>
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/device.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/mdio.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_platform.h>
+#include <linux/pcs/pcs-xpcs-rk.h>
+#include <linux/phy.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/pm_domain.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/sizes.h>
+
+#include "pcs-xpcs.h"
+
+struct dw_xpcs_rk {
+	struct platform_device *pdev;
+	struct mii_bus *bus;
+	void __iomem *reg_base;
+	struct phy *serdes_phy;
+	struct clk *csr_clk;
+	struct clk *eee_clk;
+};
+
+static ptrdiff_t xpcs_rk_addr_format(int dev, int reg)
+{
+	return FIELD_PREP(0x70000, dev) | FIELD_PREP(0xffff, reg);
+}
+
+static int xpcs_rk_read_reg(struct dw_xpcs_rk *pxpcs, int dev, int reg)
+{
+	ptrdiff_t csr;
+	int ret;
+
+	csr = xpcs_rk_addr_format(dev, reg);
+
+	ret = pm_runtime_resume_and_get(&pxpcs->pdev->dev);
+	if (ret)
+		return ret;
+
+	ret = readl(pxpcs->reg_base + (csr << 2)) & 0xffff;
+
+	pm_runtime_put(&pxpcs->pdev->dev);
+	return ret;
+}
+
+static int xpcs_rk_write_reg(struct dw_xpcs_rk *pxpcs, int dev, int reg, u16 val)
+{
+	ptrdiff_t csr;
+	int ret;
+
+	csr = xpcs_rk_addr_format(dev, reg);
+
+	ret = pm_runtime_resume_and_get(&pxpcs->pdev->dev);
+	if (ret)
+		return ret;
+
+	writel(val, pxpcs->reg_base + (csr << 2));
+
+	pm_runtime_put(&pxpcs->pdev->dev);
+	return 0;
+}
+
+#define ROCKCHIP_MMD_MII1	2
+#define ROCKCHIP_MMD_MII2	3
+#define ROCKCHIP_MMD_MII3	4
+#define ROCKCHIP_MMD_PMAPMD	6
+#define ROCKCHIP_MMD_MII	7
+
+static bool xpcs_rk_mdio_addr_validate(int addr)
+{
+	return !(addr < 0 || addr > 3);
+}
+
+static int xpcs_rk_mdio_read_remapping(int addr, int dev, int reg)
+{
+	switch (dev) {
+	case MDIO_MMD_PMAPMD:
+		return ROCKCHIP_MMD_PMAPMD;
+	case MDIO_MMD_VEND2:
+		break;
+	default:
+		return -ENXIO;
+	}
+
+	/* read remapping to MII is performed by HW */
+	switch (addr) {
+	case 0:
+		return ROCKCHIP_MMD_MII;
+	case 1:
+		return ROCKCHIP_MMD_MII1;
+	case 2:
+		return ROCKCHIP_MMD_MII2;
+	case 3:
+		return ROCKCHIP_MMD_MII3;
+	default:
+		return -ENODEV;
+	}
+}
+
+static int xpcs_rk_mdio_write_remapping(int addr, int dev, int reg)
+{
+	switch (dev) {
+	case MDIO_MMD_PMAPMD:
+		return ROCKCHIP_MMD_PMAPMD;
+	case MDIO_MMD_VEND2:
+		break;
+	default:
+		return -ENXIO;
+	}
+
+	/* Writable only on MII */
+	switch (reg) {
+	case DW_VR_MII_AN_CTRL:
+	case DW_VR_MII_AN_INTR_STS:
+	case DW_VR_MII_EEE_MCTRL0:
+	case DW_VR_MII_EEE_MCTRL1:
+	case DW_VR_MII_DIG_CTRL2:
+		return ROCKCHIP_MMD_MII;
+	default:
+		break;
+	}
+
+	switch (addr) {
+	case 0:
+		return ROCKCHIP_MMD_MII;
+	case 1:
+		return ROCKCHIP_MMD_MII1;
+	case 2:
+		return ROCKCHIP_MMD_MII2;
+	case 3:
+		return ROCKCHIP_MMD_MII3;
+	default:
+		return -ENODEV;
+	}
+}
+
+static int xpcs_rk_read_c22(struct mii_bus *bus, int addr, int reg)
+{
+	struct dw_xpcs_rk *pxpcs = bus->priv;
+	int dev;
+
+	if (!xpcs_rk_mdio_addr_validate(addr))
+		return -ENODEV;
+
+	dev = xpcs_rk_mdio_read_remapping(addr, MDIO_MMD_VEND2, reg);
+	if (dev < 0)
+		return 0xffff;
+
+	return xpcs_rk_read_reg(pxpcs, dev, reg);
+}
+
+static int xpcs_rk_write_c22(struct mii_bus *bus, int addr, int reg, u16 val)
+{
+	struct dw_xpcs_rk *pxpcs = bus->priv;
+	int dev;
+
+	if (!xpcs_rk_mdio_addr_validate(addr))
+		return -ENODEV;
+
+	dev = xpcs_rk_mdio_write_remapping(addr, MDIO_MMD_VEND2, reg);
+	if (dev < 0)
+		return 0;
+
+	return xpcs_rk_write_reg(pxpcs, dev, reg, val);
+}
+
+static int xpcs_rk_read_c45(struct mii_bus *bus, int addr, int dev, int reg)
+{
+	struct dw_xpcs_rk *pxpcs = bus->priv;
+
+	if (!xpcs_rk_mdio_addr_validate(addr))
+		return -ENODEV;
+
+	dev = xpcs_rk_mdio_read_remapping(addr, dev, reg);
+	if (dev < 0)
+		return 0xffff;
+
+	return xpcs_rk_read_reg(pxpcs, dev, reg);
+}
+
+static int xpcs_rk_write_c45(struct mii_bus *bus, int addr, int dev, int reg, u16 val)
+{
+	struct dw_xpcs_rk *pxpcs = bus->priv;
+
+	if (!xpcs_rk_mdio_addr_validate(addr))
+		return -ENODEV;
+
+	dev = xpcs_rk_mdio_write_remapping(addr, dev, reg);
+	if (dev < 0)
+		return 0;
+
+	return xpcs_rk_write_reg(pxpcs, dev, reg, val);
+}
+
+static struct dw_xpcs_rk *xpcs_rk_create_data(struct platform_device *pdev)
+{
+	struct dw_xpcs_rk *pxpcs;
+
+	pxpcs = devm_kzalloc(&pdev->dev, sizeof(*pxpcs), GFP_KERNEL);
+	if (!pxpcs)
+		return ERR_PTR(-ENOMEM);
+
+	pxpcs->pdev = pdev;
+
+	dev_set_drvdata(&pdev->dev, pxpcs);
+
+	return pxpcs;
+}
+
+static int xpcs_rk_serdes_phy_init(struct dw_xpcs_rk *pxpcs)
+{
+	struct device *dev = &pxpcs->pdev->dev;
+
+	pxpcs->serdes_phy = devm_phy_get(dev, "serdes");
+	if (IS_ERR(pxpcs->serdes_phy))
+		return dev_err_probe(dev, PTR_ERR(pxpcs->serdes_phy),
+					"Failed to get SerDes PHY\n");
+
+	return 0;
+}
+
+static void xpcs_rk_serdes_phy_poweroff(void *data)
+{
+	struct dw_xpcs_rk *pxpcs = data;
+	struct device *dev = &pxpcs->pdev->dev;
+
+	phy_power_off(pxpcs->serdes_phy);
+	phy_exit(pxpcs->serdes_phy);
+
+	dev_pm_genpd_rpm_always_on(dev, false);
+}
+
+static int xpcs_rk_serdes_phy_poweron(struct dw_xpcs_rk *pxpcs)
+{
+	struct device *dev = &pxpcs->pdev->dev;
+	int ret;
+
+	ret = dev_pm_genpd_rpm_always_on(dev, true);
+	if (ret) {
+		dev_err(dev, "Failed to power on power-domains\n");
+		return ret;
+	}
+
+	ret = phy_init(pxpcs->serdes_phy);
+	if (ret) {
+		dev_err(dev, "Failed to init SerDes PHY\n");
+		goto pm_domain;
+	}
+
+	ret = phy_power_on(pxpcs->serdes_phy);
+	if (ret) {
+		dev_err(dev, "Failed to power on SerDes PHY\n");
+		goto serdes_phy;
+	}
+
+	ret = devm_add_action_or_reset(dev, xpcs_rk_serdes_phy_poweroff, pxpcs);
+	if (ret) {
+		dev_err(dev, "Failed to register devm for SerDes PHY: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+
+serdes_phy:
+	phy_exit(pxpcs->serdes_phy);
+pm_domain:
+	dev_pm_genpd_rpm_always_on(dev, false);
+	return ret;
+}
+
+static int xpcs_rk_init_res(struct dw_xpcs_rk *pxpcs)
+{
+	struct platform_device *pdev = pxpcs->pdev;
+	struct device *dev = &pdev->dev;
+	struct resource *res;
+
+	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	if (!res) {
+		dev_err(dev, "No reg-space found\n");
+		return -EINVAL;
+	}
+
+	if (resource_size(res) < SZ_2M) {
+		dev_err(dev, "Invalid reg-space size\n");
+		return -EINVAL;
+	}
+
+	pxpcs->reg_base = devm_ioremap_resource(dev, res);
+	if (IS_ERR(pxpcs->reg_base)) {
+		dev_err(dev, "Failed to map reg-space\n");
+		return PTR_ERR(pxpcs->reg_base);
+	}
+
+	return 0;
+}
+
+static void xpcs_rk_exit_clk(void *data)
+{
+	struct dw_xpcs_rk *pxpcs = data;
+
+	clk_disable_unprepare(pxpcs->eee_clk);
+}
+
+static int xpcs_rk_init_clk(struct dw_xpcs_rk *pxpcs)
+{
+	struct device *dev = &pxpcs->pdev->dev;
+	int ret;
+
+	pxpcs->csr_clk = devm_clk_get(dev, "csr");
+	if (IS_ERR(pxpcs->csr_clk))
+		return dev_err_probe(dev, PTR_ERR(pxpcs->csr_clk),
+					 "Failed to get CSR clock\n");
+
+	pm_runtime_set_suspended(dev);
+	ret = devm_pm_runtime_enable(dev);
+	if (ret) {
+		dev_err(dev, "Failed to enable runtime-PM\n");
+		return ret;
+	}
+
+	pxpcs->eee_clk = devm_clk_get(dev, "eee");
+	if (IS_ERR(pxpcs->eee_clk))
+		return dev_err_probe(dev, PTR_ERR(pxpcs->eee_clk),
+					 "Failed to get EEE clock\n");
+
+	ret = clk_prepare_enable(pxpcs->eee_clk);
+	if (ret) {
+		dev_err(dev, "Failed to enable EEE clock\n");
+		return ret;
+	}
+
+	ret = devm_add_action_or_reset(dev, xpcs_rk_exit_clk, pxpcs);
+	if (ret) {
+		dev_err(dev, "Failed to register devm for EEE clock: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int xpcs_rk_init_bus(struct dw_xpcs_rk *pxpcs)
+{
+	struct device *dev = &pxpcs->pdev->dev;
+	static atomic_t id = ATOMIC_INIT(-1);
+	struct mii_bus *bus;
+	int ret;
+
+	bus = devm_mdiobus_alloc_size(dev, 0);
+	if (!bus)
+		return -ENOMEM;
+
+	bus->name = "Rockchip DW XPCS MCI/APB3";
+	bus->read = xpcs_rk_read_c22;
+	bus->write = xpcs_rk_write_c22;
+	bus->read_c45 = xpcs_rk_read_c45;
+	bus->write_c45 = xpcs_rk_write_c45;
+	bus->phy_mask = ~0;
+	bus->parent = dev;
+	bus->priv = pxpcs;
+
+	snprintf(bus->id, MII_BUS_ID_SIZE,
+		 "rockchip_dwxpcs-%x", atomic_inc_return(&id));
+
+	/*
+	 * MDIO-bus here serves as just a back-end engine abstracting out
+	 * the MDIO and MCI/APB3 IO interfaces utilized for the Rockchip DWXPCS CSRs
+	 * access.
+	 */
+	ret = devm_mdiobus_register(dev, bus);
+	if (ret) {
+		dev_err(dev, "Failed to create MDIO bus\n");
+		return ret;
+	}
+
+	pxpcs->bus = bus;
+	return 0;
+}
+
+static int xpcs_rk_probe(struct platform_device *pdev)
+{
+	struct dw_xpcs_rk *pxpcs;
+	int ret;
+
+	pxpcs = xpcs_rk_create_data(pdev);
+	if (IS_ERR(pxpcs))
+		return PTR_ERR(pxpcs);
+
+	/*
+	 * The XPCS may be attached to a power domain (e.g. PD_PIPE). The domain
+	 * must be powered on before any register access, otherwise the SoC will
+	 * trigger a synchronous external abort (SError).
+	 *
+	 * Accessing the XPCS registers also requires a TX clock from the SerDes,
+	 * which is needed for the soft reset.
+	 */
+	ret = xpcs_rk_serdes_phy_init(pxpcs);
+	if (ret)
+		return ret;
+
+	ret = xpcs_rk_serdes_phy_poweron(pxpcs);
+	if (ret)
+		return ret;
+
+	ret = xpcs_rk_init_res(pxpcs);
+	if (ret)
+		return ret;
+
+	ret = xpcs_rk_init_clk(pxpcs);
+	if (ret)
+		return ret;
+
+	ret = xpcs_rk_init_bus(pxpcs);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void xpcs_rk_remove(struct platform_device *pdev)
+{
+	pm_runtime_force_suspend(&pdev->dev);
+}
+
+static const struct of_device_id xpcs_rk_of_ids[] = {
+	{ .compatible = "rockchip,rk3568-xpcs" },
+	{ /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, xpcs_rk_of_ids);
+
+struct dw_xpcs *xpcs_rk_create(struct device *dev, struct device_node *np)
+{
+	struct platform_device *pdev;
+	struct device_node *pcs_np;
+	struct dw_xpcs_rk *pxpcs;
+	struct dw_xpcs *xpcs;
+	u32 port;
+
+	if (!of_device_is_available(np))
+		return ERR_PTR(-ENODEV);
+
+	if (of_property_read_u32(np, "reg", &port))
+		return ERR_PTR(-EINVAL);
+
+	if (!xpcs_rk_mdio_addr_validate((int)port))
+		return ERR_PTR(-EINVAL);
+
+	/* The XPCS pdev is attached to the parent node */
+	pcs_np = of_get_parent(np);
+	if (!pcs_np)
+		return ERR_PTR(-ENODEV);
+
+	if (!of_device_is_available(pcs_np)) {
+		of_node_put(pcs_np);
+		return ERR_PTR(-ENODEV);
+	}
+
+	if (!of_match_node(xpcs_rk_of_ids, pcs_np)) {
+		of_node_put(pcs_np);
+		return ERR_PTR(-EINVAL);
+	}
+
+	pdev = of_find_device_by_node(pcs_np);
+	of_node_put(pcs_np);
+	if (!pdev)
+		return ERR_PTR(-EPROBE_DEFER);
+
+	pxpcs = platform_get_drvdata(pdev);
+	if (!pxpcs || !pxpcs->bus) {
+		put_device(&pdev->dev);
+		return ERR_PTR(-EPROBE_DEFER);
+	}
+
+	xpcs = xpcs_create_mdiodev(pxpcs->bus, (int)port);
+	if (IS_ERR(xpcs)) {
+		put_device(&pdev->dev);
+		return xpcs;
+	}
+
+	device_link_add(dev, &pdev->dev, DL_FLAG_AUTOREMOVE_CONSUMER);
+	put_device(&pdev->dev);
+	return xpcs;
+}
+EXPORT_SYMBOL_GPL(xpcs_rk_create);
+
+static int xpcs_rk_pm_runtime_suspend(struct device *dev)
+{
+	struct dw_xpcs_rk *pxpcs = dev_get_drvdata(dev);
+
+	clk_disable_unprepare(pxpcs->csr_clk);
+
+	return 0;
+}
+
+static int xpcs_rk_pm_runtime_resume(struct device *dev)
+{
+	struct dw_xpcs_rk *pxpcs = dev_get_drvdata(dev);
+
+	return clk_prepare_enable(pxpcs->csr_clk);
+}
+
+static const struct dev_pm_ops xpcs_rk_pm_ops = {
+	SET_RUNTIME_PM_OPS(xpcs_rk_pm_runtime_suspend,
+			   xpcs_rk_pm_runtime_resume,
+			   NULL)
+};
+
+static struct platform_driver xpcs_rk_driver = {
+	.probe = xpcs_rk_probe,
+	.remove = xpcs_rk_remove,
+	.driver = {
+		.name = "rk_xpcs-dwxpcs",
+		.pm = &xpcs_rk_pm_ops,
+		.of_match_table = xpcs_rk_of_ids,
+	},
+};
+module_platform_driver(xpcs_rk_driver);
+
+MODULE_DESCRIPTION("Rockchip XPCS platform device driver");
+MODULE_AUTHOR("Coia Prant <coiaprant at gmail.com>");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/pcs/pcs-xpcs-rk.h b/include/linux/pcs/pcs-xpcs-rk.h
new file mode 100644
index 0000000000000..28723d5bd75cc
--- /dev/null
+++ b/include/linux/pcs/pcs-xpcs-rk.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __LINUX_PCS_XPCS_ROCKCHIP_H
+#define __LINUX_PCS_XPCS_ROCKCHIP_H
+
+#include <linux/device.h>
+#include <linux/of.h>
+#include <linux/pcs/pcs-xpcs.h>
+
+struct dw_xpcs *xpcs_rk_create(struct device *dev, struct device_node *np);
+
+#endif /* __LINUX_PCS_XPCS_ROCKCHIP_H */
-- 
2.47.3



From coiaprant at gmail.com  Sat Aug  1 07:22:32 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:32 +0800
Subject: [PATCH v2 08/10] net: stmmac: dwmac-rk: add SGMII support for RK3568
In-Reply-To: <20260801142244.679437-2-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
Message-ID: <20260801142244.679437-10-coiaprant@gmail.com>

The RK3568 SoC integrates a Synopsys DesignWare XPCS that can be
connected to GMAC0 or GMAC1 in SGMII mode.  Add the necessary glue
logic to support this configuration.

The current dwmac-rk driver does not support SGMII mode.  SGMII
requires a PCS to handle auto-negotiation and link state reporting,
but the existing driver only supports RGMII and RMII.

Add a set_to_sgmii() callback to configure the GMAC GRF register for
SGMII mode (bit 7 set, interface selection bits 4:6 cleared).  Also
add a supports_sgmii flag to indicate SGMII capability.

Provide pcs_init/pcs_exit callbacks to create/destroy the XPCS via
xpcs_rk_create() from the Rockchip XPCS platform driver, and a
select_pcs callback to return the XPCS to phylink.

While at it, fix the clock enable ordering in rk_gmac_powerup():
gmac_clk_enable() is now called before any register access, including
the SGMII mode setup path.  Previously SGMII mode would bypass the
clock enable via a goto, which could cause synchronous external abort
when accessing MAC registers with the clock domain disabled.

Also clean up the error handling in rk_gmac_powerup() by using a
unified clk_disable label, and add error handling for the default
(unhandled interface) case.

SGMII In-band vs Out-of-band
============================
On RK3568, the MAC clock is fixed at 125 MHz and cannot be dynamically
changed by the stmmac core's set_clk_tx_rate callback.  In-band mode
works because the PCS handles rate adaptation internally.  Out-of-band
mode does not work because the MAC would need to change the clock rate
to 125/12.5/1.25 MHz for 1000/100/10 Mbps respectively, and the clock
is fixed.

Enable default_an_inband for SGMII and disable the generic stmmac
set_clk_tx_rate callback.  This forces phylink to use in-band mode,
where the PCS is responsible for speed/duplex negotiation.  Without
this, the stmmac core would attempt to change the clock rate on speed
changes, causing TX to work but RX to fail.

Link: https://dl.radxa.com/rock3/docs/hw/datasheet/Rockchip%20RK3568%20TRM%20Part1%20V1.1-20210301.pdf (Page 386)
Signed-off-by: Coia Prant <coiaprant at gmail.com>
---
 drivers/net/ethernet/stmicro/stmmac/Kconfig   |   1 +
 .../net/ethernet/stmicro/stmmac/dwmac-rk.c    | 121 +++++++++++++++---
 2 files changed, 104 insertions(+), 18 deletions(-)

diff --git a/drivers/net/ethernet/stmicro/stmmac/Kconfig b/drivers/net/ethernet/stmicro/stmmac/Kconfig
index e3dd5adda5aca..5088acc06982e 100644
--- a/drivers/net/ethernet/stmicro/stmmac/Kconfig
+++ b/drivers/net/ethernet/stmicro/stmmac/Kconfig
@@ -170,6 +170,7 @@ config DWMAC_ROCKCHIP
 	default ARCH_ROCKCHIP
 	depends on OF && (ARCH_ROCKCHIP || COMPILE_TEST)
 	select MFD_SYSCON
+	select PCS_XPCS_ROCKCHIP
 	help
 	  Support for Ethernet controller on Rockchip RK3288 SoC.
 
diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c
index 8d7042e689261..e47ca1bec5b8b 100644
--- a/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c
+++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c
@@ -20,6 +20,7 @@
 #include <linux/delay.h>
 #include <linux/mfd/syscon.h>
 #include <linux/regmap.h>
+#include <linux/pcs/pcs-xpcs-rk.h>
 #include <linux/pm_runtime.h>
 
 #include "stmmac_platform.h"
@@ -47,6 +48,7 @@ struct rk_gmac_ops {
 	void (*set_to_rgmii)(struct rk_priv_data *bsp_priv,
 			     int tx_delay, int rx_delay);
 	void (*set_to_rmii)(struct rk_priv_data *bsp_priv);
+	void (*set_to_sgmii)(struct rk_priv_data *bsp_priv);
 	int (*set_speed)(struct rk_priv_data *bsp_priv,
 			 phy_interface_t interface, int speed);
 	void (*integrated_phy_powerup)(struct rk_priv_data *bsp_priv);
@@ -63,6 +65,7 @@ struct rk_gmac_ops {
 	bool clock_grf_reg_in_php;
 	bool supports_rgmii;
 	bool supports_rmii;
+	bool supports_sgmii;
 	bool php_grf_required;
 	bool regs_valid;
 	u32 regs[];
@@ -98,6 +101,7 @@ struct rk_priv_data {
 	bool integrated_phy;
 	bool supports_rgmii;
 	bool supports_rmii;
+	bool supports_sgmii;
 
 	struct clk_bulk_data *clks;
 	int num_clks;
@@ -809,6 +813,8 @@ static const struct rk_gmac_ops rk3528_ops = {
 #define RK3568_GRF_GMAC1_CON1		0x038c
 
 /* RK3568_GRF_GMAC0_CON1 && RK3568_GRF_GMAC1_CON1 */
+#define RK3568_GMAC_MODE_RMII_RGMII		GRF_CLR_BIT(7)
+#define RK3568_GMAC_MODE_SGMII_QSGMII		GRF_BIT(7)
 #define RK3568_GMAC_FLOW_CTRL			GRF_BIT(3)
 #define RK3568_GMAC_FLOW_CTRL_CLR		GRF_CLR_BIT(3)
 #define RK3568_GMAC_RXCLK_DLY_ENABLE		GRF_BIT(1)
@@ -851,18 +857,32 @@ static void rk3568_set_to_rgmii(struct rk_priv_data *bsp_priv,
 		     RK3568_GMAC_CLK_TX_DL_CFG(tx_delay));
 
 	regmap_write(bsp_priv->grf, con1,
+		     RK3568_GMAC_MODE_RMII_RGMII |
 		     RK3568_GMAC_RXCLK_DLY_ENABLE |
 		     RK3568_GMAC_TXCLK_DLY_ENABLE);
 }
 
+static void rk3568_set_to_sgmii(struct rk_priv_data *bsp_priv)
+{
+	u32 con1;
+
+	con1 = (bsp_priv->id == 1) ? RK3568_GRF_GMAC1_CON1 :
+				     RK3568_GRF_GMAC0_CON1;
+
+	regmap_write(bsp_priv->grf, con1, RK3568_GMAC_MODE_SGMII_QSGMII);
+}
+
 static const struct rk_gmac_ops rk3568_ops = {
 	.init = rk3568_init,
 	.set_to_rgmii = rk3568_set_to_rgmii,
+	.set_to_sgmii = rk3568_set_to_sgmii,
+
 	.set_speed = rk_set_clk_mac_speed,
 
 	.gmac_phy_intf_sel_mask = GENMASK_U16(6, 4),
 
 	.supports_rmii = true,
+	.supports_sgmii = true,
 
 	.regs_valid = true,
 	.regs = {
@@ -1208,6 +1228,43 @@ static void rk_phy_powerdown(struct rk_priv_data *bsp_priv)
 		dev_err(bsp_priv->dev, "fail to disable phy-supply\n");
 }
 
+static int rk_pcs_init(struct stmmac_priv *priv)
+{
+	struct device_node *np = priv->device->of_node;
+	struct device_node *pcs_node;
+	struct dw_xpcs *xpcs;
+
+	pcs_node = of_parse_phandle(np, "pcs-handle", 0);
+	if (!pcs_node)
+		return -ENODEV;
+
+	xpcs = xpcs_rk_create(priv->device, pcs_node);
+	of_node_put(pcs_node);
+	if (IS_ERR(xpcs))
+		return PTR_ERR(xpcs);
+
+	priv->hw->xpcs = xpcs;
+	return 0;
+}
+
+static void rk_pcs_exit(struct stmmac_priv *priv)
+{
+	if (!priv->hw->xpcs)
+		return;
+
+	xpcs_destroy(priv->hw->xpcs);
+	priv->hw->xpcs = NULL;
+}
+
+static struct phylink_pcs *rk_select_pcs(struct stmmac_priv *priv,
+					 phy_interface_t interface)
+{
+	if (!priv->hw->xpcs)
+		return NULL;
+
+	return xpcs_to_phylink_pcs(priv->hw->xpcs);
+}
+
 static struct rk_priv_data *rk_gmac_setup(struct platform_device *pdev,
 					  struct plat_stmmacenet_data *plat,
 					  const struct rk_gmac_ops *ops)
@@ -1330,6 +1387,7 @@ static struct rk_priv_data *rk_gmac_setup(struct platform_device *pdev,
 
 	bsp_priv->supports_rgmii = ops->supports_rgmii || !!ops->set_to_rgmii;
 	bsp_priv->supports_rmii = ops->supports_rmii || !!ops->set_to_rmii;
+	bsp_priv->supports_sgmii = ops->supports_sgmii || !!ops->set_to_sgmii;
 
 	if (ops->init) {
 		ret = ops->init(bsp_priv);
@@ -1361,6 +1419,10 @@ static int rk_gmac_check_ops(struct rk_priv_data *bsp_priv)
 		if (!bsp_priv->supports_rmii)
 			return -EINVAL;
 		break;
+	case PHY_INTERFACE_MODE_SGMII:
+		if (!bsp_priv->supports_sgmii)
+			return -EINVAL;
+		break;
 	default:
 		dev_err(bsp_priv->dev,
 			"unsupported interface %d", bsp_priv->phy_iface);
@@ -1379,16 +1441,19 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv)
 	if (ret)
 		return ret;
 
+	ret = gmac_clk_enable(bsp_priv, true);
+	if (ret)
+		return ret;
+
+	if (bsp_priv->phy_iface == PHY_INTERFACE_MODE_SGMII)
+		goto set_mode;
+
 	ret = rk_get_phy_intf_sel(bsp_priv->phy_iface);
 	if (ret < 0)
-		return ret;
+		goto clk_disable;
 
 	intf = ret;
 
-	ret = gmac_clk_enable(bsp_priv, true);
-	if (ret)
-		return ret;
-
 	if (bsp_priv->gmac_phy_intf_sel_mask ||
 	    bsp_priv->gmac_rmii_mode_mask) {
 		/* If defined, encode the phy_intf_sel value */
@@ -1399,10 +1464,8 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv)
 				      bsp_priv->gmac_rmii_mode_mask);
 
 		ret = rk_write_gmac_grf_reg(bsp_priv, val);
-		if (ret < 0) {
-			gmac_clk_enable(bsp_priv, false);
-			return ret;
-		}
+		if (ret < 0)
+			goto clk_disable;
 	}
 
 	if (bsp_priv->clock.rmii_mode_mask) {
@@ -1410,13 +1473,12 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv)
 				     bsp_priv->clock.rmii_mode_mask);
 
 		ret = rk_write_clock_grf_reg(bsp_priv, val);
-		if (ret < 0) {
-			gmac_clk_enable(bsp_priv, false);
-			return ret;
-		}
+		if (ret < 0)
+			goto clk_disable;
 	}
 
-	/*rmii or rgmii*/
+set_mode:
+	/* rmii, rgmii, sgmii */
 	switch (bsp_priv->phy_iface) {
 	case PHY_INTERFACE_MODE_RGMII:
 		dev_info(dev, "init for RGMII\n");
@@ -1447,15 +1509,20 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv)
 		if (bsp_priv->ops->set_to_rmii)
 			bsp_priv->ops->set_to_rmii(bsp_priv);
 		break;
+	case PHY_INTERFACE_MODE_SGMII:
+		dev_info(dev, "init for SGMII\n");
+		if (bsp_priv->ops->set_to_sgmii)
+			bsp_priv->ops->set_to_sgmii(bsp_priv);
+		break;
 	default:
 		dev_err(dev, "NO interface defined!\n");
+		ret = -EINVAL;
+		goto clk_disable;
 	}
 
 	ret = rk_phy_powerup(bsp_priv);
-	if (ret) {
-		gmac_clk_enable(bsp_priv, false);
-		return ret;
-	}
+	if (ret)
+		goto clk_disable;
 
 	pm_runtime_get_sync(dev);
 
@@ -1463,6 +1530,10 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv)
 		bsp_priv->ops->integrated_phy_powerup(bsp_priv);
 
 	return 0;
+
+clk_disable:
+	gmac_clk_enable(bsp_priv, false);
+	return ret;
 }
 
 static void rk_gmac_powerdown(struct rk_priv_data *gmac)
@@ -1486,6 +1557,9 @@ static void rk_get_interfaces(struct stmmac_priv *priv, void *bsp_priv,
 
 	if (rk->supports_rmii)
 		__set_bit(PHY_INTERFACE_MODE_RMII, interfaces);
+
+	if (rk->supports_sgmii)
+		__set_bit(PHY_INTERFACE_MODE_SGMII, interfaces);
 }
 
 static int rk_set_clk_tx_rate(void *bsp_priv_, struct clk *clk_tx_i,
@@ -1602,6 +1676,17 @@ static int rk_gmac_probe(struct platform_device *pdev)
 	plat_dat->suspend = rk_gmac_suspend;
 	plat_dat->resume = rk_gmac_resume;
 
+	if (plat_dat->phy_interface == PHY_INTERFACE_MODE_SGMII) {
+		/* SGMII clock always runs at 125 MHz */
+		plat_dat->set_clk_tx_rate = NULL;
+
+		/* SGMII requires a PCS */
+		plat_dat->default_an_inband = true;
+		plat_dat->pcs_init = rk_pcs_init;
+		plat_dat->pcs_exit = rk_pcs_exit;
+		plat_dat->select_pcs = rk_select_pcs;
+	}
+
 	plat_dat->bsp_priv = rk_gmac_setup(pdev, plat_dat, data);
 	if (IS_ERR(plat_dat->bsp_priv))
 		return PTR_ERR(plat_dat->bsp_priv);
-- 
2.47.3



From coiaprant at gmail.com  Sat Aug  1 07:22:33 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:33 +0800
Subject: [PATCH v2 09/10] arm64: dts: rockchip: rk3568-photonicat: enable SGMII LAN port
In-Reply-To: <20260801142244.679437-2-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
Message-ID: <20260801142244.679437-11-coiaprant@gmail.com>

The Ariaboard Photonicat has a Motorcomm YT8521SC Gigabit Ethernet PHY
connected to GMAC0 via XPCS SGMII.  Enable the necessary nodes to make
this port functional.

Enable combphy2 with rockchip,sgmii-mac-sel = <0> to route the SGMII
interface to GMAC0.  Enable the xpcs node and its port 0 sub-node,
referencing combphy2 as the SerDes PHY.

Add the mdio0 node with the YT8521SC PHY at address 3, including its
reset GPIO and LED configuration.  Also add LED configuration for the
existing RGMII PHY on mdio1 for consistency.

Signed-off-by: Coia Prant <coiaprant at gmail.com>
---
 .../boot/dts/rockchip/rk3568-photonicat.dts   | 77 ++++++++++++++++++-
 1 file changed, 75 insertions(+), 2 deletions(-)

diff --git a/arch/arm64/boot/dts/rockchip/rk3568-photonicat.dts b/arch/arm64/boot/dts/rockchip/rk3568-photonicat.dts
index 58c1052ba8ef3..91c17b624fd17 100644
--- a/arch/arm64/boot/dts/rockchip/rk3568-photonicat.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3568-photonicat.dts
@@ -3,6 +3,7 @@
 /dts-v1/;
 
 #include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/leds/common.h>
 #include <dt-bindings/pinctrl/rockchip.h>
 #include <dt-bindings/soc/rockchip,vop2.h>
 #include "rk3568.dtsi"
@@ -242,6 +243,7 @@ &combphy1 {
 
 &combphy2 {
 	status = "okay";
+	rockchip,sgmii-mac-sel = <0>;
 };
 
 &cpu0 {
@@ -260,9 +262,18 @@ &cpu3 {
 	cpu-supply = <&vdd_cpu>;
 };
 
-/* Motorcomm YT8521SC LAN port (require SGMII) */
+/* Motorcomm YT8521SC LAN port */
 &gmac0 {
-	status = "disabled";
+	assigned-clocks = <&cru SCLK_GMAC0_RX_TX>;
+	assigned-clock-parents = <&xpcs_gmac0_clk>;
+	pcs-handle = <&xpcs_mii0>;
+	managed = "in-band-status";
+	phy-handle = <&sgmii_phy>;
+	phy-mode = "sgmii";
+	phy-supply = <&vcc_3v3>;
+	pinctrl-names = "default";
+	pinctrl-0 = <&gmac0_miim>;
+	status = "okay";
 };
 
 /* Motorcomm YT8521SC WAN port */
@@ -341,6 +352,39 @@ &i2s0_8ch {
 	status = "okay";
 };
 
+&mdio0 {
+	sgmii_phy: ethernet-phy at 3 {
+		compatible = "ethernet-phy-id0000.011a";
+		reg = <0x3>;
+		max-speed = <1000>;
+		eee-broken-10gt;
+		eee-broken-10gkx4;
+		eee-broken-10gkr;
+		reset-assert-us = <20000>;
+		reset-deassert-us = <100000>;
+		reset-gpios = <&gpio3 RK_PC6 GPIO_ACTIVE_LOW>;
+
+		leds {
+			#address-cells = <1>;
+			#size-cells = <0>;
+
+			led at 1 {
+				reg = <1>;
+				color = <LED_COLOR_ID_AMBER>;
+				function = LED_FUNCTION_LAN;
+				default-state = "keep";
+			};
+
+			led at 2 {
+				reg = <2>;
+				color = <LED_COLOR_ID_GREEN>;
+				function = LED_FUNCTION_LAN;
+				default-state = "keep";
+			};
+		};
+	};
+};
+
 &mdio1 {
 	rgmii_phy: ethernet-phy at 3 {
 		compatible = "ethernet-phy-ieee802.3-c22";
@@ -350,6 +394,25 @@ rgmii_phy: ethernet-phy at 3 {
 		reset-gpios = <&gpio4 RK_PC0 GPIO_ACTIVE_LOW>;
 		rx-internal-delay-ps = <1500>;
 		tx-internal-delay-ps = <1500>;
+
+		leds {
+			#address-cells = <1>;
+			#size-cells = <0>;
+
+			led at 1 {
+				reg = <1>;
+				color = <LED_COLOR_ID_AMBER>;
+				function = LED_FUNCTION_WAN;
+				default-state = "keep";
+			};
+
+			led at 2 {
+				reg = <2>;
+				color = <LED_COLOR_ID_GREEN>;
+				function = LED_FUNCTION_WAN;
+				default-state = "keep";
+			};
+		};
 	};
 };
 
@@ -586,3 +649,13 @@ &xin32k {
 	pinctrl-names = "default";
 	pinctrl-0 = <&clk32k_out1>;
 };
+
+&xpcs {
+	status = "okay";
+	phys = <&combphy2 PHY_TYPE_SGMII>;
+	phy-names = "serdes";
+};
+
+&xpcs_mii0 {
+	status = "okay";
+};
-- 
2.47.3



From coiaprant at gmail.com  Sat Aug  1 07:22:34 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sat,  1 Aug 2026 22:22:34 +0800
Subject: [PATCH v2 10/10] MAINTAINERS: add entry for Rockchip XPCS driver
In-Reply-To: <20260801142244.679437-2-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
Message-ID: <20260801142244.679437-12-coiaprant@gmail.com>

Add a MAINTAINERS entry for the Rockchip RK3568 XPCS platform driver
and its device tree binding.

Include the relevant mailing lists (netdev and linux-rockchip) so that
future patches are properly distributed.

Signed-off-by: Coia Prant <coiaprant at gmail.com>
---
 MAINTAINERS | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 5114e6db7307d..cd5ffae6afd34 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23468,6 +23468,15 @@ S:	Maintained
 F:	Documentation/devicetree/bindings/sound/rockchip,rk3576-sai.yaml
 F:	sound/soc/rockchip/rockchip_sai.*
 
+ROCKCHIP XPCS DRIVER
+M:	Coia Prant <coiaprant at gmail.com>
+L:	netdev at vger.kernel.org
+L:	linux-rockchip at lists.infradead.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/net/pcs/rockchip-dwxpcs.yaml
+F:	drivers/net/pcs/pcs-xpcs-rk.c
+F:	include/linux/pcs/pcs-xpcs-rk.h
+
 ROCKER DRIVER
 M:	Jiri Pirko <jiri at resnulli.us>
 L:	netdev at vger.kernel.org
-- 
2.47.3



From heiko at sntech.de  Sat Aug  1 09:28:11 2026
From: heiko at sntech.de (Heiko =?UTF-8?B?U3TDvGJuZXI=?=)
Date: Sat, 01 Aug 2026 18:28:11 +0200
Subject: [PATCH v2 05/10] arm64: dts: rockchip: rk3568: add XPCS and fixed-clock
 nodes
In-Reply-To: <20260801142244.679437-7-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-7-coiaprant@gmail.com>
Message-ID: <2847036.GUH7pLUipS@diego>

Am Samstag, 1. August 2026, 16:22:29 Mitteleurop?ische Sommerzeit schrieb Coia Prant:
> The RK3568 SoC integrates a Synopsys DesignWare XPCS that provides
> the Physical Coding Sublayer for 1000BASE-X, SGMII, and QSGMII
> interfaces via its four MII ports.  Add the XPCS device node and
> its pcs-mii sub-nodes to the SoC device tree.
> 
> The XPCS device is accessed via the APB3 bus at 0xfda00000 and
> requires the CSR clock (PCLK_XPCS) for register access and the EEE
> clock (CLK_XPCS_EEE) for Energy Efficient Ethernet operation.  The
> PD_PIPE power domain must be enabled before any register access.
> 
> Also add two fixed-clock nodes (xpcs_gmac0_clk and xpcs_gmac1_clk)
> providing the 125 MHz reference clock for the GMACs when operating
> with XPCS.  These clocks are used as the assigned-clock-parents
> for the respective GMAC nodes in board-level device trees.
> 
> The XPCS node and its pcs-mii sub-nodes are disabled by default and
> must be enabled at the board level when 1000BASE-X/SGMII/QSGMII is
> in use.  The fixed-clock nodes are always present and do not have a
> status property, as they are static clock sources.
> 
> The XPCS node also requires a reference to the appropriate Naneng
> Combo PHY via the phys property at the board level.
> 
> Signed-off-by: Coia Prant <coiaprant at gmail.com>
> ---
>  arch/arm64/boot/dts/rockchip/rk3568.dtsi | 45 ++++++++++++++++++++++++
>  1 file changed, 45 insertions(+)
> 
> diff --git a/arch/arm64/boot/dts/rockchip/rk3568.dtsi b/arch/arm64/boot/dts/rockchip/rk3568.dtsi
> index 3bc653f027f1f..989e164c0eb39 100644
> --- a/arch/arm64/boot/dts/rockchip/rk3568.dtsi
> +++ b/arch/arm64/boot/dts/rockchip/rk3568.dtsi
> @@ -110,6 +110,51 @@ sata0: sata at fc000000 {
>  		status = "disabled";
>  	};
>  
> +	xpcs: pcs at fda00000 {
> +		compatible = "rockchip,rk3568-xpcs";
> +		#address-cells = <1>;
> +		#size-cells = <0>;
> +		reg = <0x0 0xfda00000 0x0 0x200000>;
> +		clocks = <&cru PCLK_XPCS>, <&cru CLK_XPCS_EEE>;
> +		clock-names = "csr", "eee";
> +		power-domains = <&power RK3568_PD_PIPE>;
> +		status = "disabled";
> +
> +		xpcs_mii0: pcs-mii at 0 {
> +			reg = <0>;
> +			status = "disabled";
> +		};
> +
> +		xpcs_mii1: pcs-mii at 1 {
> +			reg = <1>;
> +			status = "disabled";
> +		};
> +
> +		xpcs_mii2: pcs-mii at 2 {
> +			reg = <2>;
> +			status = "disabled";
> +		};
> +
> +		xpcs_mii3: pcs-mii at 3 {
> +			reg = <3>;
> +			status = "disabled";
> +		};
> +	};
> +
> +	xpcs_gmac0_clk: xpcs-gmac0-clock {
> +		compatible = "fixed-clock";
> +		clock-frequency = <125000000>;
> +		clock-output-names = "clk_gmac0_xpcs_mii";
> +		#clock-cells = <0>;
> +	};
> +
> +	xpcs_gmac1_clk: xpcs-gmac1-clock {
> +		compatible = "fixed-clock";
> +		clock-frequency = <125000000>;
> +		clock-output-names = "clk_gmac1_xpcs_mii";
> +		#clock-cells = <0>;
> +	};

I don't fully believe those clocks are "just there" inside the SoC.
Instead I _assume_ they are generated by some oscillator on the
individual boards?

Because as described there, they would be just magically be there.

So please aim for a real clock description (likely on a board-level)


Heiko





From coiaprant at gmail.com  Sat Aug  1 12:19:37 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sun, 2 Aug 2026 03:19:37 +0800
Subject: [PATCH v2 05/10] arm64: dts: rockchip: rk3568: add XPCS and
 fixed-clock nodes
In-Reply-To: <2847036.GUH7pLUipS@diego>
References: <20260801142244.679437-2-coiaprant@gmail.com> <20260801142244.679437-7-coiaprant@gmail.com>
 <2847036.GUH7pLUipS@diego>
Message-ID: <CALj3r0gqvFZPb2SKRMPwf9chCx076rwCnXEoOMFch_V-fOKV6w@mail.gmail.com>

Heiko St?bner <heiko at sntech.de> ?2026?8?2??? 00:28???
>
> Am Samstag, 1. August 2026, 16:22:29 Mitteleurop?ische Sommerzeit schrieb Coia Prant:
> > The RK3568 SoC integrates a Synopsys DesignWare XPCS that provides
> > the Physical Coding Sublayer for 1000BASE-X, SGMII, and QSGMII
> > interfaces via its four MII ports.  Add the XPCS device node and
> > its pcs-mii sub-nodes to the SoC device tree.
> >
> > The XPCS device is accessed via the APB3 bus at 0xfda00000 and
> > requires the CSR clock (PCLK_XPCS) for register access and the EEE
> > clock (CLK_XPCS_EEE) for Energy Efficient Ethernet operation.  The
> > PD_PIPE power domain must be enabled before any register access.
> >
> > Also add two fixed-clock nodes (xpcs_gmac0_clk and xpcs_gmac1_clk)
> > providing the 125 MHz reference clock for the GMACs when operating
> > with XPCS.  These clocks are used as the assigned-clock-parents
> > for the respective GMAC nodes in board-level device trees.
> >
> > The XPCS node and its pcs-mii sub-nodes are disabled by default and
> > must be enabled at the board level when 1000BASE-X/SGMII/QSGMII is
> > in use.  The fixed-clock nodes are always present and do not have a
> > status property, as they are static clock sources.
> >
> > The XPCS node also requires a reference to the appropriate Naneng
> > Combo PHY via the phys property at the board level.
> >
> > Signed-off-by: Coia Prant <coiaprant at gmail.com>
> > ---
> >  arch/arm64/boot/dts/rockchip/rk3568.dtsi | 45 ++++++++++++++++++++++++
> >  1 file changed, 45 insertions(+)
> >
> > diff --git a/arch/arm64/boot/dts/rockchip/rk3568.dtsi b/arch/arm64/boot/dts/rockchip/rk3568.dtsi
> > index 3bc653f027f1f..989e164c0eb39 100644
> > --- a/arch/arm64/boot/dts/rockchip/rk3568.dtsi
> > +++ b/arch/arm64/boot/dts/rockchip/rk3568.dtsi
> > @@ -110,6 +110,51 @@ sata0: sata at fc000000 {
> >               status = "disabled";
> >       };
> >
> > +     xpcs: pcs at fda00000 {
> > +             compatible = "rockchip,rk3568-xpcs";
> > +             #address-cells = <1>;
> > +             #size-cells = <0>;
> > +             reg = <0x0 0xfda00000 0x0 0x200000>;
> > +             clocks = <&cru PCLK_XPCS>, <&cru CLK_XPCS_EEE>;
> > +             clock-names = "csr", "eee";
> > +             power-domains = <&power RK3568_PD_PIPE>;
> > +             status = "disabled";
> > +
> > +             xpcs_mii0: pcs-mii at 0 {
> > +                     reg = <0>;
> > +                     status = "disabled";
> > +             };
> > +
> > +             xpcs_mii1: pcs-mii at 1 {
> > +                     reg = <1>;
> > +                     status = "disabled";
> > +             };
> > +
> > +             xpcs_mii2: pcs-mii at 2 {
> > +                     reg = <2>;
> > +                     status = "disabled";
> > +             };
> > +
> > +             xpcs_mii3: pcs-mii at 3 {
> > +                     reg = <3>;
> > +                     status = "disabled";
> > +             };
> > +     };
> > +
> > +     xpcs_gmac0_clk: xpcs-gmac0-clock {
> > +             compatible = "fixed-clock";
> > +             clock-frequency = <125000000>;
> > +             clock-output-names = "clk_gmac0_xpcs_mii";
> > +             #clock-cells = <0>;
> > +     };
> > +
> > +     xpcs_gmac1_clk: xpcs-gmac1-clock {
> > +             compatible = "fixed-clock";
> > +             clock-frequency = <125000000>;
> > +             clock-output-names = "clk_gmac1_xpcs_mii";
> > +             #clock-cells = <0>;
> > +     };
>
> I don't fully believe those clocks are "just there" inside the SoC.
> Instead I _assume_ they are generated by some oscillator on the
> individual boards?
>
> Because as described there, they would be just magically be there.
>
> So please aim for a real clock description (likely on a board-level)
>
>
> Heiko

Hi Heiko,

Thanks for the review.

During my tests, I found that the 125 MHz clocks (`xpcs_gmac0_clk` /
`xpcs_gmac1_clk`) are actually outputs of the Naneng Combo PHY
(SerDes) when it's initialized for SGMII/QSGMII mode, rather than
independent board-level oscillators.

Since these clocks are part of the SoC's internal design and
consistent across all RK3568 boards, I think keeping them in the SoC
dtsi makes sense to avoid unnecessary duplication. However, I also
understand your concern that they might appear to be "magically there"
without any explicit dependency.

To address this, I'm considering adding `status = "disabled"` to both
clock nodes in the dtsi, so boards that actually use SGMII/QSGMII can
explicitly enable them together with the XPCS node and the
corresponding Combo PHY reference. This would make the dependency and
activation requirements clearer.

What's your thought on this? Would you prefer adding `status =
"disabled"` to clarify the dependency, or do you think the nodes
should be moved to board-level dts entirely? I'm open to either
direction.

Thanks,
Coia


From andrew at lunn.ch  Sat Aug  1 13:58:39 2026
From: andrew at lunn.ch (Andrew Lunn)
Date: Sat, 1 Aug 2026 22:58:39 +0200
Subject: [PATCH v2 03/10] phy: rockchip: naneng-combphy: add SGMII MAC
 selection for RK3568
In-Reply-To: <20260801142244.679437-5-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-5-coiaprant@gmail.com>
Message-ID: <0cf1f734-69d4-471f-aa22-e1a2e7d0341b@lunn.ch>

On Sat, Aug 01, 2026 at 10:22:27PM +0800, Coia Prant wrote:
> On RK3568, the SGMII interface can be routed to either GMAC0 or
> GMAC1 via the GRF register pipe_sgmii_mac_sel.
> 
> Add support for this selection by introducing
> the "rockchip,sgmii-mac-sel" DT property.
> 
> The hardware reset value is GMAC1 (1). If the property is set to 0,
> the driver routes SGMII to GMAC0; if set to 1 (or omitted), it
> remains at GMAC1.
> 
> This is necessary for boards such as the Ariaboard Photonicat, which
> uses the SGMII interface connected to GMAC0.

If SGMII is not selected, what MII is used? RGMII?

Can this setting be implied from phy-mode?

	Andrew


From andrew at lunn.ch  Sat Aug  1 14:05:26 2026
From: andrew at lunn.ch (Andrew Lunn)
Date: Sat, 1 Aug 2026 23:05:26 +0200
Subject: [PATCH v2 05/10] arm64: dts: rockchip: rk3568: add XPCS and
 fixed-clock nodes
In-Reply-To: <CALj3r0gqvFZPb2SKRMPwf9chCx076rwCnXEoOMFch_V-fOKV6w@mail.gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-7-coiaprant@gmail.com>
 <2847036.GUH7pLUipS@diego>
 <CALj3r0gqvFZPb2SKRMPwf9chCx076rwCnXEoOMFch_V-fOKV6w@mail.gmail.com>
Message-ID: <0e9d16df-7332-446b-9321-ce3de3111ffa@lunn.ch>

> During my tests, I found that the 125 MHz clocks (`xpcs_gmac0_clk` /
> `xpcs_gmac1_clk`) are actually outputs of the Naneng Combo PHY
> (SerDes) when it's initialized for SGMII/QSGMII mode, rather than
> independent board-level oscillators.

Do you have documentation for these Combo PHYs? Can these clocks be
enabled/disabled? And how is the Combo PHY fed with a clock? Does it
have an external oscillator/crystal?

     Andrew


From andrew at lunn.ch  Sat Aug  1 14:10:39 2026
From: andrew at lunn.ch (Andrew Lunn)
Date: Sat, 1 Aug 2026 23:10:39 +0200
Subject: [PATCH v2 06/10] net: pcs: xpcs: add ANRESTART support for SGMII
 link recovery
In-Reply-To: <20260801142244.679437-8-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-8-coiaprant@gmail.com>
Message-ID: <cfddb747-11d0-476d-8402-e0886cca582e@lunn.ch>

> -	/* Clear AN complete status or interrupt */
> -	if (state->an_complete)
> +	if (state->an_complete) {
> +		/* Clear AN complete status or interrupt */
>  		xpcs_write(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_INTR_STS, 0);
>  
> +		/* Initiate the next round of AN */
> +		xpcs_modify(xpcs, MDIO_MMD_VEND2, MII_BMCR, BMCR_ANRESTART,
> +				    BMCR_ANRESTART);
> +	}
> +

This probably needs more wide spread testing. I would be afraid that
if you kick off an AN cycle, you loose the link until it completes?
You would of noticed that on your hardware, but other hardware
obviously behaves differently...

   Andrew


From andrew at lunn.ch  Sat Aug  1 18:29:35 2026
From: andrew at lunn.ch (Andrew Lunn)
Date: Sun, 2 Aug 2026 03:29:35 +0200
Subject: [PATCH v2 07/10] net: pcs: xpcs: add Rockchip RK3568 platform
 glue driver
In-Reply-To: <20260801142244.679437-9-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-9-coiaprant@gmail.com>
Message-ID: <23d8e994-8552-466f-98e6-c47d5783525b@lunn.ch>

On Sat, Aug 01, 2026 at 10:22:31PM +0800, Coia Prant wrote:
> The RK3568 SoC integrates a Synopsys DesignWare XPCS that is accessed
> via APB3 memory-mapped registers.
> This driver provides the glue logic to make the XPCS accessible to
> the generic pcs-xpcs core.
> 
> The XPCS block contains four MII ports (0..3), each of which can be
> routed to GMAC0 or GMAC1 via the pcs-handle property in the MAC node.
> The hardware maps these ports to different MMDs:
>   - port 0: MMD 7 (ROCKCHIP_MMD_MII)
>   - port 1: MMD 2 (ROCKCHIP_MMD_MII1)
>   - port 2: MMD 3 (ROCKCHIP_MMD_MII2)
>   - port 3: MMD 4 (ROCKCHIP_MMD_MII3)

Why is port 0 called ROCKCHIP_MMD_MII not ROCKCHIP_MMD_MII0 ?

> This driver creates a virtual MDIO bus that translates MDIO operations
> to APB3 register accesses, with proper address remapping for each port.
> The generic xpcs driver then creates a phylink_pcs instance on top of
> this bus, allowing the MAC to use the PCS via the standard phylink API.

I would put some of this into the driver itself, but reading the code
it is not so obvious.

> +static int xpcs_rk_read_c22(struct mii_bus *bus, int addr, int reg)
> +{
> +	struct dw_xpcs_rk *pxpcs = bus->priv;
> +	int dev;
> +
> +	if (!xpcs_rk_mdio_addr_validate(addr))
> +		return -ENODEV;
> +
> +	dev = xpcs_rk_mdio_read_remapping(addr, MDIO_MMD_VEND2, reg);

Does this mean C22 registers are mapped into the first 32 of C45
MDIO_MMD_VEND2?

> +static int xpcs_rk_read_c45(struct mii_bus *bus, int addr, int dev, int reg)
> +{
> +	struct dw_xpcs_rk *pxpcs = bus->priv;
> +
> +	if (!xpcs_rk_mdio_addr_validate(addr))
> +		return -ENODEV;
> +
> +	dev = xpcs_rk_mdio_read_remapping(addr, dev, reg);

Should it be returning an error for dev == MDIO_MMD_VEND2? Or at least
if reg < 32?

Andrew


From coiaprant at gmail.com  Sat Aug  1 20:11:40 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sun, 2 Aug 2026 11:11:40 +0800
Subject: [PATCH v2 06/10] net: pcs: xpcs: add ANRESTART support for SGMII
 link recovery
In-Reply-To: <cfddb747-11d0-476d-8402-e0886cca582e@lunn.ch>
References: <20260801142244.679437-2-coiaprant@gmail.com> <20260801142244.679437-8-coiaprant@gmail.com>
 <cfddb747-11d0-476d-8402-e0886cca582e@lunn.ch>
Message-ID: <CALj3r0i1TCTgvm8FrJAJqmZgoJp61NCxc_xmVYaDFd-gh4H=7w@mail.gmail.com>

Hi Andrew,

Thanks for the careful review.

> This probably needs more wide spread testing. I would be afraid that
> if you kick off an AN cycle, you loose the link until it completes?
> You would of noticed that on your hardware, but other hardware
> obviously behaves differently...

The trigger condition is: link is down AND an_complete is set. This means
auto-negotiation has already completed (or completed with failure), but
the link is still down. In this state, the hardware will not restart AN
on its own on RK3568, so the link stays dead forever. Kicking ANRESTART
here is the only way to recover.

For a working link (link up), state->an_complete may still be set, but
the function returns earlier at the "if (state->link)" check, so
ANRESTART is never triggered on an active link. This should not cause
unexpected link loss on any hardware.

Regarding testing:
- Jiawen Wu has tested this on Wangxun NICs and reported no regression
  (Tested-by tag included in the patch).
- On RK3568 (Photonicat board), this fixes the SGMII link recovery issue.

I agree that wider testing is beneficial, and I'm open to running more
tests on different platforms if needed. However, given the condition is
strictly limited to "link down + an_complete", the risk of disrupting an
already-working link is minimal.

Thanks,
Coia


From coiaprant at gmail.com  Sat Aug  1 20:20:27 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sun, 2 Aug 2026 11:20:27 +0800
Subject: [PATCH v2 07/10] net: pcs: xpcs: add Rockchip RK3568 platform
 glue driver
In-Reply-To: <23d8e994-8552-466f-98e6-c47d5783525b@lunn.ch>
References: <20260801142244.679437-2-coiaprant@gmail.com> <20260801142244.679437-9-coiaprant@gmail.com>
 <23d8e994-8552-466f-98e6-c47d5783525b@lunn.ch>
Message-ID: <CALj3r0i5yVcJY8V75y0Hm5xjtZSBpH0DXSAWKsAUqguChVcxYA@mail.gmail.com>

> On Sat, Aug 01, 2026 at 10:22:31PM +0800, Coia Prant wrote:
> > The XPCS block contains four MII ports (0..3), each of which can be
> > routed to GMAC0 or GMAC1 via the pcs-handle property in the MAC node.
> > The hardware maps these ports to different MMDs:
> >   - port 0: MMD 7 (ROCKCHIP_MMD_MII)
> >   - port 1: MMD 2 (ROCKCHIP_MMD_MII1)
> >   - port 2: MMD 3 (ROCKCHIP_MMD_MII2)
> >   - port 3: MMD 4 (ROCKCHIP_MMD_MII3)
>
> Why is port 0 called ROCKCHIP_MMD_MII not ROCKCHIP_MMD_MII0 ?

Hi Andrew,

The naming follows the Rockchip TRM. The hardware documentation refers to
the MMD for the first port as simply "MII" without a numeric suffix, while
the other ports are named "MII1", "MII2", "MII3". I kept the naming
consistent with the TRM to make it easier to cross-reference.

As I understand it, this is probably because the MII controls not only
Port 0, but
also the entire PCS (these registers are read-only in Ports 1-3 and reflected
back to the MII).

If you prefer, I can rename it to ROCKCHIP_MMD_MII0 for consistency. Let me
know and I'll update it in v3.

>
> > +static int xpcs_rk_read_c22(struct mii_bus *bus, int addr, int reg)
> > +{
> > +     struct dw_xpcs_rk *pxpcs = bus->priv;
> > +     int dev;
> > +
> > +     if (!xpcs_rk_mdio_addr_validate(addr))
> > +             return -ENODEV;
> > +
> > +     dev = xpcs_rk_mdio_read_remapping(addr, MDIO_MMD_VEND2, reg);
>
> Does this mean C22 registers are mapped into the first 32 of C45
> MDIO_MMD_VEND2?

Yes, exactly. The C22 register space (reg 0-31) is mapped into the first
32 registers of the VEND2 MMD (MMD 7). This is how the hardware is designed
and matches the standard C22 to C45 address mapping.

>
> > +static int xpcs_rk_read_c45(struct mii_bus *bus, int addr, int dev, int reg)
> > +{
> > +     struct dw_xpcs_rk *pxpcs = bus->priv;
> > +
> > +     if (!xpcs_rk_mdio_addr_validate(addr))
> > +             return -ENODEV;
> > +
> > +     dev = xpcs_rk_mdio_read_remapping(addr, dev, reg);
>
> Should it be returning an error for dev == MDIO_MMD_VEND2? Or at least
> if reg < 32?

No, we cannot simply return an error here. The generic xpcs driver uses
C45 reads to access the VEND2 MMD for various operations, including reading
the device ID (xpcs_read_id). If we returned an error for VEND2 or blocked
reg < 32, the xpcs core driver would fail to identify the device and the
PCS would not work.

The VEND2 MMD is fully accessible on this hardware and contains the
standard C22 registers (0-31) as well as vendor-specific registers.
The remapping function maps the C45 address space to the correct
hardware MMD, and then the read/write functions handle the actual access.
This design matches how other xpcs platform drivers (e.g., pcs-xpcs-plat.c)
handle similar hardware.

For invalid MMIDs (e.g., MDIO_MMD_PCS, MDIO_MMD_VEND1, etc.), the
xpcs_rk_mdio_read_remapping() function returns -ENXIO, which is then
returned as 0xffff (meaning "no link" / "unavailable"). This is the
standard MDIO behavior for unimplemented registers.

Thanks,
Coia


From coiaprant at gmail.com  Sat Aug  1 20:28:12 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sun, 2 Aug 2026 11:28:12 +0800
Subject: [PATCH v2 05/10] arm64: dts: rockchip: rk3568: add XPCS and
 fixed-clock nodes
In-Reply-To: <0e9d16df-7332-446b-9321-ce3de3111ffa@lunn.ch>
References: <20260801142244.679437-2-coiaprant@gmail.com> <20260801142244.679437-7-coiaprant@gmail.com>
 <2847036.GUH7pLUipS@diego> <CALj3r0gqvFZPb2SKRMPwf9chCx076rwCnXEoOMFch_V-fOKV6w@mail.gmail.com>
 <0e9d16df-7332-446b-9321-ce3de3111ffa@lunn.ch>
Message-ID: <CALj3r0guMmNRN4EBbKz7stPNMSqkLBvms643YXBEDMzqHHM=FA@mail.gmail.com>

Hi Andrew,

Thanks for the detailed questions. I've looked into the clock tree
more carefully.

> Do you have documentation for these Combo PHYs?

Yes, the information is available in the Rockchip RK3568 TRM
(Technical Reference Manual).
The relevant clock tree details are also mentioned in upstream
discussions and patches.

> Can these clocks be enabled/disabled?

The 100 MHz input clock to the Combo PHY is a prerequisite for its operation.
It is generated by the SoC's internal PPLL and fed to the PHY through
a fixed divider.
It is not a gate that can be individually enabled or disabled by
software; it is part of the
PHY's fundamental operating conditions.

> And how is the Combo PHY fed with a clock? Does it have an external oscillator/crystal?

The Combo PHY is fed by an internal 100 MHz clock derived from the PPLL.
Specifically, the PPLL runs at 200 MHz, and this is divided by 2 to provide the
 100 MHz clock input to the Combo PHY.
So, it does not use an external oscillator or crystal for its main
reference clock;
it's generated internally by the SoC.

To summarize:
- The 125 MHz clocks (xpcs_gmac0_clk / xpcs_gmac1_clk) are outputs
from the Combo PHY.
- The Combo PHY itself is clocked by an internal 100 MHz clock from the PPLL.
- This is all internal to the SoC and does not involve a board-level oscillator.

I hope this clarifies the clock architecture.

Thanks,
Coia


From coiaprant at gmail.com  Sat Aug  1 19:51:37 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Sun, 02 Aug 2026 10:51:37 +0800
Subject: =?US-ASCII?Q?Re=3A_=5BPATCH_v2_03/10=5D_phy=3A_rockchip=3A_naneng-?=
 =?US-ASCII?Q?combphy=3A_add_SGMII_MAC_selection_for_RK3568?=
In-Reply-To: <0cf1f734-69d4-471f-aa22-e1a2e7d0341b@lunn.ch>
References: <20260801142244.679437-2-coiaprant@gmail.com> <20260801142244.679437-5-coiaprant@gmail.com> <0cf1f734-69d4-471f-aa22-e1a2e7d0341b@lunn.ch>
Message-ID: <7F609DE6-F3C6-487E-9113-449F5BE05D94@gmail.com>

On August 2, 2026 4:58:39 AM GMT+08:00, Andrew Lunn <andrew at lunn.ch> wrote:
>On Sat, Aug 01, 2026 at 10:22:27PM +0800, Coia Prant wrote:
>> On RK3568, the SGMII interface can be routed to either GMAC0 or
>> GMAC1 via the GRF register pipe_sgmii_mac_sel.
>> 
>> Add support for this selection by introducing
>> the "rockchip,sgmii-mac-sel" DT property.
>> 
>> The hardware reset value is GMAC1 (1). If the property is set to 0,
>> the driver routes SGMII to GMAC0; if set to 1 (or omitted), it
>> remains at GMAC1.
>> 
>> This is necessary for boards such as the Ariaboard Photonicat, which
>> uses the SGMII interface connected to GMAC0.
>
>If SGMII is not selected, what MII is used? RGMII?
>
>Can this setting be implied from phy-mode?
>
>	Andrew

Hi Andrew,

If phy-mode is not "sgmii" (e.g., "rgmii" or "rmii"), the GMAC works in
the corresponding mode and this register is not touched. The reset
default (GMAC1) remains, but it has no effect in non-SGMII modes because
the register only controls SGMII signal routing.

Regarding inference from phy-mode: phy-mode = "sgmii" only tells the MAC
driver that the interface is SGMII. It does not specify whether the
SGMII signals are physically routed to GMAC0 or GMAC1 on the board.
That routing is a board-level decision and cannot be derived from any
other DT property.

Also, the PHY (Naneng Combo PHY) is initialized by the XPCS glue driver
(pcs-xpcs-rk), not by the GMAC driver. The PHY driver has no way to know
which GMAC is using it. Making the PHY aware of the MAC ID would require
significant changes to the PHY layer, which is out of scope
for this series.

Therefore, an explicit DT property is the cleanest and most maintainable
way to describe this hardware routing.

Thanks,
Coia


From sashiko-bot at kernel.org  Sun Aug  2 07:25:32 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 14:25:32 +0000
Subject: [PATCH v2 07/10] net: pcs: xpcs: add Rockchip RK3568 platform
 glue driver
In-Reply-To: <20260801142244.679437-9-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-9-coiaprant@gmail.com>
Message-ID: <20260802142533.C14121F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Probe fails unconditionally on systems without a PM domain or with CONFIG_PM_GENERIC_DOMAINS disabled.
- [High] Premature `pm_runtime_force_suspend()` in `remove()` breaks PHY teardown, causes SErrors, and permanently disables PM runtime on rebind.
- [High] Concurrent unbinding of the XPCS driver during `xpcs_rk_create` leads to a Use-After-Free of `pxpcs` and its MDIO bus.
- [High] Unchecked return value of `device_link_add()` allows Use-After-Free on supplier unbind.
- [Low] Unconditional assignment of `dev_pm_ops` and missing `pm_ptr()` wrapper causes unused function compiler warnings.
--

--- Patch [7]: [PATCH v2 07/10] net: pcs: xpcs: add Rockchip RK3568 platform glue driver ---
commit 749227defaf729e0f171dc87722abef8ebe69759
Author: Coia Prant <coiaprant at gmail.com>
net: pcs: xpcs: add Rockchip RK3568 platform glue driver

This commit introduces a platform glue driver for the Synopsys DesignWare XPCS
controller found on Rockchip RK3568 SoCs. It translates MDIO operations to
APB3 register accesses via a virtual MDIO bus. The generic xpcs driver then
creates a phylink_pcs instance on top of this bus.

Link: https://dl.radxa.com/rock3/docs/hw/datasheet/Rockchip%20RK3568%20TRM%20Part2%20V1.1-20210301.pdf (Page 2078)

diff --git a/drivers/net/pcs/pcs-xpcs-rk.c b/drivers/net/pcs/pcs-xpcs-rk.c
index 0000000000000..36c1557b83367
--- /dev/null
+++ b/drivers/net/pcs/pcs-xpcs-rk.c
@@ -0,0 +1,538 @@
[ ... ]
> +static int xpcs_rk_serdes_phy_poweron(struct dw_xpcs_rk *pxpcs)
> +{
> +	struct device *dev = &pxpcs->pdev->dev;
> +	int ret;
> +
> +	ret = dev_pm_genpd_rpm_always_on(dev, true);
> +	if (ret) {
> +		dev_err(dev, "Failed to power on power-domains\n");
> +		return ret;
> +	}

[Severity: High]
Does this code unintentionally fail the probe on systems without a PM domain
or when PM generic domains are disabled?

When CONFIG_PM_GENERIC_DOMAINS is disabled, dev_pm_genpd_rpm_always_on()
returns -EOPNOTSUPP. If the device has no power domain (which appears to be
optional), it returns -ENODEV.

Because the probe is aborted on any non-zero return value, the driver will
fail to load on these hardware configurations or in COMPILE_TEST builds.
Should -ENODEV and -EOPNOTSUPP be explicitly ignored here?

[ ... ]
> +static void xpcs_rk_remove(struct platform_device *pdev)
> +{
> +	pm_runtime_force_suspend(&pdev->dev);
> +}

[Severity: High]
Can this break PHY teardown and disable PM runtime permanently upon rebind?

Because the MDIO bus is registered using devm_mdiobus_register(), devres
will unregister it after xpcs_rk_remove() returns. However,
pm_runtime_force_suspend() is called here first, cutting the clock and
incrementing the PM runtime disable depth.

When devres subsequently unregisters the MDIO bus, attached PHY devices
are unbound, which triggers MDIO accesses for teardown. These accesses
will call pm_runtime_resume_and_get(), which fails with -EACCES because
PM runtime is disabled. This failure can cause a synchronous external abort
if the clock is cut during access.

Additionally, the double-disable (here and in devm_pm_runtime_enable cleanup)
leaves the disable depth greater than zero on rebind.

[ ... ]
> +struct dw_xpcs *xpcs_rk_create(struct device *dev, struct device_node *np)
> +{
[ ... ]
> +	pdev = of_find_device_by_node(pcs_np);
> +	of_node_put(pcs_np);
> +	if (!pdev)
> +		return ERR_PTR(-EPROBE_DEFER);
> +
> +	pxpcs = platform_get_drvdata(pdev);
> +	if (!pxpcs || !pxpcs->bus) {
> +		put_device(&pdev->dev);
> +		return ERR_PTR(-EPROBE_DEFER);
> +	}
> +
> +	xpcs = xpcs_create_mdiodev(pxpcs->bus, (int)port);

[Severity: High]
Is it possible for the XPCS driver to be unbound concurrently while
platform_get_drvdata() is reading the data?

The device structure itself is pinned by of_find_device_by_node(), but
without holding the device lock, a concurrent sysfs unbind could trigger
devres cleanup. This would free pxpcs and unregister the MDIO bus right
before xpcs_create_mdiodev() accesses pxpcs->bus, leading to a
use-after-free.

Should the device lock be held to verify the driver is still bound before
proceeding?

> +	if (IS_ERR(xpcs)) {
> +		put_device(&pdev->dev);
> +		return xpcs;
> +	}
> +
> +	device_link_add(dev, &pdev->dev, DL_FLAG_AUTOREMOVE_CONSUMER);

[Severity: High]
Does this ignore the return value of device_link_add() if the link creation
fails?

If device_link_add() fails (for example, due to a memory allocation failure),
no link is established. Later, if the XPCS supplier unbinds, the driver
core will not automatically unbind the MAC consumer. The XPCS devres
cleanup will free the MDIO bus while the MAC driver is still bound and
potentially using it, which can cause a use-after-free.

Should the return value of device_link_add() be checked?

[ ... ]
> +static const struct dev_pm_ops xpcs_rk_pm_ops = {
> +	SET_RUNTIME_PM_OPS(xpcs_rk_pm_runtime_suspend,
> +			   xpcs_rk_pm_runtime_resume,
> +			   NULL)
> +};
> +
> +static struct platform_driver xpcs_rk_driver = {
> +	.probe = xpcs_rk_probe,
> +	.remove = xpcs_rk_remove,
> +	.driver = {
> +		.name = "rk_xpcs-dwxpcs",
> +		.pm = &xpcs_rk_pm_ops,

[Severity: Low]
Will this generate unused function warnings when CONFIG_PM is disabled?

The xpcs_rk_pm_runtime_suspend() and xpcs_rk_pm_runtime_resume() callbacks
are defined as static. When CONFIG_PM is not set, SET_RUNTIME_PM_OPS expands
to nothing, leaving the static functions unreferenced.

Since xpcs_rk_pm_ops is unconditionally assigned to .pm without the
pm_ptr() wrapper, the struct is always referenced, but the callbacks inside
might not be. Should RUNTIME_PM_OPS() and pm_ptr() be used instead?

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260801142244.679437-2-coiaprant at gmail.com?part=7


From sashiko-bot at kernel.org  Sun Aug  2 07:25:36 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 14:25:36 +0000
Subject: [PATCH v2 06/10] net: pcs: xpcs: add ANRESTART support for
 SGMII link recovery
In-Reply-To: <20260801142244.679437-8-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-8-coiaprant@gmail.com>
Message-ID: <20260802142537.173511F00A3A@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Stale documentation: the patch introduces BMCR_ANRESTART for MAC-side SGMII but fails to update the block comment that explicitly states this is not needed.
--

--- Patch [6]: [PATCH v2 06/10] net: pcs: xpcs: add ANRESTART support for SGMII link recovery ---
commit 2bc2528d92a030c16f9b0b3ce9ca602d98573c26
Author: Coia Prant <coiaprant at gmail.com>

net: pcs: xpcs: add ANRESTART support for SGMII link recovery

This commit adds ANRESTART support for SGMII link recovery on hardware
using the DesignWare XPCS IP. It sets BMCR_ANRESTART when starting
auto-negotiation and triggers it again if the link goes down after
completion to ensure the negotiation restarts properly.

> diff --git a/drivers/net/pcs/pcs-xpcs.c b/drivers/net/pcs/pcs-xpcs.c
> --- a/drivers/net/pcs/pcs-xpcs.c
> +++ b/drivers/net/pcs/pcs-xpcs.c
> @@ -816,9 +816,14 @@ static int xpcs_config_aneg_c37_sgmii(struct dw_xpcs *xpcs,

[ ... ]

>  	if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
>  		ret = xpcs_write(xpcs, MDIO_MMD_VEND2, MII_BMCR,
> -				 mdio_ctrl | BMCR_ANENABLE);
> +				 mdio_ctrl | BMCR_ANENABLE | BMCR_ANRESTART);

[Severity: Low]
This isn't a bug, but does this change make the large block comment earlier
in xpcs_config_aneg_c37_sgmii() contradictory?

The existing comment explicitly states:
"There is also no need to trigger AN restart for MAC-side SGMII."

Since this patch adds the BMCR_ANRESTART flag to the MAC-side SGMII path,
should the comment be updated to reflect the new behavior?

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260801142244.679437-2-coiaprant at gmail.com?part=6


From sashiko-bot at kernel.org  Sun Aug  2 07:25:38 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 14:25:38 +0000
Subject: [PATCH v2 04/10] dt-bindings: net: pcs: add
 rockchip,rk3568-xpcs binding
In-Reply-To: <20260801142244.679437-6-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-6-coiaprant@gmail.com>
Message-ID: <20260802142539.3C9F31F00A3D@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Missing XPCS initialization for Rockchip DWMAC (Hardware Initialization Failure).
- [Low] Invalid use of 'minimum' and 'maximum' constraints on the 'reg' property in the subnode.
--

--- Patch [4]: [PATCH v2 04/10] dt-bindings: net: pcs: add rockchip,rk3568-xpcs binding ---
commit 3427e61df71bfcc8fb661a8f120af0f4c141f21b
Author: Coia Prant <coiaprant at gmail.com>

dt-bindings: net: pcs: add rockchip,rk3568-xpcs binding

Add device tree binding documentation for the Synopsys DesignWare
XPCS integrated on the Rockchip RK3568 SoC.

> diff --git a/Documentation/devicetree/bindings/net/pcs/rockchip-dwxpcs.yaml b/Documentation/devicetree/bindings/net/pcs/rockchip-dwxpcs.yaml
> new file mode 100644
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/net/pcs/rockchip-dwxpcs.yaml
[ ... ]
> +patternProperties:
> +  "^pcs-mii@[0-3]$":
> +    type: object
> +    description: |
> +      One of the four MII ports of the XPCS.
> +      The port number is specified by the reg property (0..3).
> +      The port is linked to an Ethernet MAC controller via the
> +      pcs-handle property in the MAC's device tree node.

[Severity: High]
How is the pcs-handle property parsed by the Rockchip driver in practice?

With the generic pcs-handle parsing logic removed from stmmac_pcs_setup() in
favor of the platform-specific pcs_init callback:

drivers/net/ethernet/stmicro/stmmac/stmmac_mdio.c:stmmac_pcs_setup() {
	if (!priv->plat->pcs_init)
		return 0;

	ret = priv->plat->pcs_init(priv);
}

Does the Rockchip DWMAC driver provide this callback? If not, it seems
stmmac_pcs_setup() will silently return 0 without parsing the pcs-handle,
leaving the XPCS uninitialized and breaking the SGMII/QSGMII support described
here.

> +
> +    properties:
> +      reg:
> +        minimum: 0
> +        maximum: 3

[Severity: Low]
Does applying scalar numeric constraints like minimum and maximum directly to
the reg property cause a meta-schema validation error during
make dt_binding_check?

In the dtschema core, reg evaluates to an array of integers rather than a
scalar, so strict JSON schema validation typically fails with an error that
minimum is not allowed for an array. 

Could this be constrained as an array item instead, or omitted entirely in
favor of maxItems: 1 since the node name regex (^pcs-mii@[0-3]$) already
enforces the port number?

> +        description: |
> +          MII port number of PCS.

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260801142244.679437-2-coiaprant at gmail.com?part=4


From sashiko-bot at kernel.org  Sun Aug  2 07:25:40 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 14:25:40 +0000
Subject: [PATCH v2 02/10] dt-bindings: phy: rockchip: naneng-combphy:
 add rockchip,sgmii-mac-sel property
In-Reply-To: <20260801142244.679437-4-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-4-coiaprant@gmail.com>
Message-ID: <20260802142541.395631F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The `rockchip,sgmii-mac-sel` property is defined in the device tree bindings but is not implemented in any kernel driver.
--

--- Patch [2]: [PATCH v2 02/10] dt-bindings: phy: rockchip: naneng-combphy: add rockchip,sgmii-mac-sel property ---
commit d067678ae28ccb1e1d1f20a8cb8013f22a387abc
Author: Coia Prant <coiaprant at gmail.com>

dt-bindings: phy: rockchip: naneng-combphy: add rockchip,sgmii-mac-sel property

This commit adds the optional rockchip,sgmii-mac-sel property to the Rockchip
Naneng combphy device tree binding. The property is intended to allow routing
the SGMII interface to either GMAC0 or GMAC1 via GRF registers.

> diff --git a/Documentation/devicetree/bindings/phy/phy-rockchip-naneng-combphy.yaml b/Documentation/devicetree/bindings/phy/phy-rockchip-naneng-combphy.yaml
> --- a/Documentation/devicetree/bindings/phy/phy-rockchip-naneng-combphy.yaml
> +++ b/Documentation/devicetree/bindings/phy/phy-rockchip-naneng-combphy.yaml
[ ... ]
> @@ -80,6 +80,15 @@ properties:
>      description:
>        Some additional pipe settings are accessed through GRF regs.
>  
> +  rockchip,sgmii-mac-sel:
> +    $ref: /schemas/types.yaml#/definitions/uint32
> +    enum: [0, 1]

[Severity: Medium]
Is the driver implementation for this property missing from the series?

The device tree binding defines rockchip,sgmii-mac-sel, but it appears
no kernel driver currently parses this property or writes the corresponding
configuration to the GRF registers.

Without driver support, boards relying on this property to configure the MAC
will fail to route SGMII to GMAC0, leaving it at the default GMAC1.

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260801142244.679437-2-coiaprant at gmail.com?part=2


From sashiko-bot at kernel.org  Sun Aug  2 07:25:42 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 14:25:42 +0000
Subject: [PATCH v2 08/10] net: stmmac: dwmac-rk: add SGMII support for
 RK3568
In-Reply-To: <20260801142244.679437-10-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-10-coiaprant@gmail.com>
Message-ID: <20260802142542.A26DA1F00A3D@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] rk3568_set_to_sgmii fails to clear interface selection bits 4:6, leading to potential hardware misconfiguration.
- [Low] DWMAC_ROCKCHIP selects PCS_XPCS_ROCKCHIP without inheriting its PM dependency.
--

--- Patch [8]: [PATCH v2 08/10] net: stmmac: dwmac-rk: add SGMII support for RK3568 ---
commit 53e13be00ad70a538e5aaf2d42200c1ae27d3eef
Author: Coia Prant <coiaprant at gmail.com>

net: stmmac: dwmac-rk: add SGMII support for RK3568
    
The RK3568 SoC integrates a Synopsys DesignWare XPCS that can be
connected to GMAC0 or GMAC1 in SGMII mode. Add the necessary glue
logic to support this configuration.

Link: https://dl.radxa.com/rock3/docs/hw/datasheet/Rockchip%20RK3568%20TRM%20Part1%20V1.1-20210301.pdf (Page 386)

> diff --git a/drivers/net/ethernet/stmicro/stmmac/Kconfig b/drivers/net/ethernet/stmicro/stmmac/Kconfig
> index e3dd5adda5aca..5088acc06982e 100644
> --- a/drivers/net/ethernet/stmicro/stmmac/Kconfig
> +++ b/drivers/net/ethernet/stmicro/stmmac/Kconfig
> @@ -170,6 +170,7 @@ config DWMAC_ROCKCHIP
>  	default ARCH_ROCKCHIP
>  	depends on OF && (ARCH_ROCKCHIP || COMPILE_TEST)
>  	select MFD_SYSCON
> +	select PCS_XPCS_ROCKCHIP

[Severity: Low]
Could selecting PCS_XPCS_ROCKCHIP here cause a Kconfig unmet dependency
warning or build failure if PM is disabled? 

PCS_XPCS_ROCKCHIP depends on PM || COMPILE_TEST, but DWMAC_ROCKCHIP lacks
this dependency.

>  	help
>  	  Support for Ethernet controller on Rockchip RK3288 SoC.
>  

> diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c
> index 8d7042e689261..e47ca1bec5b8b 100644
> --- a/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c
> +++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c
[ ... ]
> @@ -851,18 +857,32 @@ static void rk3568_set_to_rgmii(struct rk_priv_data *bsp_priv,
>  		     RK3568_GMAC_CLK_TX_DL_CFG(tx_delay));
>  
>  	regmap_write(bsp_priv->grf, con1,
> +		     RK3568_GMAC_MODE_RMII_RGMII |
>  		     RK3568_GMAC_RXCLK_DLY_ENABLE |
>  		     RK3568_GMAC_TXCLK_DLY_ENABLE);
>  }
>  
> +static void rk3568_set_to_sgmii(struct rk_priv_data *bsp_priv)
> +{
> +	u32 con1;
> +
> +	con1 = (bsp_priv->id == 1) ? RK3568_GRF_GMAC1_CON1 :
> +				     RK3568_GRF_GMAC0_CON1;
> +
> +	regmap_write(bsp_priv->grf, con1, RK3568_GMAC_MODE_SGMII_QSGMII);
> +}

[Severity: High]
Does this actually clear the interface selection bits 4:6 as mentioned in the
commit message? 

It looks like this only writes RK3568_GMAC_MODE_SGMII_QSGMII (which sets
bit 7) but doesn't supply a write mask or data to clear bits 4:6. If the
bootloader left bits 4:6 set to a non-zero value, could this lead to an
incorrect hardware configuration?

[ ... ]
> @@ -1379,16 +1441,19 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv)
>  	if (ret)
>  		return ret;
>  
> +	ret = gmac_clk_enable(bsp_priv, true);
> +	if (ret)
> +		return ret;
> +
> +	if (bsp_priv->phy_iface == PHY_INTERFACE_MODE_SGMII)
> +		goto set_mode;
> +

[Severity: High]
Because of the goto here, the standard clearing of interface selection bits
using bsp_priv->gmac_phy_intf_sel_mask that happens below is bypassed for
SGMII mode. 

Should rk3568_set_to_sgmii() handle clearing those bits instead?

>  	ret = rk_get_phy_intf_sel(bsp_priv->phy_iface);
>  	if (ret < 0)
> -		return ret;
> +		goto clk_disable;

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260801142244.679437-2-coiaprant at gmail.com?part=8


From sashiko-bot at kernel.org  Sun Aug  2 07:25:43 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 14:25:43 +0000
Subject: [PATCH v2 09/10] arm64: dts: rockchip: rk3568-photonicat:
 enable SGMII LAN port
In-Reply-To: <20260801142244.679437-11-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-11-coiaprant@gmail.com>
Message-ID: <20260802142544.1B61F1F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The YT8521SC PHY node specifies 10G Energy Efficient Ethernet (EEE) errata properties (`eee-broken-10gt`, `eee-broken-10gkx4`, `eee-broken-10gkr`) despite being a Gigabit PHY.
--

--- Patch [9]: [PATCH v2 09/10] arm64: dts: rockchip: rk3568-photonicat: enable SGMII LAN port ---
commit 08ee4bf87f62aaa593b62ffb0a37f8f8618a6a2a
Author: Coia Prant <coiaprant at gmail.com>

arm64: dts: rockchip: rk3568-photonicat: enable SGMII LAN port

This commit enables the SGMII LAN port on the Ariaboard Photonicat,
adding the necessary XPCS, GMAC0, and MDIO0 nodes to make the Motorcomm
YT8521SC Gigabit Ethernet PHY functional.

> diff --git a/arch/arm64/boot/dts/rockchip/rk3568-photonicat.dts b/arch/arm64/boot/dts/rockchip/rk3568-photonicat.dts
> --- a/arch/arm64/boot/dts/rockchip/rk3568-photonicat.dts
> +++ b/arch/arm64/boot/dts/rockchip/rk3568-photonicat.dts
[ ... ]
> @@ -341,6 +352,39 @@ &i2s0_8ch {
>  	status = "okay";
>  };
>  
> +&mdio0 {
> +	sgmii_phy: ethernet-phy at 3 {
> +		compatible = "ethernet-phy-id0000.011a";
> +		reg = <0x3>;
> +		max-speed = <1000>;
> +		eee-broken-10gt;
> +		eee-broken-10gkx4;
> +		eee-broken-10gkr;

[Severity: Low]
Are these 10G Energy Efficient Ethernet (EEE) errata properties intended for
this Gigabit PHY? Since max-speed is limited to 1000, these 10G-specific
properties appear to be inapplicable to this device.

> +		reset-assert-us = <20000>;
> +		reset-deassert-us = <100000>;

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260801142244.679437-2-coiaprant at gmail.com?part=9


From andrew at lunn.ch  Sun Aug  2 07:30:52 2026
From: andrew at lunn.ch (Andrew Lunn)
Date: Sun, 2 Aug 2026 16:30:52 +0200
Subject: [PATCH v2 06/10] net: pcs: xpcs: add ANRESTART support for SGMII
 link recovery
In-Reply-To: <CALj3r0i1TCTgvm8FrJAJqmZgoJp61NCxc_xmVYaDFd-gh4H=7w@mail.gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-8-coiaprant@gmail.com>
 <cfddb747-11d0-476d-8402-e0886cca582e@lunn.ch>
 <CALj3r0i1TCTgvm8FrJAJqmZgoJp61NCxc_xmVYaDFd-gh4H=7w@mail.gmail.com>
Message-ID: <2886b24e-1c3d-48f7-8b22-9620fa0404f7@lunn.ch>

On Sun, Aug 02, 2026 at 11:11:40AM +0800, Coia Prant wrote:
> Hi Andrew,
> 
> Thanks for the careful review.
> 
> > This probably needs more wide spread testing. I would be afraid that
> > if you kick off an AN cycle, you loose the link until it completes?
> > You would of noticed that on your hardware, but other hardware
> > obviously behaves differently...
> 
> The trigger condition is: link is down AND an_complete is set.

Ah, i missed the "link is down" check. That makes it safer.

I still find it interesting that this devices is different. Has the
silicon vendor done the integration of the licensed IP wrong?

> Regarding testing:
> - Jiawen Wu has tested this on Wangxun NICs and reported no regression
>   (Tested-by tag included in the patch).

Probably Maxime will test on the platform he has, but i don't know if
any use the XPCS.

	Andrew


From andrew at lunn.ch  Sun Aug  2 07:39:46 2026
From: andrew at lunn.ch (Andrew Lunn)
Date: Sun, 2 Aug 2026 16:39:46 +0200
Subject: [PATCH v2 07/10] net: pcs: xpcs: add Rockchip RK3568 platform
 glue driver
In-Reply-To: <CALj3r0i5yVcJY8V75y0Hm5xjtZSBpH0DXSAWKsAUqguChVcxYA@mail.gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-9-coiaprant@gmail.com>
 <23d8e994-8552-466f-98e6-c47d5783525b@lunn.ch>
 <CALj3r0i5yVcJY8V75y0Hm5xjtZSBpH0DXSAWKsAUqguChVcxYA@mail.gmail.com>
Message-ID: <302c67b0-6729-4930-a409-a2408dde33fc@lunn.ch>

On Sun, Aug 02, 2026 at 11:20:27AM +0800, Coia Prant wrote:
> > On Sat, Aug 01, 2026 at 10:22:31PM +0800, Coia Prant wrote:
> > > The XPCS block contains four MII ports (0..3), each of which can be
> > > routed to GMAC0 or GMAC1 via the pcs-handle property in the MAC node.
> > > The hardware maps these ports to different MMDs:
> > >   - port 0: MMD 7 (ROCKCHIP_MMD_MII)
> > >   - port 1: MMD 2 (ROCKCHIP_MMD_MII1)
> > >   - port 2: MMD 3 (ROCKCHIP_MMD_MII2)
> > >   - port 3: MMD 4 (ROCKCHIP_MMD_MII3)
> >
> > Why is port 0 called ROCKCHIP_MMD_MII not ROCKCHIP_MMD_MII0 ?
> 
> Hi Andrew,
> 
> The naming follows the Rockchip TRM. The hardware documentation refers to
> the MMD for the first port as simply "MII" without a numeric suffix, while
> the other ports are named "MII1", "MII2", "MII3". I kept the naming
> consistent with the TRM to make it easier to cross-reference.
> 
> As I understand it, this is probably because the MII controls not only
> Port 0, but
> also the entire PCS (these registers are read-only in Ports 1-3 and reflected
> back to the MII).
> 
> If you prefer, I can rename it to ROCKCHIP_MMD_MII0 for consistency. Let me
> know and I'll update it in v3.

If the TRM gives it this name, then O.K. It just makes the code look
odd, unbalanced.

> > > +static int xpcs_rk_read_c22(struct mii_bus *bus, int addr, int reg)
> > > +{
> > > +     struct dw_xpcs_rk *pxpcs = bus->priv;
> > > +     int dev;
> > > +
> > > +     if (!xpcs_rk_mdio_addr_validate(addr))
> > > +             return -ENODEV;
> > > +
> > > +     dev = xpcs_rk_mdio_read_remapping(addr, MDIO_MMD_VEND2, reg);
> >
> > Does this mean C22 registers are mapped into the first 32 of C45
> > MDIO_MMD_VEND2?
> 
> Yes, exactly. The C22 register space (reg 0-31) is mapped into the first
> 32 registers of the VEND2 MMD (MMD 7). This is how the hardware is designed
> and matches the standard C22 to C45 address mapping.

Does the xpcs code actually perform any C22 access? A quick look
suggests it is C45 only. xpcs_read() calls mdiodev_c45_read(). If C22
is not needed, i would not provide these functions.

> 
> >
> > > +static int xpcs_rk_read_c45(struct mii_bus *bus, int addr, int dev, int reg)
> > > +{
> > > +     struct dw_xpcs_rk *pxpcs = bus->priv;
> > > +
> > > +     if (!xpcs_rk_mdio_addr_validate(addr))
> > > +             return -ENODEV;
> > > +
> > > +     dev = xpcs_rk_mdio_read_remapping(addr, dev, reg);
> >
> > Should it be returning an error for dev == MDIO_MMD_VEND2? Or at least
> > if reg < 32?
> 
> No, we cannot simply return an error here.

Thanks for the explanation.

       Andrew


From maxime.chevallier at bootlin.com  Sun Aug  2 07:59:28 2026
From: maxime.chevallier at bootlin.com (Maxime Chevallier)
Date: Sun, 2 Aug 2026 16:59:28 +0200
Subject: [PATCH v2 06/10] net: pcs: xpcs: add ANRESTART support for SGMII
 link recovery
In-Reply-To: <2886b24e-1c3d-48f7-8b22-9620fa0404f7@lunn.ch>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-8-coiaprant@gmail.com>
 <cfddb747-11d0-476d-8402-e0886cca582e@lunn.ch>
 <CALj3r0i1TCTgvm8FrJAJqmZgoJp61NCxc_xmVYaDFd-gh4H=7w@mail.gmail.com>
 <2886b24e-1c3d-48f7-8b22-9620fa0404f7@lunn.ch>
Message-ID: <82df688c-80c7-4536-afd8-1332005b0736@bootlin.com>

Hi,

On 8/2/26 16:30, Andrew Lunn wrote:
> On Sun, Aug 02, 2026 at 11:11:40AM +0800, Coia Prant wrote:
>> Hi Andrew,
>>
>> Thanks for the careful review.
>>
>>> This probably needs more wide spread testing. I would be afraid that
>>> if you kick off an AN cycle, you loose the link until it completes?
>>> You would of noticed that on your hardware, but other hardware
>>> obviously behaves differently...
>>
>> The trigger condition is: link is down AND an_complete is set.
> 
> Ah, i missed the "link is down" check. That makes it safer.
> 
> I still find it interesting that this devices is different. Has the
> silicon vendor done the integration of the licensed IP wrong?
> 
>> Regarding testing:
>> - Jiawen Wu has tested this on Wangxun NICs and reported no regression
>>   (Tested-by tag included in the patch).
> 
> Probably Maxime will test on the platform he has, but i don't know if
> any use the XPCS.

Indeed :( The only XPCS HW I have right now is the ksz9477 switch, and it
uses an old version of the IP with its own quirks.

I'll test it indeed, but I've yet to find some affordable HW with an XPCS
and that I can easily source :(

Maxime

> 
> 	Andrew



From andrew at lunn.ch  Sun Aug  2 08:19:37 2026
From: andrew at lunn.ch (Andrew Lunn)
Date: Sun, 2 Aug 2026 17:19:37 +0200
Subject: [PATCH v2 05/10] arm64: dts: rockchip: rk3568: add XPCS and
 fixed-clock nodes
In-Reply-To: <CALj3r0guMmNRN4EBbKz7stPNMSqkLBvms643YXBEDMzqHHM=FA@mail.gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-7-coiaprant@gmail.com>
 <2847036.GUH7pLUipS@diego>
 <CALj3r0gqvFZPb2SKRMPwf9chCx076rwCnXEoOMFch_V-fOKV6w@mail.gmail.com>
 <0e9d16df-7332-446b-9321-ce3de3111ffa@lunn.ch>
 <CALj3r0guMmNRN4EBbKz7stPNMSqkLBvms643YXBEDMzqHHM=FA@mail.gmail.com>
Message-ID: <4106e359-aec0-4f38-b74f-e9757a48eebb@lunn.ch>

On Sun, Aug 02, 2026 at 11:28:12AM +0800, Coia Prant wrote:
> Hi Andrew,
> 
> Thanks for the detailed questions. I've looked into the clock tree
> more carefully.
> 
> > Do you have documentation for these Combo PHYs?
> 
> Yes, the information is available in the Rockchip RK3568 TRM
> (Technical Reference Manual).
> The relevant clock tree details are also mentioned in upstream
> discussions and patches.
> 
> > Can these clocks be enabled/disabled?
> 
> The 100 MHz input clock to the Combo PHY is a prerequisite for its operation.
> It is generated by the SoC's internal PPLL and fed to the PHY through
> a fixed divider.
> It is not a gate that can be individually enabled or disabled by
> software; it is part of the
> PHY's fundamental operating conditions.
> 
> > And how is the Combo PHY fed with a clock? Does it have an external oscillator/crystal?
> 
> The Combo PHY is fed by an internal 100 MHz clock derived from the PPLL.
> Specifically, the PPLL runs at 200 MHz, and this is divided by 2 to provide the
>  100 MHz clock input to the Combo PHY.
> So, it does not use an external oscillator or crystal for its main
> reference clock;
> it's generated internally by the SoC.
> 
> To summarize:
> - The 125 MHz clocks (xpcs_gmac0_clk / xpcs_gmac1_clk) are outputs
> from the Combo PHY.
> - The Combo PHY itself is clocked by an internal 100 MHz clock from the PPLL.
> - This is all internal to the SoC and does not involve a board-level oscillator.
> 
> I hope this clarifies the clock architecture.

So what is the purpose of the fixed-clock DT nodes?

If you want to describe the hardware, is the PPLL in DT? Should you
then add a fixed divider to give 100MHz? And then a fixed multiplier
to give 125MHz?

   Andrew


From coiaprant at gmail.com  Sun Aug  2 11:25:45 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Mon, 03 Aug 2026 02:25:45 +0800
Subject: =?US-ASCII?Q?Re=3A_=5BPATCH_v2_06/10=5D_net=3A_pcs=3A_xpcs=3A_add_?=
 =?US-ASCII?Q?ANRESTART_support_for_SGMII_link_recovery?=
In-Reply-To: <2886b24e-1c3d-48f7-8b22-9620fa0404f7@lunn.ch>
References: <20260801142244.679437-2-coiaprant@gmail.com> <20260801142244.679437-8-coiaprant@gmail.com> <cfddb747-11d0-476d-8402-e0886cca582e@lunn.ch> <CALj3r0i1TCTgvm8FrJAJqmZgoJp61NCxc_xmVYaDFd-gh4H=7w@mail.gmail.com> <2886b24e-1c3d-48f7-8b22-9620fa0404f7@lunn.ch>
Message-ID: <2E676CD9-82F3-404A-B24D-27679042783B@gmail.com>

On August 2, 2026 10:30:52 PM GMT+08:00, Andrew Lunn <andrew at lunn.ch> wrote:
>On Sun, Aug 02, 2026 at 11:11:40AM +0800, Coia Prant wrote:
>> Hi Andrew,
>> 
>> Thanks for the careful review.
>> 
>> > This probably needs more wide spread testing. I would be afraid that
>> > if you kick off an AN cycle, you loose the link until it completes?
>> > You would of noticed that on your hardware, but other hardware
>> > obviously behaves differently...
>> 
>> The trigger condition is: link is down AND an_complete is set.
>
>Ah, i missed the "link is down" check. That makes it safer.
>
>I still find it interesting that this devices is different. Has the
>silicon vendor done the integration of the licensed IP wrong?
>
>> Regarding testing:
>> - Jiawen Wu has tested this on Wangxun NICs and reported no regression
>>   (Tested-by tag included in the patch).
>
>Probably Maxime will test on the platform he has, but i don't know if
>any use the XPCS.
>
>	Andrew

Hi Andrew,

Thanks for the follow-up.

Honestly, I'm not sure why the RK3568 XPCS behaves this way. I don't have
other DW XPCS hardware to compare against, so I can't say whether this is
a silicon integration quirk or something else.

What I can confirm is that on RK3568, the PCS does not restart AN
automatically when the link drops and comes back. Without this ANRESTART
change, the link stays dead forever.

I agree that wider testing is needed. If Maxime or anyone else finds a
regression on other hardware, we can revisit and condition it with a
platform-specific flag. For now, the change is limited to the SGMII
recovery path with a safe condition (link down + an_complete), so the
risk should be minimal.

Thanks,
Coia


From coiaprant at gmail.com  Sun Aug  2 11:33:32 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Mon, 03 Aug 2026 02:33:32 +0800
Subject: =?US-ASCII?Q?Re=3A_=5BPATCH_v2_07/10=5D_net=3A_pcs=3A_xpcs=3A_a?=
 =?US-ASCII?Q?dd_Rockchip_RK3568_platform_glue_driver?=
In-Reply-To: <302c67b0-6729-4930-a409-a2408dde33fc@lunn.ch>
References: <20260801142244.679437-2-coiaprant@gmail.com> <20260801142244.679437-9-coiaprant@gmail.com> <23d8e994-8552-466f-98e6-c47d5783525b@lunn.ch> <CALj3r0i5yVcJY8V75y0Hm5xjtZSBpH0DXSAWKsAUqguChVcxYA@mail.gmail.com> <302c67b0-6729-4930-a409-a2408dde33fc@lunn.ch>
Message-ID: <58C6FD30-02CF-4DC8-BE7F-4E2AAC5985E4@gmail.com>

On August 2, 2026 10:39:46 PM GMT+08:00, Andrew Lunn <andrew at lunn.ch> wrote:
>On Sun, Aug 02, 2026 at 11:20:27AM +0800, Coia Prant wrote:
>> > On Sat, Aug 01, 2026 at 10:22:31PM +0800, Coia Prant wrote:
>> > > The XPCS block contains four MII ports (0..3), each of which can be
>> > > routed to GMAC0 or GMAC1 via the pcs-handle property in the MAC node.
>> > > The hardware maps these ports to different MMDs:
>> > >   - port 0: MMD 7 (ROCKCHIP_MMD_MII)
>> > >   - port 1: MMD 2 (ROCKCHIP_MMD_MII1)
>> > >   - port 2: MMD 3 (ROCKCHIP_MMD_MII2)
>> > >   - port 3: MMD 4 (ROCKCHIP_MMD_MII3)
>> >
>> > Why is port 0 called ROCKCHIP_MMD_MII not ROCKCHIP_MMD_MII0 ?
>> 
>> Hi Andrew,
>> 
>> The naming follows the Rockchip TRM. The hardware documentation refers to
>> the MMD for the first port as simply "MII" without a numeric suffix, while
>> the other ports are named "MII1", "MII2", "MII3". I kept the naming
>> consistent with the TRM to make it easier to cross-reference.
>> 
>> As I understand it, this is probably because the MII controls not only
>> Port 0, but
>> also the entire PCS (these registers are read-only in Ports 1-3 and reflected
>> back to the MII).
>> 
>> If you prefer, I can rename it to ROCKCHIP_MMD_MII0 for consistency. Let me
>> know and I'll update it in v3.
>
>If the TRM gives it this name, then O.K. It just makes the code look
>odd, unbalanced.
>
>> > > +static int xpcs_rk_read_c22(struct mii_bus *bus, int addr, int reg)
>> > > +{
>> > > +     struct dw_xpcs_rk *pxpcs = bus->priv;
>> > > +     int dev;
>> > > +
>> > > +     if (!xpcs_rk_mdio_addr_validate(addr))
>> > > +             return -ENODEV;
>> > > +
>> > > +     dev = xpcs_rk_mdio_read_remapping(addr, MDIO_MMD_VEND2, reg);
>> >
>> > Does this mean C22 registers are mapped into the first 32 of C45
>> > MDIO_MMD_VEND2?
>> 
>> Yes, exactly. The C22 register space (reg 0-31) is mapped into the first
>> 32 registers of the VEND2 MMD (MMD 7). This is how the hardware is designed
>> and matches the standard C22 to C45 address mapping.
>
>Does the xpcs code actually perform any C22 access? A quick look
>suggests it is C45 only. xpcs_read() calls mdiodev_c45_read(). If C22
>is not needed, i would not provide these functions.
>
>> 
>> >
>> > > +static int xpcs_rk_read_c45(struct mii_bus *bus, int addr, int dev, int reg)
>> > > +{
>> > > +     struct dw_xpcs_rk *pxpcs = bus->priv;
>> > > +
>> > > +     if (!xpcs_rk_mdio_addr_validate(addr))
>> > > +             return -ENODEV;
>> > > +
>> > > +     dev = xpcs_rk_mdio_read_remapping(addr, dev, reg);
>> >
>> > Should it be returning an error for dev == MDIO_MMD_VEND2? Or at least
>> > if reg < 32?
>> 
>> No, we cannot simply return an error here.
>
>Thanks for the explanation.
>
>       Andrew

Hi Andrew,

You're right that the xpcs core uses C45 exclusively, and modern kernels
no longer require C22 callbacks for mdiobus_register().

However, I'd prefer to keep them for two reasons:
1. Debugging tools (mdio-tools, ethtool, etc.) often use C22 reads to
   inspect PHY/PCS registers. Having these callbacks makes debugging
   much easier without having to patch the driver.
2. It keeps the driver consistent with pcs-xpcs-plat.c, which also
   provides both C22 and C45 callbacks even though the xpcs core
   only uses C45.

If you strongly prefer removing them to keep the code minimal, I can do
that in v3. But I think the debug benefit justifies keeping them.

Thanks,
Coia


From coiaprant at gmail.com  Sun Aug  2 11:36:43 2026
From: coiaprant at gmail.com (Coia Prant)
Date: Mon, 03 Aug 2026 02:36:43 +0800
Subject: =?US-ASCII?Q?Re=3A_=5BPATCH_v2_05/10=5D_arm64=3A_dts=3A_rockchi?=
 =?US-ASCII?Q?p=3A_rk3568=3A_add_XPCS_and_fixed-clock_nodes?=
In-Reply-To: <4106e359-aec0-4f38-b74f-e9757a48eebb@lunn.ch>
References: <20260801142244.679437-2-coiaprant@gmail.com> <20260801142244.679437-7-coiaprant@gmail.com> <2847036.GUH7pLUipS@diego> <CALj3r0gqvFZPb2SKRMPwf9chCx076rwCnXEoOMFch_V-fOKV6w@mail.gmail.com> <0e9d16df-7332-446b-9321-ce3de3111ffa@lunn.ch> <CALj3r0guMmNRN4EBbKz7stPNMSqkLBvms643YXBEDMzqHHM=FA@mail.gmail.com> <4106e359-aec0-4f38-b74f-e9757a48eebb@lunn.ch>
Message-ID: <2810D16E-6D32-4D01-846A-65A853279603@gmail.com>

On August 2, 2026 11:19:37 PM GMT+08:00, Andrew Lunn <andrew at lunn.ch> wrote:
>On Sun, Aug 02, 2026 at 11:28:12AM +0800, Coia Prant wrote:
>> Hi Andrew,
>> 
>> Thanks for the detailed questions. I've looked into the clock tree
>> more carefully.
>> 
>> > Do you have documentation for these Combo PHYs?
>> 
>> Yes, the information is available in the Rockchip RK3568 TRM
>> (Technical Reference Manual).
>> The relevant clock tree details are also mentioned in upstream
>> discussions and patches.
>> 
>> > Can these clocks be enabled/disabled?
>> 
>> The 100 MHz input clock to the Combo PHY is a prerequisite for its operation.
>> It is generated by the SoC's internal PPLL and fed to the PHY through
>> a fixed divider.
>> It is not a gate that can be individually enabled or disabled by
>> software; it is part of the
>> PHY's fundamental operating conditions.
>> 
>> > And how is the Combo PHY fed with a clock? Does it have an external oscillator/crystal?
>> 
>> The Combo PHY is fed by an internal 100 MHz clock derived from the PPLL.
>> Specifically, the PPLL runs at 200 MHz, and this is divided by 2 to provide the
>>  100 MHz clock input to the Combo PHY.
>> So, it does not use an external oscillator or crystal for its main
>> reference clock;
>> it's generated internally by the SoC.
>> 
>> To summarize:
>> - The 125 MHz clocks (xpcs_gmac0_clk / xpcs_gmac1_clk) are outputs
>> from the Combo PHY.
>> - The Combo PHY itself is clocked by an internal 100 MHz clock from the PPLL.
>> - This is all internal to the SoC and does not involve a board-level oscillator.
>> 
>> I hope this clarifies the clock architecture.
>
>So what is the purpose of the fixed-clock DT nodes?
>
>If you want to describe the hardware, is the PPLL in DT? Should you
>then add a fixed divider to give 100MHz? And then a fixed multiplier
>to give 125MHz?
>
>   Andrew

Hi Andrew,

Thanks for the follow-up.

The fixed-clock nodes (`xpcs_gmac0_clk` and `xpcs_gmac1_clk`) serve as
clock inputs to the GMAC controller. From the GMAC driver's perspective,
the 125 MHz clock is a fixed, external input. The fact that it's
generated internally by the Combo PHY from the PPLL is an implementation
detail of the SoC, not something the GMAC driver needs to be aware of.

In the device tree, the principle is to describe the hardware
interfaces, not the entire internal clock generation pipeline. The
PPLL configuration (200 MHz) is already set up in the CRU node via
assigned-clocks. The Combo PHY's internal PLL that generates 125 MHz is
part of the PHY's own operation, not something that the system clock
framework needs to manage or represent as a separate clock node.

If we were to fully model PPLL -> divider (100 MHz) -> Combo PHY PLL
(125 MHz) in DT, we would be overcomplicating the DT for no real gain.
It would also make board variations harder to handle (e.g., if a board
uses an external oscillator instead of the internal path, we'd have to
rewrite the entire clock tree). This approach is consistent with how
other similar SoCs (e.g., pcs-xpcs-plat) handle fixed reference clocks.

I think keeping these as fixed-clock nodes is the right balance between
accuracy and simplicity. If you still have concerns, I can add a comment
in the DT to clarify that these clocks are outputs of the Combo PHY.

Thanks,
Coia


From andrew at lunn.ch  Sun Aug  2 12:00:38 2026
From: andrew at lunn.ch (Andrew Lunn)
Date: Sun, 2 Aug 2026 21:00:38 +0200
Subject: [PATCH v2 07/10] net: pcs: xpcs: add Rockchip RK3568 platform
 glue driver
In-Reply-To: <58C6FD30-02CF-4DC8-BE7F-4E2AAC5985E4@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-9-coiaprant@gmail.com>
 <23d8e994-8552-466f-98e6-c47d5783525b@lunn.ch>
 <CALj3r0i5yVcJY8V75y0Hm5xjtZSBpH0DXSAWKsAUqguChVcxYA@mail.gmail.com>
 <302c67b0-6729-4930-a409-a2408dde33fc@lunn.ch>
 <58C6FD30-02CF-4DC8-BE7F-4E2AAC5985E4@gmail.com>
Message-ID: <ba5522ab-1e77-4236-89cd-9483ed1f2da0@lunn.ch>

> Hi Andrew,
> 
> You're right that the xpcs core uses C45 exclusively, and modern kernels
> no longer require C22 callbacks for mdiobus_register().
> 
> However, I'd prefer to keep them for two reasons:
> 1. Debugging tools (mdio-tools, ethtool, etc.) often use C22 reads to
>    inspect PHY/PCS registers. Having these callbacks makes debugging
>    much easier without having to patch the driver.
> 2. It keeps the driver consistent with pcs-xpcs-plat.c, which also
>    provides both C22 and C45 callbacks even though the xpcs core
>    only uses C45.
> 
> If you strongly prefer removing them to keep the code minimal, I can do
> that in v3. But I think the debug benefit justifies keeping them.

Keep them. I was thinking it side steps the issue of accidentally
mixing up C22 registers and C45 registers. But there are useful use
cases for C22.

      Andrew


From stefandoesinger at gmail.com  Sun Aug  2 13:33:32 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:32 +0300
Subject: [PATCH v9 00/12] ZTE zx297520v3 clock bindings and driver
Message-ID: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>

Hi,

I am sending version 9 of my zx297520v3 clock patch. The main change vs 
v8 is that the clock driver uses parent_data.hw instead of global names 
for parent matching.

I think the list of clocks in my driver is fairly complete; It is 
certainly a lot better than what the downstream ZTE drivers have. I 
deduced a lot of it by trial and error. I am sure there are some clocks 
missing that will need to be added to the binding later. AFAIU adding 
clocks is not an issue, but removing or reordering them is an ABI break.

Merge dependencies: The binding patches (1, 2, 3) and DT patch (12) in 
order. The MFD parent (4) is independent, although changing the ordering 
likely needs trivial adjustments in MAINTAINERS. The clock driver 
patches depend on the headers added by 1-3 and apply in order 
(5-10). Reset needs headers (1-3), but it is independent of clock.

Though in practice mfd, clock and reset drivers are dead code until 
patch 12 adds the DT nodes. Clock and reset drivers won't be 
instantiated without the MFD parent.

Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>
---
Changes in v9:
*) Use parent_data in the clock driver instead of parent_names. That 
required some infrastructure to keep track of registered clocks rather 
than devm-and-forget. A nice side effect is that it uncovered a missing 
top->matrix clock link. (Brian)

*) Remove the self-referencing USB PHY reset in topcrm (Krzysztof)

*) (Hopefully) get unevaluatedProperties and additionalProperties right 
in topcrm (Krzysztof)

*) GMAC and SDIO on the matrixcrm controller need topcrm's ahb clock as 
parent. It is added to Matrix's binding and fixed in the driver

*) LSP reset and pclk handling moved from clock child to mfd parent 
driver

- Link to v8: https://lore.kernel.org/r/20260727-zx29clk-v8-0-7a107b00f1dd at gmail.com

Changes in v8:
*) Put #phy-cells into topcrm. The mfd child will have to assign itself 
the MFD parent's of_node to satisfy the check in phy-core.c's
__of_phy_provider_register().

*) Added another register to matrixclk that controls in-controller 
distribution gates. No functional change, but it nicely confirms the 
top->matrix interface

*) Changed clock indices to start at 0, adjusted driver to not use 0 as 
"do not export" marker.

*) Rename DT fixed clock nodes from osc26m -> clock-26m and osc32k -> 
clock-32k

*) Fixed more issues pointed out by Sashiko

- Link to v7: https://lore.kernel.org/r/20260717-zx29clk-v7-0-408411cfcf36 at gmail.com

Changes in v7:

*) Moved DT bindings back to clock/ (Krzysztof)
*) Added a usb-phy child node to top to expose the USB status register
*) Add clock gates that control input of PLL clocks into topcrm and 
forward to matrixcrm. Strip down matrixcrm inputs in the bindings to the 
known clocks in this register
*) Add DDR clocks and resets, arm arch timer clock. They are marked 
critical and tell CCF not to gate off the aforementioned input/forward 
gates
*) Remove syscon from matrixcrm (Krzysztof). The DDR mailbox system is 
on a different controller (soc_sys at 0x140000). ZTE's code merely 
controlled clocks in matrixcrm, which doesn't require syscon on this 
controller.
*) Consistently use double quotes in bindings and full include paths 
(Krzysztof)

The individual patches call out changes in more detail.

- Link to v6: https://lore.kernel.org/r/20260702-zx29clk-v6-0-377b704f80c4 at gmail.com

Changes in v6:
*) Use MFD for all 3 controllers - I hope both Conor and Philipp will 
agree. I kept top and matrix bindings in soc/zte and lsp in clock/ 
though.

*) Clean up issues found by Sashiko. I pointed them out in the individual 
patches. They are localized fixes and don't affect the overall design

*) Small code consistency: Changed "zx297520v3_lsp" to use "-" , "rst" 
in driver names to "reset"

Changes in v5:
*) Use MFD instead of aux bus for top and matrix clocks
*) Move top and matrix bindings to soc/zte
*) Give USB PHY its own resets
*) Other localized changes are noted in the individual patches
- Link to v4: https://lore.kernel.org/r/20260616-zx29clk-v4-0-ca994bd22e9d at gmail.com

Changes in v4:
*) Use syscon and regmap instead of raw IO
*) Move reset to its own driver on the aux bus, but keep reset and clk 
in the same binding as it matches the way the hardware works
*) Go back to having matrixclk in its own device because syscon deals 
poorly with multi io reg devices. List all PLL outputs from topclk as 
inputs to matrixclk
*) Some more hardware research: Figure out the parents of the 4 possible
GPIO clock outputs and declare them in the driver. They are unused on 
the hardware I have, but they show that all PLLs can be used.

- Link to v3: https://lore.kernel.org/r/20260529-zx29clk-v3-0-c7fe54ea388f at gmail.com

Changes in v3:
Model top and matrix clocks as one device
Add PLL driver
Fixed a few issues found by Sashiko: register lock, some missing devm_, 
error handling

v2: Fix build issues introduced by checkpatch.pl fixes that I didn't 
spot earlier.

---
Stefan D?singer (12):
      dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller
      dt-bindings: clk: zte: Add zx297520v3 matrix clock and reset controller
      dt-bindings: clk: zte: Add zx297520v3 LSP clock and reset controller
      mfd: zx297520v3: Add a clock and reset MFD driver
      clk: zte: Add Clock registration infrastructure
      clk: zte: Add regmap-based clocks
      clk: zte: Add zx PLL support infrastructure
      clk: zte: Introduce a driver for zx297520v3 top clocks
      clk: zte: Introduce a driver for zx297520v3 matrix clocks
      clk: zte: Introduce a driver for zx297520v3 LSP clocks
      reset: zte: Add a zx297520v3 reset driver
      ARM: dts: zte: Declare zx297520v3 CRM device nodes

 .../bindings/clock/zte,zx297520v3-lspcrm.yaml      |  101 ++
 .../bindings/clock/zte,zx297520v3-matrixcrm.yaml   |   98 ++
 .../bindings/clock/zte,zx297520v3-topcrm.yaml      |  122 ++
 MAINTAINERS                                        |    9 +
 arch/arm/boot/dts/zte/zx297520v3.dtsi              |   81 +-
 drivers/clk/Kconfig                                |    1 +
 drivers/clk/Makefile                               |    1 +
 drivers/clk/zte/Kconfig                            |   28 +
 drivers/clk/zte/Makefile                           |    6 +
 drivers/clk/zte/clk-regmap.c                       |  237 +++
 drivers/clk/zte/clk-zx.c                           |  189 +++
 drivers/clk/zte/clk-zx.h                           |  142 ++
 drivers/clk/zte/clk-zx297520v3.c                   | 1571 ++++++++++++++++++++
 drivers/clk/zte/pll-zx.c                           |  580 ++++++++
 drivers/mfd/Kconfig                                |   11 +
 drivers/mfd/Makefile                               |    2 +
 drivers/mfd/zte-zx297520v3-crm.c                   |  117 ++
 drivers/reset/Kconfig                              |   13 +
 drivers/reset/Makefile                             |    1 +
 drivers/reset/reset-zte-zx297520v3.c               |  236 +++
 include/dt-bindings/clock/zte,zx297520v3-clk.h     |  144 ++
 include/dt-bindings/phy/zte,zx297520v3-topcrm.h    |   12 +
 include/dt-bindings/reset/zte,zx297520v3-reset.h   |   61 +
 23 files changed, 3755 insertions(+), 8 deletions(-)
---
base-commit: bee763d5f341b99cf472afeb508d4988f62a6ca1
change-id: 20260510-zx29clk-2e4d39e3128c

Best regards,
-- 
Stefan D?singer <stefandoesinger at gmail.com>



From stefandoesinger at gmail.com  Sun Aug  2 13:33:33 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:33 +0300
Subject: [PATCH v9 01/12] dt-bindings: clk: zte: Add zx297520v3 top clock
 and reset controller
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-1-d05530d85d28@gmail.com>

These SoCs have 3 clock and reset controllers: Top, Matrix and LSP.
Clocks go from oscillator -> top -> matrix -> LSP, with a register in
top controlling most clocks that get passed to matrix and a register in
matrix controlling all the clocks that get passed to LSP.

Generally every device has two clocks (one work clock, and one that
connects it to the bus, I call it PCLK), two reset bits (I don't know
what the difference is - sometimes asserting one is enough to reset the
device, sometimes both need to be asserted). PCLK and WCLK are
controlled by individual gates. Some devices have a mux and/or a
divider for their work clock. Some devices, like the GPIO controller,
only have reset bits and no clocks.

The top clock controller is fed by a 26 MHz external oscillator and has 4
PLLs to generate other clock rates. ZTE's kernel mostly relies on the
boot ROM to set up PLLs, but one LTE-related PLL is not configured
on some boards. Therefore my driver contains code to program PLLs. It
produces identical settings as the boot ROM for the pre-programmed
frequencies.

Not all clocks will have an explicit user in the end. I am defining a
lot of them simply to shut them off. The boot loader sets up a few of
the proprietary timers, which will send regular IRQs (although the
kernel of course doesn't need to listen to them). I don't plan to add a
driver for the proprietary timer as I see no use for them - the ARM arch
timer works just fine. I will add a driver for the very similar
proprietary watchdog though.

The clock list in this patch is pretty complete but not exhaustive.
There are other bits that are enabled, but I couldn't deduce what they
are controlling by trial and error. Some of them seem to do nothing.
Others cause an instant hang of the board when disabled. It is quite
likely that a handful more clocks will be added in the future, but not a
large number.

Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

---

Changes v8->v9:
*) "unevaluatedProperties: false" for the syscon child node (Krzysztof)
*) "additionalProperties: false" for the binding itself (Krzysztof)
*) Remove USB PHY resets, but USB controller resets remain unchanged
(Krzysztof)
*) Rename syscon reboot node to "reboot". While both "reboot" and
"syscon-reboot" are common in actual device trees, syscon-reboot.yaml's
own example uses "reboot". DT's Generic Names Recommendation
contains neither (Sashiko, patch 12)

Changes v7->v8 (all Krzysztof):
*) Fold the PHY into the top-level node
*) Start clock indices at 0
*) Rename the phy include file to zte,zx297520v3-topcrm.h.

And a non-change despite/because of new discoveries: USB has more PHY
and DWC controls in a different controller at 0x140000. ZTE calls this
controller SOC_SYS, and it is a textbook case for syscon: It has a lot
of different controls: A remap flag for boot ROM, a control to bring the
A53 up in arm32 vs aarch64 mode, the entrypoint of the LTE DSP, SDIO,
I2S, ...

USB-wise it has a register with PHY and DWC controls: Endianness for the
DWC2 registers, PHY clock adjustments, PHY and DWC host/device switch.
In spite of this I think topcrm is the better home for #phy-cells: The
USB ready flags in topcrm are essential for bringing up the USB devices
whereas the soc_sys controls are a curiosity. Host mode only works if I
externally inject VBUS. Clock adjustment is too inflexible to be of real
value. Flipping the DWC registers to big endian works, but what for? If
I find a use case for changing the default USB values in soc_sys it can
be handled via syscon.

soc_sys also has power controls for a few devices, including USB. This
will be handled via #power-domain-cells.

The self-referencing reset property causes the following boot messages:
/soc/clock-controller at 13b000: Fixed dependency cycle(s) with
    /soc/clock-controller at 13b000
/soc/clock-controller at 13b000: Fixed dependency cycle(s) with
    /soc/clock-controller at 13b000

Do I need to add post-init-providers? As far as I understand it, this
message is informational and not a problem that needs to be addressed.

Changes v6->v7:
*) Moved the bindings back into clock/ (Krzysztof). Use clock-controller@
in example and in the later DTSI patch.

*) With a lucky find in strings in LTE's cpko.ko blob I could make sense
of the register that controls which clocks get passed from PLLs into
the clock distribution inside topcrm. Importantly that also narrows
which clocks can possibly be passed to Matrix, so I could slim down the
bindings considerably.

*) Add a usb phy child node with the phy schema added in the previous
patch. I don't expect the USB status register to ever be in another
place, so the binding expresses the @84 expectation.

Changes v5->v6:
Set value for syscon-reboot example (Sashiko). It was my intention to
set only the lowest bit, and I think Sashiko is right that without
'value' being set, all other bits are actively set to 0. It shouldn't
matter given my understanding of the hardware (afaics all other bits are
ignored), but actively clearing bits was not my intention.

I haven't changed the name match for "syscon-reboot". I see plenty of
examples of hardcoding this string as opposed to having a regex for
syscon-reboot at 12345678 in other bindings.

Changes v4->v5:

Rename from zte,zx297520v3-topclk to zte,zx297520v3-topcrm and move to
soc/zte
Fix path in MAINTAINERS
Add syscon-reboot node to the binding
Give the USB and HSIC PHY resets their own reset control
---
 .../bindings/clock/zte,zx297520v3-topcrm.yaml      | 122 +++++++++++++++++++++
 MAINTAINERS                                        |   4 +
 include/dt-bindings/clock/zte,zx297520v3-clk.h     |  66 +++++++++++
 include/dt-bindings/phy/zte,zx297520v3-topcrm.h    |  12 ++
 include/dt-bindings/reset/zte,zx297520v3-reset.h   |  30 +++++
 5 files changed, 234 insertions(+)

diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
new file mode 100644
index 000000000000..ecd332f421b9
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
@@ -0,0 +1,122 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx297520v3-topcrm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx297520v3 SoC top clock and reset controller
+
+maintainers:
+  - Stefan D?singer <stefandoesinger at gmail.com>
+
+description: |
+  The zx297520v3 top clock and reset controller generates clocks for core
+  devices on the board like the main bus, USB and timers. In addition to clocks
+  it has reset controls for peripherals, a global board reset, watchdog reset
+  controls and a USB status register.
+
+  The controller has two clock inputs: a 26 MHz and a 32 kHz external
+  oscillator. They need to be provided as input clocks. The controller provides
+  PLL output frequencies to downstream clock controllers.
+
+  The USB PHY is exposed as two PHYs, one for USB and another for HSIC. Note
+  that while the controls for both PHYs are always present, the PHYs themselves
+  may be absent. If this is the case the controller never signals that the
+  missing PHY is ready.
+
+  All available clocks are defined as preprocessor macros in the
+  "include/dt-bindings/clock/zte,zx297520v3-clk.h" header. The resets are
+  defined in the "include/dt-bindings/reset/zte,zx297520v3-reset.h" header. PHY
+  defines are found in "include/dt-bindings/phy/zte,zx297520v3-topcrm.h".
+
+properties:
+  compatible:
+    items:
+      - const: zte,zx297520v3-topcrm
+      - const: syscon
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: 26 MHz external oscillator
+      - description: 32 kHz external oscillator
+
+  clock-names:
+    items:
+      - const: osc26m
+      - const: osc32k
+
+  interrupts:
+    items:
+      - description: IRQ reporting USB connection
+      - description: IRQ reporting USB disconnection
+      - description: IRQ reporting HSIC connection
+      - description: IRQ reporting HSIC disconnection
+
+  interrupt-names:
+    items:
+      - const: usb-up
+      - const: usb-down
+      - const: hsic-up
+      - const: hsic-down
+
+  "#clock-cells":
+    const: 1
+
+  "#phy-cells":
+    const: 1
+
+  "#reset-cells":
+    const: 1
+
+  reboot:
+    type: object
+    $ref: /schemas/power/reset/syscon-reboot.yaml#
+    unevaluatedProperties: false
+    description:
+      Reboot method for the SoC.
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - clock-names
+  - interrupts
+  - interrupt-names
+  - reboot
+  - "#clock-cells"
+  - "#phy-cells"
+  - "#reset-cells"
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/interrupt-controller/arm-gic.h>
+    #include <dt-bindings/reset/zte,zx297520v3-reset.h>
+    #include <dt-bindings/phy/zte,zx297520v3-topcrm.h>
+    #include <dt-bindings/clock/zte,zx297520v3-clk.h>
+
+    topcrm: clock-controller at 13b000 {
+        compatible = "zte,zx297520v3-topcrm", "syscon";
+        reg = <0x0013b000 0x400>;
+        clocks = <&osc26m>, <&osc32k>;
+        clock-names = "osc26m", "osc32k";
+        interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>,
+                     <GIC_SPI 43 IRQ_TYPE_EDGE_RISING>,
+                     <GIC_SPI 45 IRQ_TYPE_EDGE_RISING>,
+                     <GIC_SPI 46 IRQ_TYPE_EDGE_RISING>;
+        interrupt-names = "usb-up", "usb-down", "hsic-up", "hsic-down";
+        #clock-cells = <1>;
+        #phy-cells = <1>;
+        #reset-cells = <1>;
+
+        reboot {
+          compatible = "syscon-reboot";
+          offset = <0x0>;
+          mask = <0x1>;
+          value = <0x1>;
+        };
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index 92a2167f1eb8..d53c0a2c7d9b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3910,8 +3910,12 @@ L:	linux-arm-kernel at lists.infradead.org (moderated for non-subscribers)
 S:	Odd fixes
 F:	Documentation/arch/arm/zte/
 F:	Documentation/devicetree/bindings/arm/zte.yaml
+F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
 F:	arch/arm/boot/dts/zte/
 F:	arch/arm/mach-zte/
+F:	include/dt-bindings/clock/zte,zx297520v3-clk.h
+F:	include/dt-bindings/phy/zte,zx297520v3-topcrm.h
+F:	include/dt-bindings/reset/zte,zx297520v3-reset.h
 
 ARM/ZYNQ ARCHITECTURE
 M:	Michal Simek <michal.simek at amd.com>
diff --git a/include/dt-bindings/clock/zte,zx297520v3-clk.h b/include/dt-bindings/clock/zte,zx297520v3-clk.h
new file mode 100644
index 000000000000..3aed94ccc26f
--- /dev/null
+++ b/include/dt-bindings/clock/zte,zx297520v3-clk.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) Stefan D?singer.
+ */
+
+#ifndef __DT_BINDINGS_CLOCK_ZX297520V3_H
+#define __DT_BINDINGS_CLOCK_ZX297520V3_H
+
+#define ZX297520V3_M0_WCLK			0
+#define ZX297520V3_SRAM1_PCLK			1
+#define ZX297520V3_SRAM2_PCLK			2
+#define ZX297520V3_UART0_WCLK			3
+#define ZX297520V3_UART0_PCLK			4
+#define ZX297520V3_I2C0_WCLK			5
+#define ZX297520V3_I2C0_PCLK			6
+#define ZX297520V3_RTC_WCLK			7
+#define ZX297520V3_RTC_PCLK			8
+#define ZX297520V3_LPM_GSM_WCLK			9
+#define ZX297520V3_LPM_GSM_PCLK			10
+#define ZX297520V3_LPM_LTE_WCLK			11
+#define ZX297520V3_LPM_LTE_PCLK			12
+#define ZX297520V3_LPM_TD_WCLK			13
+#define ZX297520V3_LPM_TD_PCLK			14
+#define ZX297520V3_LPM_W_WCLK			15
+#define ZX297520V3_LPM_W_PCLK			16
+#define ZX297520V3_TIMER_T08_WCLK		17
+#define ZX297520V3_TIMER_T08_PCLK		18
+#define ZX297520V3_TIMER_T09_WCLK		19
+#define ZX297520V3_TIMER_T09_PCLK		20
+#define ZX297520V3_MPLL				21
+#define ZX297520V3_MPLL_D5			22
+#define ZX297520V3_DPLL				23
+#define ZX297520V3_GPLL				24
+#define ZX297520V3_GPLL_D2			25
+#define ZX297520V3_GATED_OSC26M			26
+#define ZX297520V3_PMM_WCLK			27
+#define ZX297520V3_PMM_PCLK			28
+#define ZX297520V3_OUT0_WCLK			29
+#define ZX297520V3_OUT1_WCLK			30
+#define ZX297520V3_OUT2_WCLK			31
+#define ZX297520V3_OUT32K_WCLK			32
+#define ZX297520V3_RMIIPHY_WCLK			33
+#define ZX297520V3_TIMER_T12_WCLK		34
+#define ZX297520V3_TIMER_T12_PCLK		35
+#define ZX297520V3_TIMER_T13_WCLK		36
+#define ZX297520V3_TIMER_T13_PCLK		37
+#define ZX297520V3_TIMER_T14_WCLK		38
+#define ZX297520V3_TIMER_T14_PCLK		39
+#define ZX297520V3_TIMER_T15_WCLK		40
+#define ZX297520V3_TIMER_T15_PCLK		41
+#define ZX297520V3_TIMER_T16_WCLK		42
+#define ZX297520V3_TIMER_T16_PCLK		43
+#define ZX297520V3_TIMER_T17_WCLK		44
+#define ZX297520V3_TIMER_T17_PCLK		45
+#define ZX297520V3_WDT_T18_WCLK			46
+#define ZX297520V3_WDT_T18_PCLK			47
+#define ZX297520V3_USIM1_WCLK			48
+#define ZX297520V3_USIM1_PCLK			49
+#define ZX297520V3_AHB_WCLK			50
+#define ZX297520V3_AHB_PCLK			51
+#define ZX297520V3_USB_WCLK			52
+#define ZX297520V3_USB_PCLK			53
+#define ZX297520V3_HSIC_WCLK			54
+#define ZX297520V3_HSIC_PCLK			55
+
+#endif /* __DT_BINDINGS_CLOCK_ZX297520V3_H */
diff --git a/include/dt-bindings/phy/zte,zx297520v3-topcrm.h b/include/dt-bindings/phy/zte,zx297520v3-topcrm.h
new file mode 100644
index 000000000000..b36ccf2f54ef
--- /dev/null
+++ b/include/dt-bindings/phy/zte,zx297520v3-topcrm.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) Stefan D?singer.
+ */
+
+#ifndef __DT_BINDINGS_PHY_ZTE_ZX297520V3_TOPCRM_H
+#define __DT_BINDINGS_PHY_ZTE_ZX297520V3_TOPCRM_H
+
+#define ZX297520V3_USB_PHY	0
+#define ZX297520V3_HSIC_PHY	1
+
+#endif /* __DT_BINDINGS_PHY_ZTE_ZX297520V3_TOPCRM_H */
diff --git a/include/dt-bindings/reset/zte,zx297520v3-reset.h b/include/dt-bindings/reset/zte,zx297520v3-reset.h
new file mode 100644
index 000000000000..f60aaccf0f4d
--- /dev/null
+++ b/include/dt-bindings/reset/zte,zx297520v3-reset.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) Stefan D?singer.
+ */
+
+#ifndef __DT_BINDINGS_RESET_ZX297520V3_H
+#define __DT_BINDINGS_RESET_ZX297520V3_H
+
+#define ZX297520V3_ZSP_RESET			0
+#define ZX297520V3_UART0_RESET			1
+#define ZX297520V3_I2C0_RESET			2
+#define ZX297520V3_RTC_RESET			3
+#define ZX297520V3_TIMER_T08_RESET		4
+#define ZX297520V3_TIMER_T09_RESET		5
+#define ZX297520V3_PMM_RESET			6
+#define ZX297520V3_GPIO_RESET			7
+#define ZX297520V3_GPIO8_RESET			8
+#define ZX297520V3_TIMER_T12_RESET		9
+#define ZX297520V3_TIMER_T13_RESET		10
+#define ZX297520V3_TIMER_T14_RESET		11
+#define ZX297520V3_TIMER_T15_RESET		12
+#define ZX297520V3_TIMER_T16_RESET		13
+#define ZX297520V3_TIMER_T17_RESET		14
+#define ZX297520V3_WDT_T18_RESET		15
+#define ZX297520V3_USIM1_RESET			16
+#define ZX297520V3_AHB_RESET			17
+#define ZX297520V3_USB_RESET			18
+#define ZX297520V3_HSIC_RESET			19
+
+#endif /* __DT_BINDINGS_RESET_ZX297520V3_H */

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:34 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:34 +0300
Subject: [PATCH v9 02/12] dt-bindings: clk: zte: Add zx297520v3 matrix
 clock and reset controller
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-2-d05530d85d28@gmail.com>

This controller contains clocks and resets for high speed devices on the
zx297520v3 board and hardware spinlocks that I expect will be necessary
to communicate correctly with the LTE DSP firmware blob.

I don't know much about the hwlocks yet other than that they exist, so
there is no header yet adding meaning to their indices. Presumably that
meaning doesn't exist in hardware and is de facto defined by ZTE's
firmware blobs running on the rprocs.

The zte-zx297520v3-crm driver will instantiate separate MFD children for
clocks and resets, which are handled by separate clock and reset
drivers.

Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski at oss.qualcomm.com>
Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>
---

v8->v9:
*) The SDIO controllers and GMAC depend on topcrm's ahb clock for
register access / dma operation. Add the input to the binding.

v7->v8:
*) Start clock indices at 0
*) Fix indentation

*) Add a prose note about discovered registers that allow writing
CNTVAL. I am not exposing them in any way because for now I see no need
to use this functionality. It is possible that the counter resets to 0
during standby, which means someone needs to fix it up using those
registers.

Nevertheless, no syscon yet for these reasons: I have no idea how CNTVAL
behaves during standby. It might be just fine. Standby very likely needs
firmware handling running on the Cortex M0, which can restore CNTVAL if
necessary. If Linux needs to do it, it can probably be done by the clock
or MFD parent driver. Given what I've learned so far I'd rather add
syscon later than put it in place without a clear user.

Details of how to manipulate CNTVAL will be in the matrix clk driver.

*) A non-change in the binding text, but worth mentioning: Register
0x118 contains gates that gate off matrix-synthesized PLL fixed
dividers. They reinforce the understanding of the top->matrix interface
and the adequacy of the bindings.

v6->v7:

*) Moved it back to clock/

*) Remove syscon compatible again. I looked into remaining unknown
registers and I am convinced that there is no odd misc register left
that can't be exposed as a proper subsystem provider.

The controls in 0x100 that ZTE's DRAM reading code exercises are auto
clock gating controls and the actual mailbox is in a different
controller.

Unknown registers in 0x120-0x12c are a frequency voting mechanism for
the AXI bus clock. The driver will participate in that voting rather
than set the frequency via 0x0. If need be it could be exposed
as #interconnect-cells, but it won't necessitate "syscon".

*) Reduce the number of input clocks to match register controls in
topclk's input control that affect matrix and LSP.

*) Add DDR mux and gates. Not to change them, but to make sure CCF knows
about which PLL is feeding the sysmem memory

Changes
v5->v6:
Add syscon compatible - Conor suggested this after v4 and I forgot it in v5.

v4->v5:
Move binding to soc/zte
Remove topclk from the example
Add #hwlock-cells for hw spinlock registers
Add more clocks I stumbled into: sram0 and another LTE related device

v3->v4:
Split matrixclk into its own controller again because syscon/regmap
deals poorly with device nodes that have more than one memory region. As
a consequence I am passing all PLL outputs generated on Topclk down to
Matrixclk.
---
 .../bindings/clock/zte,zx297520v3-matrixcrm.yaml   | 98 ++++++++++++++++++++++
 MAINTAINERS                                        |  1 +
 include/dt-bindings/clock/zte,zx297520v3-clk.h     | 41 +++++++++
 include/dt-bindings/reset/zte,zx297520v3-reset.h   | 12 +++
 4 files changed, 152 insertions(+)

diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
new file mode 100644
index 000000000000..bb7df02c8a0f
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
@@ -0,0 +1,98 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx297520v3-matrixcrm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx297520v3 SoC matrix clock and reset controller
+
+maintainers:
+  - Stefan D?singer <stefandoesinger at gmail.com>
+
+description: |
+  This controller contains clock and reset controls for high speed devices on
+  the zx297520v3 board: The CPU, RAM, SDIO and Ethernet clocks and resets are
+  found here. This controller requires PLL-generated clocks from Topcrm as well
+  as the fixed 26 MHz and 32 kHz oscillators found on this board.
+
+  This controller also contains hardware mutex registers for synchronization
+  with different processors on this board.
+
+  An additional curiosity are registers that allow setting (but not reading) the
+  ARM architected timer's CNTVAL. This is not exposed in the binding because
+  there shouldn't be a need to access it.
+
+  All available clocks are defined as preprocessor macros in the
+  "include/dt-bindings/clock/zte,zx297520v3-clk.h" header. Resets are defined in
+  the "include/dt-bindings/reset/zte,zx297520v3-reset.h" header.
+
+properties:
+  compatible:
+    const: zte,zx297520v3-matrixcrm
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: 26 MHz external oscillator
+      - description: 32 kHz external oscillator
+      - description: AHB clock from topcrm
+      - description: Main PLL output from topcrm (usually 624 MHz)
+      - description: Main PLL subdivision factor 5
+      - description: Dpll output from topcrm (usually 491.52 MHz)
+      - description: Gpll output from topcrm (usually 200 MHz)
+      - description: Gpll subdivision factor 2
+      - description: osc26m work clock from topcrm
+
+  clock-names:
+    items:
+      - const: osc26m
+      - const: osc32k
+      - const: ahb
+      - const: mpll
+      - const: mpll-d5
+      - const: dpll
+      - const: gpll
+      - const: gpll-d2
+      - const: wclk-osc26m
+
+  "#clock-cells":
+    const: 1
+
+  "#hwlock-cells":
+    const: 1
+
+  "#reset-cells":
+    const: 1
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - clock-names
+  - "#clock-cells"
+  - "#hwlock-cells"
+  - "#reset-cells"
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/zte,zx297520v3-clk.h>
+    #include <dt-bindings/reset/zte,zx297520v3-reset.h>
+
+    clock-controller at 1306000 {
+        compatible = "zte,zx297520v3-matrixcrm";
+        reg = <0x01306000 0x400>;
+        clocks = <&osc26m>, <&osc32k>, <&topcrm ZX297520V3_AHB_PCLK>,
+                 <&topcrm ZX297520V3_MPLL>, <&topcrm ZX297520V3_MPLL_D5>,
+                 <&topcrm ZX297520V3_DPLL>, <&topcrm ZX297520V3_GPLL>,
+                 <&topcrm ZX297520V3_GPLL_D2>,
+                 <&topcrm ZX297520V3_GATED_OSC26M>;
+        clock-names = "osc26m", "osc32k", "ahb", "mpll", "mpll-d5", "dpll",
+                      "gpll", "gpll-d2", "wclk-osc26m";
+        #clock-cells = <1>;
+        #hwlock-cells = <1>;
+        #reset-cells = <1>;
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index d53c0a2c7d9b..9a919e60254a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3910,6 +3910,7 @@ L:	linux-arm-kernel at lists.infradead.org (moderated for non-subscribers)
 S:	Odd fixes
 F:	Documentation/arch/arm/zte/
 F:	Documentation/devicetree/bindings/arm/zte.yaml
+F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
 F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
 F:	arch/arm/boot/dts/zte/
 F:	arch/arm/mach-zte/
diff --git a/include/dt-bindings/clock/zte,zx297520v3-clk.h b/include/dt-bindings/clock/zte,zx297520v3-clk.h
index 3aed94ccc26f..a8477e9b2a14 100644
--- a/include/dt-bindings/clock/zte,zx297520v3-clk.h
+++ b/include/dt-bindings/clock/zte,zx297520v3-clk.h
@@ -63,4 +63,45 @@
 #define ZX297520V3_HSIC_WCLK			54
 #define ZX297520V3_HSIC_PCLK			55
 
+#define ZX297520V3_SYS_TIMER_WCLK		0
+#define ZX297520V3_CPU_WCLK			1
+#define ZX297520V3_CPU_PCLK			2
+#define ZX297520V3_ZSP_WCLK			3
+#define ZX297520V3_DDR_CTRL_PCLK		4
+#define ZX297520V3_DDR_CTRL_WCLK		5
+#define ZX297520V3_EDCP_WCLK			6
+#define ZX297520V3_EDCP_PCLK			7
+#define ZX297520V3_SD0_WCLK			8
+#define ZX297520V3_SD0_PCLK			9
+#define ZX297520V3_SD0_CDET			10
+#define ZX297520V3_SD1_WCLK			11
+#define ZX297520V3_SD1_PCLK			12
+#define ZX297520V3_SD1_CDET			13
+#define ZX297520V3_NAND_WCLK			14
+#define ZX297520V3_NAND_PCLK			15
+#define ZX297520V3_DMA_PCLK			16
+#define ZX297520V3_MBOX_PCLK			17
+#define ZX297520V3_PDCFG_WCLK			18
+#define ZX297520V3_PDCFG_PCLK			19
+#define ZX297520V3_SSC_WCLK			20
+#define ZX297520V3_SSC_PCLK			21
+#define ZX297520V3_AXI_WCLK			22
+#define ZX297520V3_GMAC_WCLK			23
+#define ZX297520V3_GMAC_PCLK			24
+#define ZX297520V3_GMAC_AHB			25
+#define ZX297520V3_VOU_WCLK			26
+#define ZX297520V3_VOU_PCLK			27
+#define ZX297520V3_LSP_MPLL_D5_WCLK		28
+#define ZX297520V3_LSP_MPLL_D4_WCLK		29
+#define ZX297520V3_LSP_MPLL_D6_WCLK		30
+#define ZX297520V3_LSP_MPLL_D8_WCLK		31
+#define ZX297520V3_LSP_MPLL_D12_WCLK		32
+#define ZX297520V3_LSP_OSC26M_WCLK		33
+#define ZX297520V3_LSP_OSC32K_WCLK		34
+#define ZX297520V3_LSP_PCLK			35
+#define ZX297520V3_LSP_TDM_WCLK			36
+#define ZX297520V3_LSP_DPLL_D4_WCLK		37
+#define ZX297520V3_SRAM0_PCLK			38
+#define ZX297520V3_GSM_CFG_PCLK			39
+
 #endif /* __DT_BINDINGS_CLOCK_ZX297520V3_H */
diff --git a/include/dt-bindings/reset/zte,zx297520v3-reset.h b/include/dt-bindings/reset/zte,zx297520v3-reset.h
index f60aaccf0f4d..d79fa601afa6 100644
--- a/include/dt-bindings/reset/zte,zx297520v3-reset.h
+++ b/include/dt-bindings/reset/zte,zx297520v3-reset.h
@@ -27,4 +27,16 @@
 #define ZX297520V3_USB_RESET			18
 #define ZX297520V3_HSIC_RESET			19
 
+#define ZX297520V3_CPU_RESET			0
+#define ZX297520V3_DDR_CTRL_RESET		1
+#define ZX297520V3_EDCP_RESET			2
+#define ZX297520V3_SD0_RESET			3
+#define ZX297520V3_SD1_RESET			4
+#define ZX297520V3_NAND_RESET			5
+#define ZX297520V3_PDCFG_RESET			6
+#define ZX297520V3_SSC_RESET			7
+#define ZX297520V3_GMAC_RESET			8
+#define ZX297520V3_VOU_RESET			9
+#define ZX297520V3_LSP_RESET			10
+
 #endif /* __DT_BINDINGS_RESET_ZX297520V3_H */

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:35 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:35 +0300
Subject: [PATCH v9 03/12] dt-bindings: clk: zte: Add zx297520v3 LSP clock
 and reset controller
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-3-d05530d85d28@gmail.com>

The clock controller of the zx297520v3 Low Speed Peripherals is
relatively clean. One register per device with gates, muxes and resets
and for some devices a divider. There are even bits in the matrix
controller to control propagation of clock lines down to LSP.

The clocks are sorted by register address and I am convinced that the
device list is complete. There are, however, a few more registers that
are likely extra dividers for TDM and I2S devices.

Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski at oss.qualcomm.com>
Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>
---

Patch changelog:

v8:

*) Start clock indices at 0
*) A minor grammar fix in the second paragraph of the binding prose

v5:
Rename from lspclk to lspcrm
Remove matrixcrm from example

v4: Order properties compatible->reg->clocks->clock->names->#cells
---
 .../bindings/clock/zte,zx297520v3-lspcrm.yaml      | 101 +++++++++++++++++++++
 MAINTAINERS                                        |   1 +
 include/dt-bindings/clock/zte,zx297520v3-clk.h     |  37 ++++++++
 include/dt-bindings/reset/zte,zx297520v3-reset.h   |  19 ++++
 4 files changed, 158 insertions(+)

diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml
new file mode 100644
index 000000000000..cfad0e0372f4
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx297520v3-lspcrm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx297520v3 SoC LSP clock and reset controller
+
+maintainers:
+  - Stefan D?singer <stefandoesinger at gmail.com>
+
+description: |
+  This clock and reset controller controls low speed peripherals on the board.
+  This is a relatively isolated subsystem containing UART, I2C, I2S and SPI
+  devices. The controller is responsible for bringing the devices out of reset
+  and enabling their clocks as needed.
+
+  The controller receives its clock signals from the matrix controller, and
+  these signals need to be declared as clock inputs.
+
+  All available clocks are defined as preprocessor macros in the
+  "include/dt-bindings/clock/zte,zx297520v3-clk.h" header. Resets are defined in
+  the "include/dt-bindings/reset/zte,zx297520v3-reset.h" header.
+
+properties:
+  compatible:
+    const: zte,zx297520v3-lspcrm
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: Main PLL divided by 5 output from matrixcrm (124.8 MHz)
+      - description: Main PLL divided by 4 output from matrixcrm (156 MHz)
+      - description: Main PLL divided by 6 output from matrixcrm (104 MHz)
+      - description: Main PLL divided by 8 output from matrixcrm (78 MHz)
+      - description: Main PLL divided by 12 output from matrixcrm (52 MHz)
+      - description: Main oscillator output from matrixcrm (26 MHz)
+      - description: Timer oscillator output from matrixcrm (32 kHz)
+      - description: LSP pclk output from matrixcrm (26 MHz)
+      - description: TDM wclk mux output from matrixcrm
+      - description: DPLL divided by 4 output from matrixcrm (122.88 MHz)
+
+  clock-names:
+    items:
+      - const: mpll-d5
+      - const: mpll-d4
+      - const: mpll-d6
+      - const: mpll-d8
+      - const: mpll-d12
+      - const: osc26m
+      - const: osc32k
+      - const: pclk
+      - const: tdm-wclk
+      - const: dpll-d4
+
+  resets:
+    maxItems: 1
+
+  "#clock-cells":
+    const: 1
+
+  "#reset-cells":
+    const: 1
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - clock-names
+  - resets
+  - "#clock-cells"
+  - "#reset-cells"
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/zte,zx297520v3-clk.h>
+    #include <dt-bindings/reset/zte,zx297520v3-reset.h>
+
+    clock-controller at 1400000 {
+        compatible = "zte,zx297520v3-lspcrm";
+        reg = <0x01400000 0x100>;
+        clocks = <&matrixcrm ZX297520V3_LSP_MPLL_D5_WCLK>,
+                 <&matrixcrm ZX297520V3_LSP_MPLL_D4_WCLK>,
+                 <&matrixcrm ZX297520V3_LSP_MPLL_D6_WCLK>,
+                 <&matrixcrm ZX297520V3_LSP_MPLL_D8_WCLK>,
+                 <&matrixcrm ZX297520V3_LSP_MPLL_D12_WCLK>,
+                 <&matrixcrm ZX297520V3_LSP_OSC26M_WCLK>,
+                 <&matrixcrm ZX297520V3_LSP_OSC32K_WCLK>,
+                 <&matrixcrm ZX297520V3_LSP_PCLK>,
+                 <&matrixcrm ZX297520V3_LSP_TDM_WCLK>,
+                 <&matrixcrm ZX297520V3_LSP_DPLL_D4_WCLK>;
+        clock-names = "mpll-d5", "mpll-d4", "mpll-d6", "mpll-d8", "mpll-d12",
+                      "osc26m", "osc32k", "pclk", "tdm-wclk", "dpll-d4";
+        resets = <&matrixcrm ZX297520V3_LSP_RESET>;
+        #clock-cells = <1>;
+        #reset-cells = <1>;
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index 9a919e60254a..e01ef014c110 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3910,6 +3910,7 @@ L:	linux-arm-kernel at lists.infradead.org (moderated for non-subscribers)
 S:	Odd fixes
 F:	Documentation/arch/arm/zte/
 F:	Documentation/devicetree/bindings/arm/zte.yaml
+F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-lspcrm.yaml
 F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
 F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
 F:	arch/arm/boot/dts/zte/
diff --git a/include/dt-bindings/clock/zte,zx297520v3-clk.h b/include/dt-bindings/clock/zte,zx297520v3-clk.h
index a8477e9b2a14..8824740468a3 100644
--- a/include/dt-bindings/clock/zte,zx297520v3-clk.h
+++ b/include/dt-bindings/clock/zte,zx297520v3-clk.h
@@ -104,4 +104,41 @@
 #define ZX297520V3_SRAM0_PCLK			38
 #define ZX297520V3_GSM_CFG_PCLK			39
 
+#define ZX297520V3_TIMER_L1_WCLK		0
+#define ZX297520V3_TIMER_L1_PCLK		1
+#define ZX297520V3_WDT_L2_WCLK			2
+#define ZX297520V3_WDT_L2_PCLK			3
+#define ZX297520V3_WDT_L3_WCLK			4
+#define ZX297520V3_WDT_L3_PCLK			5
+#define ZX297520V3_PWM_WCLK			6
+#define ZX297520V3_PWM_PCLK			7
+#define ZX297520V3_I2S0_WCLK			8
+#define ZX297520V3_I2S0_PCLK			9
+#define ZX297520V3_I2S1_WCLK			10
+#define ZX297520V3_I2S1_PCLK			11
+#define ZX297520V3_QSPI_WCLK			12
+#define ZX297520V3_QSPI_PCLK			13
+#define ZX297520V3_UART1_WCLK			14
+#define ZX297520V3_UART1_PCLK			15
+#define ZX297520V3_I2C1_WCLK			16
+#define ZX297520V3_I2C1_PCLK			17
+#define ZX297520V3_SPI0_WCLK			18
+#define ZX297520V3_SPI0_PCLK			19
+#define ZX297520V3_TIMER_LB_WCLK		20
+#define ZX297520V3_TIMER_LB_PCLK		21
+#define ZX297520V3_TIMER_LC_WCLK		22
+#define ZX297520V3_TIMER_LC_PCLK		23
+#define ZX297520V3_UART2_WCLK			24
+#define ZX297520V3_UART2_PCLK			25
+#define ZX297520V3_WDT_LE_WCLK			26
+#define ZX297520V3_WDT_LE_PCLK			27
+#define ZX297520V3_TIMER_LF_WCLK		28
+#define ZX297520V3_TIMER_LF_PCLK		29
+#define ZX297520V3_SPI1_WCLK			30
+#define ZX297520V3_SPI1_PCLK			31
+#define ZX297520V3_TIMER_L11_WCLK		32
+#define ZX297520V3_TIMER_L11_PCLK		33
+#define ZX297520V3_TDM_WCLK			34
+#define ZX297520V3_TDM_PCLK			35
+
 #endif /* __DT_BINDINGS_CLOCK_ZX297520V3_H */
diff --git a/include/dt-bindings/reset/zte,zx297520v3-reset.h b/include/dt-bindings/reset/zte,zx297520v3-reset.h
index d79fa601afa6..303fbef12a42 100644
--- a/include/dt-bindings/reset/zte,zx297520v3-reset.h
+++ b/include/dt-bindings/reset/zte,zx297520v3-reset.h
@@ -39,4 +39,23 @@
 #define ZX297520V3_VOU_RESET			9
 #define ZX297520V3_LSP_RESET			10
 
+#define ZX297520V3_TIMER_L1_RESET		0
+#define ZX297520V3_WDT_L2_RESET			1
+#define ZX297520V3_WDT_L3_RESET			2
+#define ZX297520V3_PWM_RESET			3
+#define ZX297520V3_I2S0_RESET			4
+#define ZX297520V3_I2S1_RESET			5
+#define ZX297520V3_QSPI_RESET			6
+#define ZX297520V3_UART1_RESET			7
+#define ZX297520V3_I2C1_RESET			8
+#define ZX297520V3_SPI0_RESET			9
+#define ZX297520V3_TIMER_LB_RESET		10
+#define ZX297520V3_TIMER_LC_RESET		11
+#define ZX297520V3_UART2_RESET			12
+#define ZX297520V3_WDT_LE_RESET			13
+#define ZX297520V3_TIMER_LF_RESET		14
+#define ZX297520V3_SPI1_RESET			15
+#define ZX297520V3_TIMER_L11_RESET		16
+#define ZX297520V3_TDM_RESET			17
+
 #endif /* __DT_BINDINGS_RESET_ZX297520V3_H */

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:36 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:36 +0300
Subject: [PATCH v9 04/12] mfd: zx297520v3: Add a clock and reset MFD driver
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-4-d05530d85d28@gmail.com>

This driver registers child devices for the zx297520v3 clock and reset
controllers. The clk-zx297520v3 and reset-zte-zx297520v3 submitted in
the next patches will drive the respective functionalities.

Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

---

What I am still unsure about: How much sense does it make to have this
module-capable and able to unbind: In practice, the board doesn't work
without this driver and its clock and reset children.

Changes v9:
Deassert the LSP reset and enable the LSP pclk here. In practice the
boot rom needs to do that because it prints to an LSP-connected UART and
reads the boot policy from the LSP connected flash chip.

In doing so, I migrated the match data to an enum as mfd.md suggests.

Changes v8:
*) Remove .of_compatible from PHY mfd child

*) Move to drivers/mfd (Sashiko)

For me either soc/zte or mfd/ is fine. Note though that this MFD parent
is very specific to the zx297520v3 SoC. I don't expect this to be reused
anywhere else. There are two more MFD-ish devices in there: soc_sys at
0x140000, which I plan to handle in the same driver, and an i2c PMIC,
which would get its own host driver.

*) Use PLATFORM_DEVID_AUTO (Sashiko). NONE was intentional as I only
ever expect one instance, but I don't see any harm in doing the standard
thing and use AUTO

*) On the suggestion not to put the link to mfd_cells[] into match data:
I see that it is spelled out in Sashiko's mfd.md, written by Lee Jones,
the MFD maintainer. I would appreciate some education on the rationale
behind it: Putting a pointer into the void *data is a common pattern in
the kernel. I don't understand in which situation this can possibly
break?

Both structs are static const in the same compilation unit. data is a
const void *, not a uintptr_t or kernel_ulong_t, so it seems passing a
pointer is the intended use. What am I missing?

Changes v7:
Add phy MFD child

Changes v6: Make the ZTE SoC driver section depend on HAS_IOMEM
(Sashiko). The entire MFD section, which contains MFD_CORE, depends on
HAS_IOMEM even with COMPILE_TEST.

Add a NULL ptr check for of_device_get_match_data (Sashiko). While not
uniform, rave-sp, rohm-bd9576, atc260x, da9052-i2c protect against
incorrect manual attachment that way.

Add lspclk here as well in an attempt to satisfy both Conor Dooley, who
asks for MFD for top and matrix, and Philipp Zabel, who prefers aux but
at least wants the reset driver limited to one driver type.

Changes v5: Use MFD instead of Aux bus for top and matrix crm because of
extra functionality: Reboot in top, hwlock in Matrix.

LSP clocks stay with the aux bus and are thus not handled in this
driver. The clk driver will bind directly to the lspcrm node.
---
 MAINTAINERS                      |   1 +
 drivers/mfd/Kconfig              |  11 ++++
 drivers/mfd/Makefile             |   2 +
 drivers/mfd/zte-zx297520v3-crm.c | 117 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 131 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index e01ef014c110..b9f96455bca7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3915,6 +3915,7 @@ F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
 F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
 F:	arch/arm/boot/dts/zte/
 F:	arch/arm/mach-zte/
+F:	drivers/mfd/zte-zx297520v3-crm.c
 F:	include/dt-bindings/clock/zte,zx297520v3-clk.h
 F:	include/dt-bindings/phy/zte,zx297520v3-topcrm.h
 F:	include/dt-bindings/reset/zte,zx297520v3-reset.h
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 35f6e9b76d05..c0c7e83487f4 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -2572,5 +2572,16 @@ config MFD_MAX7360
 	  additional drivers must be enabled in order to use the functionality
 	  of the device.
 
+config MFD_ZTE_ZX297520V3_CRM
+	tristate "ZTE zx297520v3 Clock and Reset Manager"
+	select MFD_CORE
+	default SOC_ZX297520V3
+	help
+	  Say yes here to enable the driver for the ZTE zx297520v3 clock and
+	  reset manager MFD driver. This driver provides the host device for
+	  the clock and reset drivers and is required to boot the SoC. You
+	  will also need to enable CLK_ZTE_ZX297520V3 and RESET_ZTE_ZX297520V3
+	  to build the actual clock and reset child drivers.
+
 endmenu
 endif
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index dd4bb7e77c33..4f6a78170ac5 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -303,3 +303,5 @@ obj-$(CONFIG_MFD_RSMU_SPI)	+= rsmu_spi.o rsmu_core.o
 obj-$(CONFIG_MFD_UPBOARD_FPGA)	+= upboard-fpga.o
 
 obj-$(CONFIG_MFD_LOONGSON_SE)	+= loongson-se.o
+
+obj-$(CONFIG_MFD_ZTE_ZX297520V3_CRM)	+= zte-zx297520v3-crm.o
diff --git a/drivers/mfd/zte-zx297520v3-crm.c b/drivers/mfd/zte-zx297520v3-crm.c
new file mode 100644
index 000000000000..37cd0c14680e
--- /dev/null
+++ b/drivers/mfd/zte-zx297520v3-crm.c
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan D?singer
+ */
+
+#include <linux/clk.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/mfd/core.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+
+enum zx297520v3_mfd_type {
+	ZX297520V3_MFD_INVALID = 0,
+	ZX297520V3_MFD_TOPCRM,
+	ZX297520V3_MFD_MATRIXCRM,
+	ZX297520V3_MFD_LSPCRM,
+};
+
+static const struct mfd_cell zx297520v3_topcrm_cells[] = {
+	{
+		.name = "zx297520v3-topclk",
+	},
+	{
+		.name = "zx297520v3-topreset",
+	},
+	{
+		.name = "reboot",
+		.of_compatible = "syscon-reboot",
+	},
+	{
+		.name = "zx297520v3-usb-phy",
+	},
+};
+
+static const struct mfd_cell zx297520v3_matrixcrm_cells[] = {
+	{
+		.name = "zx297520v3-matrixclk",
+	},
+	{
+		.name = "zx297520v3-matrixreset",
+	},
+	/* A set of hwlock controllers is found here as well, but no driver is implemented yet */
+};
+
+static const struct mfd_cell zx297520v3_lspcrm_cells[] = {
+	{
+		.name = "zx297520v3-lspclk",
+	},
+	{
+		.name = "zx297520v3-lspreset",
+	},
+};
+
+static int zx297520v3_crm_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	enum zx297520v3_mfd_type type;
+	const struct mfd_cell *cells;
+	struct reset_control *rst;
+	unsigned int num_cells;
+	struct clk *pclk;
+
+	type = (enum zx297520v3_mfd_type)of_device_get_match_data(dev);
+	switch (type) {
+	case ZX297520V3_MFD_TOPCRM:
+		cells = zx297520v3_topcrm_cells;
+		num_cells = ARRAY_SIZE(zx297520v3_topcrm_cells);
+		break;
+
+	case ZX297520V3_MFD_MATRIXCRM:
+		cells = zx297520v3_matrixcrm_cells;
+		num_cells = ARRAY_SIZE(zx297520v3_matrixcrm_cells);
+		break;
+
+	case ZX297520V3_MFD_LSPCRM:
+		cells = zx297520v3_lspcrm_cells;
+		num_cells = ARRAY_SIZE(zx297520v3_lspcrm_cells);
+
+		pclk = devm_clk_get_enabled(dev, "pclk");
+		if (IS_ERR(pclk))
+			return dev_err_probe(dev, PTR_ERR(pclk), "Could not get pclk\n");
+
+		rst = devm_reset_control_get_exclusive_deasserted(dev, NULL);
+		if (IS_ERR(rst))
+			return dev_err_probe(dev, PTR_ERR(rst), "Could not get reset\n");
+		break;
+
+	default:
+		return -ENODEV;
+	}
+
+	return devm_mfd_add_devices(dev, PLATFORM_DEVID_AUTO, cells, num_cells, NULL, 0, NULL);
+}
+
+static const struct of_device_id of_match_zx297520v3_crm[] = {
+	{ .compatible = "zte,zx297520v3-topcrm", .data = (void *)ZX297520V3_MFD_TOPCRM },
+	{ .compatible = "zte,zx297520v3-matrixcrm", .data = (void *)ZX297520V3_MFD_MATRIXCRM },
+	{ .compatible = "zte,zx297520v3-lspcrm", .data = (void *)ZX297520V3_MFD_LSPCRM },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, of_match_zx297520v3_crm);
+
+static struct platform_driver zx297520v3_crm = {
+	.probe = zx297520v3_crm_probe,
+	.driver = {
+		.name = "zx297520v3-crm",
+		.of_match_table = of_match_zx297520v3_crm,
+	},
+};
+module_platform_driver(zx297520v3_crm);
+
+MODULE_AUTHOR("Stefan D?singer <stefandoesinger at gmail.com>");
+MODULE_DESCRIPTION("ZTE zx297520v3 CRM MFD host driver");
+MODULE_LICENSE("GPL");

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:37 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:37 +0300
Subject: [PATCH v9 05/12] clk: zte: Add Clock registration infrastructure
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-5-d05530d85d28@gmail.com>

The next patches will implement the regmap clocks and PLL driver. The
actual hardware specific clock listing will live in a separate module.

Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

---

Version 9:
Rework parent matching and clock export control. Most of the changes
will be in the next patches. The visible change here is that the clock
array is unified with single clocks having a type rather than one array
per type.

Depend on HAS_IOMEM, similarly to Sashiko's flagged issue in the Reset
patch

ZX297520V3_CLK_NO_EXPORT is gone again.

The explicit clock input array and enabling of LSP's pclk is removed
from the clock driver. The MFD parent takes care of this instead. The
clock driver does some sanity checking on the static clock data though.

Version 8:
Use ZX297520V3_CLK_NO_EXPORT=(~0u) for unexported clocks. While using 0,
and starting clock indices at 1, is a common pattern in existing
drivers, it exposes a driver implementation detail in the hardware
binding interface.

If desired, I can change the special index to a separate field in the
structs.

Fix the return value if the ->init() callback fails. (Sashiko)

Version 7:
*) Add fixed dividers to handle PLL subdivisions
*) Never register PLLs directly as exported clocks - everything on this
SoC goes through a gate before it leaves a controller.

Version 6:
*) Remove auxdev now that LSP clocks also use MFD
*) Error codepath fixes pointed out by Sashiko.

Version 5:

*) Pass the static clk data instead of calling get_match_data to prepare
for operating as an MFD child.

*) Don't use devm_kzalloc to allocate the auxiliary_device
structure. I guess Sashiko is right, and that's what "Because once the
device is placed on the bus the parent driver can not tell what other
code may have a reference to this data" is trying to tell me.

*) Fix error check for device_node_to_regmap.
---
 MAINTAINERS                  |   1 +
 drivers/clk/Kconfig          |   1 +
 drivers/clk/Makefile         |   1 +
 drivers/clk/zte/Kconfig      |  17 ++++
 drivers/clk/zte/Makefile     |   5 ++
 drivers/clk/zte/clk-regmap.c |  32 ++++++++
 drivers/clk/zte/clk-zx.c     | 189 +++++++++++++++++++++++++++++++++++++++++++
 drivers/clk/zte/clk-zx.h     | 142 ++++++++++++++++++++++++++++++++
 drivers/clk/zte/pll-zx.c     |  18 +++++
 9 files changed, 406 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index b9f96455bca7..77fa86c671b8 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3915,6 +3915,7 @@ F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
 F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
 F:	arch/arm/boot/dts/zte/
 F:	arch/arm/mach-zte/
+F:	drivers/clk/zte/
 F:	drivers/mfd/zte-zx297520v3-crm.c
 F:	include/dt-bindings/clock/zte,zx297520v3-clk.h
 F:	include/dt-bindings/phy/zte,zx297520v3-topcrm.h
diff --git a/drivers/clk/Kconfig b/drivers/clk/Kconfig
index 1717ce75a907..6f0a863951ca 100644
--- a/drivers/clk/Kconfig
+++ b/drivers/clk/Kconfig
@@ -545,6 +545,7 @@ source "drivers/clk/uniphier/Kconfig"
 source "drivers/clk/visconti/Kconfig"
 source "drivers/clk/x86/Kconfig"
 source "drivers/clk/xilinx/Kconfig"
+source "drivers/clk/zte/Kconfig"
 source "drivers/clk/zynqmp/Kconfig"
 
 # Kunit test cases
diff --git a/drivers/clk/Makefile b/drivers/clk/Makefile
index 6899e32e14aa..651fe866ddad 100644
--- a/drivers/clk/Makefile
+++ b/drivers/clk/Makefile
@@ -167,5 +167,6 @@ ifeq ($(CONFIG_COMMON_CLK), y)
 obj-$(CONFIG_X86)			+= x86/
 endif
 obj-y					+= xilinx/
+obj-$(CONFIG_COMMON_CLK_ZTE)		+= zte/
 obj-$(CONFIG_ARCH_ZYNQ)			+= zynq/
 obj-$(CONFIG_COMMON_CLK_ZYNQMP)         += zynqmp/
diff --git a/drivers/clk/zte/Kconfig b/drivers/clk/zte/Kconfig
new file mode 100644
index 000000000000..152c2423b8f0
--- /dev/null
+++ b/drivers/clk/zte/Kconfig
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# ZTE Clock Drivers
+#
+
+config COMMON_CLK_ZTE
+	tristate "Clock driver for ZTE SoCs"
+	depends on HAS_IOMEM
+	depends on ARCH_ZTE || COMPILE_TEST
+	default ARCH_ZTE
+	select MFD_SYSCON
+	help
+	  This option selects common clock infrastructure for ZTE based SoCs.
+	  You will need to enable one or more SoC specific drivers to make use
+	  of this.
+
+	  Enable this if you are building a kernel for a ZTE designed board.
diff --git a/drivers/clk/zte/Makefile b/drivers/clk/zte/Makefile
new file mode 100644
index 000000000000..27db07293165
--- /dev/null
+++ b/drivers/clk/zte/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+obj-$(CONFIG_COMMON_CLK_ZTE) += clk-zte.o
+
+clk-zte-y += clk-zx.o pll-zx.o clk-regmap.o
diff --git a/drivers/clk/zte/clk-regmap.c b/drivers/clk/zte/clk-regmap.c
new file mode 100644
index 000000000000..80332dba97f6
--- /dev/null
+++ b/drivers/clk/zte/clk-regmap.c
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026 Stefan D?singer
+ */
+
+#include <linux/clk-provider.h>
+#include <linux/device.h>
+#include <linux/errno.h>
+#include <linux/regmap.h>
+
+#include "clk-zx.h"
+
+struct clk_hw *zx_clk_register_gate(struct device *dev, struct regmap *regmap,
+				    const struct zx_gate_desc *desc,
+				    struct clk_hw * const *clocks)
+{
+	return ERR_PTR(-ENODEV);
+}
+
+struct clk_hw *zx_clk_register_divider(struct device *dev, struct regmap *regmap,
+				       const struct zx_div_desc *desc,
+				       struct clk_hw * const *clocks)
+{
+	return ERR_PTR(-ENODEV);
+}
+
+struct clk_hw *zx_clk_register_mux(struct device *dev, struct regmap *regmap,
+				   const struct zx_mux_desc *desc,
+				   struct clk_hw * const *clocks)
+{
+	return ERR_PTR(-ENODEV);
+}
diff --git a/drivers/clk/zte/clk-zx.c b/drivers/clk/zte/clk-zx.c
new file mode 100644
index 000000000000..27bbafd8ce4d
--- /dev/null
+++ b/drivers/clk/zte/clk-zx.c
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan D?singer
+ */
+
+#include <linux/clk-provider.h>
+#include <linux/clk.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+
+#include "clk-zx.h"
+
+static struct clk_hw *zx_clk_register_fixed_div(struct device *dev,
+						const struct zx_fixed_divider_desc *desc,
+						struct clk_hw * const *clocks)
+{
+	const struct zx_parent_desc *p = &desc->parent;
+
+	switch (p->type) {
+	case ZX_PARENT_FW:
+		return devm_clk_hw_register_fixed_factor_fwname(dev, NULL, desc->name, p->fw_name,
+								CLK_SET_RATE_PARENT, 1, desc->div);
+
+	case ZX_PARENT_ID:
+		return devm_clk_hw_register_fixed_factor_parent_hw(dev, desc->name, clocks[p->id],
+								   CLK_SET_RATE_PARENT, 1,
+								   desc->div);
+	}
+	WARN_ON_ONCE(1);
+	return ERR_PTR(-EINVAL);
+}
+
+static int zx_clk_validate(struct device *dev, struct device_node *of_node,
+			   const struct zx_clk_data *data)
+{
+	const struct zx_parent_desc *parents;
+	unsigned int i, p, num_parents;
+	struct clk *clk;
+
+	/*
+	 * Sanity check: Make sure all parents are there and write a clear message rather than
+	 * leave potential orphans.
+	 */
+	for (i = 0; i < data->num_clocks; ++i) {
+		switch (data->clocks[i].type) {
+		case ZX_CLOCK_PLL:
+			parents = data->clocks[i].pll.parents;
+			num_parents = data->clocks[i].pll.num_parents;
+			break;
+
+		case ZX_CLOCK_FIXED_DIV:
+			parents = &data->clocks[i].fixed_div.parent;
+			num_parents = 1;
+			break;
+
+		case ZX_CLOCK_MUX:
+			parents = data->clocks[i].mux.parents;
+			num_parents = data->clocks[i].mux.num_parents;
+			break;
+
+		case ZX_CLOCK_DIV:
+			parents = &data->clocks[i].div.parent;
+			num_parents = 1;
+			break;
+
+		case ZX_CLOCK_GATE:
+			parents = &data->clocks[i].gate.parent;
+			num_parents = 1;
+			break;
+
+		default:
+			return dev_err_probe(dev, -EINVAL, "Invalid clock entry %u\n", i);
+		}
+
+		for (p = 0; p < num_parents; ++p) {
+			switch (parents[p].type) {
+			case ZX_PARENT_FW:
+				clk = of_clk_get_by_name(of_node, parents[p].fw_name);
+				if (IS_ERR(clk))
+					return dev_err_probe(dev, PTR_ERR(clk),
+							     "Input clk %s failure\n",
+							     parents[p].fw_name);
+				clk_put(clk);
+				break;
+
+			case ZX_PARENT_ID:
+				if (parents[p].id >= i)
+					return dev_err_probe(dev, -EINVAL,
+							     "Clock %u has parent %u\n",
+							     i, parents[p].id);
+				break;
+
+			default:
+				return dev_err_probe(dev, -EINVAL,
+						     "Clock %u has unexpected parent of type %u\n",
+						     i, parents[p].type);
+			}
+		}
+	}
+
+	return 0;
+}
+
+int zx_clk_common_probe(struct device *dev, struct device_node *of_node,
+			const struct zx_clk_data *data)
+{
+	struct clk_hw_onecell_data *exports;
+	struct clk_hw **clocks;
+	struct regmap *map;
+	unsigned int i;
+	int res;
+
+	res = zx_clk_validate(dev, of_node, data);
+	if (res)
+		return res;
+
+	map = device_node_to_regmap(of_node);
+	if (IS_ERR(map))
+		return PTR_ERR(map);
+
+	clocks = devm_kcalloc(dev, data->num_clocks, sizeof(*clocks), GFP_KERNEL);
+	if (!clocks)
+		return -ENOMEM;
+
+	if (data->init) {
+		res = data->init(map);
+		if (res)
+			return res;
+	}
+
+	for (i = 0; i < data->num_clocks; ++i) {
+		struct clk_hw *hw;
+
+		switch (data->clocks[i].type) {
+		case ZX_CLOCK_PLL:
+			hw = zx_clk_register_pll(dev, map, &data->clocks[i].pll, clocks);
+			break;
+
+		case ZX_CLOCK_FIXED_DIV:
+			hw = zx_clk_register_fixed_div(dev, &data->clocks[i].fixed_div, clocks);
+			break;
+
+		case ZX_CLOCK_MUX:
+			hw = zx_clk_register_mux(dev, map, &data->clocks[i].mux, clocks);
+			break;
+
+		case ZX_CLOCK_DIV:
+			hw = zx_clk_register_divider(dev, map, &data->clocks[i].div, clocks);
+			break;
+
+		case ZX_CLOCK_GATE:
+			hw = zx_clk_register_gate(dev, map, &data->clocks[i].gate, clocks);
+			break;
+
+		default:
+			return -EINVAL;
+		}
+
+		if (IS_ERR(hw))
+			return dev_err_probe(dev, PTR_ERR(hw), "Failed to register clk %u\n", i);
+
+		clocks[i] = hw;
+	}
+
+	exports = devm_kzalloc(dev, struct_size(exports, hws, data->num_exports), GFP_KERNEL);
+	if (!exports)
+		return -ENOMEM;
+	exports->num = data->num_exports;
+
+	for (i = 0; i < data->num_exports; ++i) {
+		if (data->exports[i] >= data->num_clocks)
+			return dev_err_probe(dev, -EINVAL,
+					     "Export %u points to out of range clock %u\n",
+					     i, data->exports[i]);
+
+		exports->hws[i] = clocks[data->exports[i]];
+	}
+
+	devm_kfree(dev, clocks);
+
+	return devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, exports);
+}
+EXPORT_SYMBOL_NS_GPL(zx_clk_common_probe, "ZTE_CLK");
+
+MODULE_AUTHOR("Stefan D?singer <stefandoesinger at gmail.com>");
+MODULE_DESCRIPTION("ZTE common clock driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/clk/zte/clk-zx.h b/drivers/clk/zte/clk-zx.h
new file mode 100644
index 000000000000..175fbba3e0b3
--- /dev/null
+++ b/drivers/clk/zte/clk-zx.h
@@ -0,0 +1,142 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2026 Stefan D?singer
+ */
+
+#ifndef __DRV_CLK_ZX_H
+#define __DRV_CLK_ZX_H
+
+#include <linux/clk-provider.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+
+#define CLK_ZX_PLL_PREPARE_IS_ENABLE	1
+#define CLK_ZX_MAX_PARENTS		8
+
+enum zx_parent_type {
+	ZX_PARENT_FW,
+	ZX_PARENT_ID,
+};
+
+struct zx_parent_desc {
+	enum zx_parent_type		type;
+	union {
+		const char		*fw_name;
+		unsigned int		id;
+	};
+};
+
+#define PARENT_FW(_name) { .type = ZX_PARENT_FW, .fw_name = (_name) }
+
+#define PARENT_ID(_id) { .type = ZX_PARENT_ID, .id = (_id) }
+
+static inline struct clk_parent_data zx_get_parent(const struct zx_parent_desc *p,
+						   struct clk_hw * const *clocks)
+{
+	struct clk_parent_data ret = {};
+
+	switch (p->type) {
+	case ZX_PARENT_FW:
+		ret.fw_name = p->fw_name;
+		ret.index = -1;
+		return ret;
+
+	case ZX_PARENT_ID:
+		ret.hw = clocks[p->id];
+		ret.index = -1;
+		return ret;
+	};
+
+	WARN_ON_ONCE(1);
+	return ret;
+}
+
+struct zx_pll_desc {
+	const char			*name;
+	const struct zx_parent_desc	*parents;
+	unsigned int			num_parents;
+	unsigned long			rate;
+	u16				reg;
+	u16				flags;
+};
+
+struct zx_fixed_divider_desc {
+	const char			*name;
+	struct zx_parent_desc		parent;
+	unsigned int			div;
+};
+
+struct zx_mux_desc {
+	const char			*name;
+	const struct zx_parent_desc	*parents;
+	unsigned int			num_parents;
+	u16				reg;
+	u8				shift, size;
+};
+
+struct zx_div_desc {
+	const char			*name;
+	struct zx_parent_desc		parent;
+	u16				reg;
+	u8				shift, size;
+};
+
+struct zx_gate_desc {
+	const char			*name;
+	struct zx_parent_desc		parent;
+	unsigned long			flags;
+	u16				reg;
+	u8				shift;
+};
+
+enum zx_clock_type {
+	ZX_CLOCK_INVALID = 0,
+	ZX_CLOCK_PLL,
+	ZX_CLOCK_FIXED_DIV,
+	ZX_CLOCK_MUX,
+	ZX_CLOCK_DIV,
+	ZX_CLOCK_GATE,
+};
+
+struct zx_clock {
+	enum zx_clock_type type;
+	union {
+		struct zx_pll_desc pll;
+		struct zx_fixed_divider_desc fixed_div;
+		struct zx_mux_desc mux;
+		struct zx_div_desc div;
+		struct zx_gate_desc gate;
+	};
+};
+
+struct clk_hw *zx_clk_register_pll(struct device *dev, struct regmap *regmap,
+				   const struct zx_pll_desc *desc,
+				   struct clk_hw * const *clocks);
+struct clk_hw *zx_clk_register_mux(struct device *dev, struct regmap *regmap,
+				   const struct zx_mux_desc *desc,
+				   struct clk_hw * const *clocks);
+struct clk_hw *zx_clk_register_divider(struct device *dev, struct regmap *regmap,
+				       const struct zx_div_desc *desc,
+				       struct clk_hw * const *clocks);
+struct clk_hw *zx_clk_register_gate(struct device *dev, struct regmap *regmap,
+				    const struct zx_gate_desc *desc,
+				    struct clk_hw * const *clocks);
+
+struct zx_clk_export {
+	unsigned int priv;
+	unsigned int binding;
+};
+
+struct zx_clk_data {
+	int (*init)(struct regmap *map);
+	const struct zx_clock *clocks;
+	unsigned int num_clocks;
+	unsigned int *exports;
+	unsigned int num_exports;
+};
+
+int zx_clk_common_probe(struct device *dev, struct device_node *of_node,
+			const struct zx_clk_data *data);
+
+#endif /* __DRV_CLK_ZX_H */
diff --git a/drivers/clk/zte/pll-zx.c b/drivers/clk/zte/pll-zx.c
new file mode 100644
index 000000000000..fc76c6524a16
--- /dev/null
+++ b/drivers/clk/zte/pll-zx.c
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan D?singer
+ */
+
+#include <linux/clk-provider.h>
+#include <linux/device.h>
+#include <linux/errno.h>
+#include <linux/regmap.h>
+
+#include "clk-zx.h"
+
+struct clk_hw *zx_clk_register_pll(struct device *dev, struct regmap *regmap,
+				   const struct zx_pll_desc *desc,
+				   struct clk_hw * const *clocks)
+{
+	return ERR_PTR(-ENODEV);
+}

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:38 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:38 +0300
Subject: [PATCH v9 06/12] clk: zte: Add regmap-based clocks
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-6-d05530d85d28@gmail.com>

This is based on meson/clk-regmap.c, although slightly simplified. I
have kept the copyright lines at the top of the file to indicate its
origin.

I see that numerous clock drivers have their own incarnation of regmap-
based mux/div/gate clocks. If there is any version of it that is likely
to be elevated to shared code like clk-gate.c I'll copy that and try to
use it as unmodified as possible.

Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

---

Version 9:
Register single clocks per function invocation, remove
ZX297520V3_CLK_NO_EXPORT again

Version 8: Use ZX297520V3_CLK_NO_EXPORT instead of 0 for unexported clocks.

Version 6: Remove stray regmap (Sashiko)

Version 5: Use regmap_test_bits in zte_clk_regmap_gate_is_enabled
---
 drivers/clk/zte/clk-regmap.c | 219 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 212 insertions(+), 7 deletions(-)

diff --git a/drivers/clk/zte/clk-regmap.c b/drivers/clk/zte/clk-regmap.c
index 80332dba97f6..022b2be7bb3d 100644
--- a/drivers/clk/zte/clk-regmap.c
+++ b/drivers/clk/zte/clk-regmap.c
@@ -1,32 +1,237 @@
 // SPDX-License-Identifier: GPL-2.0
 /*
+ * Copyright (c) 2018 BayLibre, SAS.
  * Copyright (c) 2026 Stefan D?singer
  */
 
+#include <linux/bits.h>
 #include <linux/clk-provider.h>
+#include <linux/container_of.h>
 #include <linux/device.h>
+#include <linux/err.h>
 #include <linux/errno.h>
 #include <linux/regmap.h>
+#include <linux/types.h>
 
 #include "clk-zx.h"
 
+struct zte_clk_regmap {
+	struct clk_hw	hw;
+	struct regmap	*map;
+	u16		reg;
+	u8		shift;
+	u8		size;
+};
+
+static inline struct zte_clk_regmap *to_zte_clk_regmap(struct clk_hw *hw)
+{
+	return container_of(hw, struct zte_clk_regmap, hw);
+}
+
+static int zte_clk_regmap_gate_enable(struct clk_hw *hw)
+{
+	struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+	return regmap_set_bits(clk->map, clk->reg, BIT(clk->shift));
+}
+
+static void zte_clk_regmap_gate_disable(struct clk_hw *hw)
+{
+	struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+	regmap_clear_bits(clk->map, clk->reg, BIT(clk->shift));
+}
+
+static int zte_clk_regmap_gate_is_enabled(struct clk_hw *hw)
+{
+	struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+	return regmap_test_bits(clk->map, clk->reg, BIT(clk->shift));
+}
+
+static const struct clk_ops zte_clk_regmap_gate_ops = {
+	.enable		= zte_clk_regmap_gate_enable,
+	.disable	= zte_clk_regmap_gate_disable,
+	.is_enabled	= zte_clk_regmap_gate_is_enabled,
+};
+
 struct clk_hw *zx_clk_register_gate(struct device *dev, struct regmap *regmap,
-				    const struct zx_gate_desc *desc,
-				    struct clk_hw * const *clocks)
+				    const struct zx_gate_desc *desc, struct clk_hw * const *clocks)
+{
+	struct clk_parent_data parent = zx_get_parent(&desc->parent, clocks);
+	struct clk_init_data init = {};
+	struct zte_clk_regmap *clk;
+	int res;
+
+	clk = devm_kzalloc(dev, sizeof(*clk), GFP_KERNEL);
+	if (!clk)
+		return ERR_PTR(-ENOMEM);
+
+	init.name = desc->name;
+	init.ops = &zte_clk_regmap_gate_ops;
+	init.parent_data = &parent;
+	init.num_parents = 1;
+	init.flags = CLK_SET_RATE_PARENT | desc->flags;
+	clk->hw.init = &init;
+	clk->map = regmap;
+	clk->reg = desc->reg;
+	clk->shift = desc->shift;
+	clk->size = 1;
+
+	res = devm_clk_hw_register(dev, &clk->hw);
+	if (res)
+		return ERR_PTR(res);
+
+	return &clk->hw;
+}
+
+static unsigned long zte_clk_regmap_div_recalc_rate(struct clk_hw *hw,
+						    unsigned long prate)
+{
+	struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+	unsigned int val;
+	int ret;
+
+	ret = regmap_read(clk->map, clk->reg, &val);
+	if (ret)
+		return 0; /* Gives a hint that something is wrong */
+
+	val >>= clk->shift;
+	val &= clk_div_mask(clk->size);
+
+	return divider_recalc_rate(hw, prate, val, NULL, 0, clk->size);
+}
+
+static int zte_clk_regmap_div_determine_rate(struct clk_hw *hw,
+					     struct clk_rate_request *req)
+{
+	struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+
+	return divider_determine_rate(hw, req, NULL, clk->size, 0);
+}
+
+static int zte_clk_regmap_div_set_rate(struct clk_hw *hw, unsigned long rate,
+				       unsigned long parent_rate)
 {
-	return ERR_PTR(-ENODEV);
+	struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+	unsigned int val;
+	int ret;
+
+	ret = divider_get_val(rate, parent_rate, NULL, clk->size, 0);
+	if (ret < 0)
+		return ret;
+
+	val = (unsigned int)ret << clk->shift;
+
+	return regmap_update_bits(clk->map, clk->reg, clk_div_mask(clk->size) << clk->shift, val);
 }
 
+static const struct clk_ops zte_clk_regmap_divider_ops = {
+	.recalc_rate = zte_clk_regmap_div_recalc_rate,
+	.determine_rate = zte_clk_regmap_div_determine_rate,
+	.set_rate = zte_clk_regmap_div_set_rate,
+};
+
 struct clk_hw *zx_clk_register_divider(struct device *dev, struct regmap *regmap,
 				       const struct zx_div_desc *desc,
 				       struct clk_hw * const *clocks)
 {
-	return ERR_PTR(-ENODEV);
+	struct clk_parent_data parent = zx_get_parent(&desc->parent, clocks);
+	struct clk_init_data init = {};
+	struct zte_clk_regmap *clk;
+	int res;
+
+	clk = devm_kzalloc(dev, sizeof(*clk), GFP_KERNEL);
+	if (!clk)
+		return ERR_PTR(-ENOMEM);
+
+	init.name = desc->name;
+	init.ops = &zte_clk_regmap_divider_ops;
+	init.parent_data = &parent;
+	init.num_parents = 1;
+	init.flags = CLK_SET_RATE_PARENT;
+	clk->hw.init = &init;
+	clk->map = regmap;
+	clk->reg = desc->reg;
+	clk->shift = desc->shift;
+	clk->size = desc->size;
+
+	res = devm_clk_hw_register(dev, &clk->hw);
+	if (res)
+		return ERR_PTR(res);
+
+	return &clk->hw;
 }
 
+static u8 zte_clk_regmap_mux_get_parent(struct clk_hw *hw)
+{
+	struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+	unsigned int val;
+	int ret;
+
+	ret = regmap_read(clk->map, clk->reg, &val);
+	if (ret)
+		return 0xff;
+
+	val >>= clk->shift;
+	val &= GENMASK(clk->size - 1, 0);
+
+	return clk_mux_val_to_index(hw, NULL, 0, val);
+}
+
+static int zte_clk_regmap_mux_set_parent(struct clk_hw *hw, u8 index)
+{
+	struct zte_clk_regmap *clk = to_zte_clk_regmap(hw);
+	unsigned int val = clk_mux_index_to_val(NULL, 0, index);
+
+	return regmap_update_bits(clk->map, clk->reg,
+				  GENMASK(clk->size - 1, 0) << clk->shift,
+				  val << clk->shift);
+}
+
+static int zte_clk_regmap_mux_determine_rate(struct clk_hw *hw, struct clk_rate_request *req)
+{
+	return clk_mux_determine_rate_flags(hw, req, 0);
+}
+
+static const struct clk_ops zte_clk_regmap_mux_ops = {
+	.get_parent = zte_clk_regmap_mux_get_parent,
+	.set_parent = zte_clk_regmap_mux_set_parent,
+	.determine_rate = zte_clk_regmap_mux_determine_rate,
+};
+
 struct clk_hw *zx_clk_register_mux(struct device *dev, struct regmap *regmap,
-				   const struct zx_mux_desc *desc,
-				   struct clk_hw * const *clocks)
+			  const struct zx_mux_desc *desc, struct clk_hw * const *clocks)
 {
-	return ERR_PTR(-ENODEV);
+	struct clk_parent_data parents[CLK_ZX_MAX_PARENTS];
+	struct clk_init_data init = {};
+	struct zte_clk_regmap *clk;
+	unsigned int i;
+	int res;
+
+	if (WARN_ON(desc->num_parents > ARRAY_SIZE(parents)))
+		return ERR_PTR(-EINVAL);
+
+	for (i = 0; i < desc->num_parents; ++i)
+		parents[i] = zx_get_parent(&desc->parents[i], clocks);
+
+	clk = devm_kzalloc(dev, sizeof(*clk), GFP_KERNEL);
+	if (!clk)
+		return ERR_PTR(-ENOMEM);
+
+	init.name = desc->name;
+	init.ops = &zte_clk_regmap_mux_ops;
+	init.parent_data = parents;
+	init.num_parents = desc->num_parents;
+	clk->hw.init = &init;
+	clk->map = regmap;
+	clk->reg = desc->reg;
+	clk->shift = desc->shift;
+	clk->size = desc->size;
+
+	res = devm_clk_hw_register(dev, &clk->hw);
+	if (res)
+		return ERR_PTR(res);
+
+	return &clk->hw;
 }

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:39 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:39 +0300
Subject: [PATCH v9 07/12] clk: zte: Add zx PLL support infrastructure
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-7-d05530d85d28@gmail.com>

I am guessing how much of this is reusable among other zx chips or even
differently named ZTE platforms (if there are any). From reading the old
zx2967 code, I think the PLL code would be reusable there, maybe with
platform-specific bitmasks but otherwise the same logic.

Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

---

Version 9:
*) Take req->min_rate and req->max_rate into account when looking for
possible PLL configurations (sashiko). In practice the code will still
only ever encounter a fixed request to set dpll to 491.52 MHz.

*) The same code style changes Brian requested on the other clk patches.

Version 8:
*) Document the behavior of unlocked PLLs better: They don't pass
through their reference/parent, but pass through the fixed clock-26m
oscillator, even if their reference clock is something else.
*) dpll has working fractionals. Add this in the comment, but there is
no actual code support for it - the LTE hardware doesn't need it.

As for Sashiko's comments on the .set_rate implementation: In practice
.set_rate will only ever set one rate, 491.52 MHz for dpll. All other
PLLs are bootloader configured. Dpll could be handled by writing a magic
constant into its config.

I want to have the rate finding code as documentation, and maybe there
is more elaborate future use for it (e.g. more flexible underclocking),
but attempts to handle eventualities like rate searches or misconfigured
bootloader values would be dead code.

Version 7:
*) Always keep unknownpll enabled when prepared so dpll can acquire a
lock in its prepare() function.
*) Clean up error reporting a bit (Sashiko)

Version 6:
*) Use abs_diff to compare target and candidate PLL rate (Sashiko).
*) Use req->best_parent_rate in zx29_pll_determine_rate. Add a TODO
comment about the parent rate flexibility.

Version 5: Fix some issues pointed out by Sashiko: NULL dev,
zx29_pll_recalc_rate error handling, disable PLL again on enable error.
---
 drivers/clk/zte/pll-zx.c | 568 ++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 565 insertions(+), 3 deletions(-)

diff --git a/drivers/clk/zte/pll-zx.c b/drivers/clk/zte/pll-zx.c
index fc76c6524a16..e61b462ec0b4 100644
--- a/drivers/clk/zte/pll-zx.c
+++ b/drivers/clk/zte/pll-zx.c
@@ -4,15 +4,577 @@
  */
 
 #include <linux/clk-provider.h>
+#include <linux/clk.h>
+#include <linux/container_of.h>
 #include <linux/device.h>
+#include <linux/err.h>
 #include <linux/errno.h>
+#include <linux/math.h>
+#include <linux/rational.h>
 #include <linux/regmap.h>
+#include <linux/types.h>
+#include <linux/units.h>
 
 #include "clk-zx.h"
 
+/*
+ * This code has only been tested with zx297520v3 PLLs, but from reading the zx296718 clock code it
+ * looks like PLL registers are similar. ZTE's sources explain the PLL register contents only in a
+ * .cmm file (A Lauterbach TRACE32 script) and some unused headers in their U-Boot code dump, which
+ * may not be accurate. When calculating the frequencies from the default PLL configuration the
+ * results match the fixed rate clocks from their clock driver.
+ *
+ * The 26 MHz and 32 kHz clocks can be easily observed with the timers. The 104 MHz output can be
+ * observed through the UART. One 122.88 MHz clock can be observed through the TDM device. All
+ * others can only be indirectly inferred, e.g. by comparing CPU speed or SDIO transfer rate between
+ * the fixed 26 MHz oscillator and the provided PLL frequency.
+ *
+ * The formula to calculate the clock is ((ref / refdiv) * fbdiv) / postdiv1 / postdiv2. The masks
+ * are given below. There are a few control flags:
+ *
+ * Bit 31: Disables the PLL, but passes clock-26m through unmodified. Whether POSTDIV_OUT_DISABLE
+ *         still matters is different between PLLs.
+ * Bit 30: Returns if the PLL is locked
+ * Bit 29: Not named in ZTE's code, but can be set. There is no obvious impact. Lock times are
+ *         unchanged, so it doesn't influence or bypass lock detection. It doesn't raise any IRQs or
+ *         influence GPIOs.
+ * Bit 27: Given its name it likely disables the Delta-Sigma Modulator, if one exists at all. The
+ *         boot ROM sets it on every PLL. Unsetting it marginally decreases the time it takes to
+ *         lock to the reference clock (from ~400 us to ~300 us).
+ * Bit 24: Bypasses the VCO, but still applies refdiv and postdiv. Doesn't matter if PLL_DISABLE=1.
+ *
+ * NB: Some PLLs have an automatic bypass logic that forwards clock-26m (REGARDLESS of reference)
+ * when they don't have a lock, regardless of reason. This can be triggered by disabling the PLL,
+ * setting an out-of-spec VCO frequency or disabling the parent. This shouldn't matter in regular
+ * operation, but caused me some confusion when reverse engineering the clock tree. E.g. clock-26m->
+ * unknownpll(disabled) -> dpll(enabled) counterintuitively results in a 26 MHz output clock.
+ */
+
+#define ZX29_PLL_DISABLE			BIT(31)
+#define ZX29_PLL_LOCKED				BIT(30)
+#define ZX29_PLL_LOCK_FILTER			BIT(29)
+#define ZX29_PLL_DSM_DISABLE			BIT(27)
+#define ZX29_PLL_PARENT_MASK			GENMASK(26, 25)
+#define ZX29_PLL_PARENT_SHIFT			25
+#define ZX29_PLL_BYPASS				BIT(24)
+#define ZX29_PLL_REFDIV_MASK			GENMASK(23, 18)
+#define ZX29_PLL_REFDIV_SHIFT			18
+#define ZX29_PLL_FBDIV_MASK			GENMASK(17, 6)
+#define ZX29_PLL_FBDIV_SHIFT			6
+#define ZX29_PLL_POSTDIV1_MASK			GENMASK(5, 3)
+#define ZX29_PLL_POSTDIV1_SHIFT			3
+#define ZX29_PLL_POSTDIV2_MASK			GENMASK(2, 0)
+#define ZX29_PLL_POSTDIV2_SHIFT			0
+
+/*
+ * The second register has a 24 bit fractional value, which only matters when ZX29_PLL_DSM_DISABLE
+ * is not set, and only seems to matter for dpll. ZTE's firmware does not make use of the fractional
+ * and it is unimplemented in this driver. Experimental testing confirms that it has an impact on
+ * dpll.
+ *
+ * Bits 27:24 contain more flags:
+ *
+ * Bit 27: Setting ZX29_PLL_DACAP slows down the lock time and obviates the speed gained from
+ *         !DSM_DISABLE. No other effect observed.
+ *
+ * Bit 26: ZX29_PLL_4PHASE_OUT_DISABLE is set on some PLLs on boot but not on others. It is set on
+ *         boot on mpll and upll, but not gpll, dpll or unknownpll. I am not sure what it does
+ *         either. The SDIO devices break if they are fed from gpll with this flag set, but they
+ *         work OK if they are fed from mpll without this flag set.
+ *
+ * Bit 25: ZX29_PLL_POSTDIV_OUT_DISABLE seems to disable the PLL output entirely. Whether it is
+ *         bypassed by PLL_DISABLE differs between PLLs. gpll still produces an output clock if
+ *         PLL_DISABLE = 1 and POSTDIV_DISABLE = 1, but produces no output if PLL_DISABLE = 0 and
+ *         POSTDIV_DISABLE = 1. The dpll feeder ("unknownpll") at 0x100 produces no output clock if
+ *         both PLL_DISABLE and POSTDIV_DISABLE are set to 1.
+ *
+ * Bit 24: ZX29_PLL_VCO_OUT_DISABLE probably disables the output of the VCO clock without
+ *         post-VCO-dividers, but the raw VCO output is not a possible parent of any consumer clock,
+ *         so I could not confirm this. It does not disable the VCO entirely - that's what
+ *         PLL_DISABLE does.
+ *
+ * A spinlock should not be needed. PLLs don't share their registers with anything else and the
+ * global prepare mutex and enable spinlock should be enough. Beware of conflicts in reg2 between
+ * POSTDIV_OUT_DISABLE and the fractional value in case you find out how fractional dividers work
+ * and add support for them.
+ */
+#define ZX29_PLL_REG2_OFFSET			4
+#define ZX29_PLL_DACAP				BIT(27)
+#define ZX29_PLL_4PHASE_OUT_DISABLE		BIT(26)
+#define ZX29_PLL_POSTDIV_OUT_DISABLE		BIT(25)
+#define ZX29_PLL_VCO_OUT_DISABLE		BIT(24)
+#define ZX29_PLL_FRACT				GENMASK(23, 0)
+
+/*
+ * The VCO's frequency range is limited. The stock settings run the VCO between 960 and 1248 MHz.
+ * Ad-hoc testing with gpll suggests that at least this PLL remains stable down to about 7 MHz and
+ * up to 2 GHz and produces a clock that can be used by the SDIO controller. Attempting to run the
+ * mpll VCO at 624 MHz and setting postdiv1 = postdiv2 = 1 - which should result in the same output
+ * frequency - or running it at 1872 MHz with an effective post divider of 3 crashes the CPU. Most
+ * likely the PLLs become unstable outside their core range and the SDIO controller is much more
+ * forgiving than CPU and DRAM are.
+ */
+#define ZX29_PLL_VCO_MAX_FREQ			(1300 * HZ_PER_MHZ)
+#define ZX29_PLL_VCO_MIN_FREQ			(900 * HZ_PER_MHZ)
+
+struct zx29_clk_pll {
+	struct clk_hw	hw;
+	struct device	*dev;
+	struct regmap	*map;
+	u16		reg;
+};
+
+static inline struct zx29_clk_pll *to_zx29_clk_pll(struct clk_hw *hw)
+{
+	return container_of(hw, struct zx29_clk_pll, hw);
+}
+
+static int zx29_pll_is_prepared(struct clk_hw *hw)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+	int res;
+
+	res = regmap_test_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+	if (res < 0)
+		return res;
+
+	return !res;
+}
+
+static int zx29_pll_prepare(struct clk_hw *hw)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+	u32 val = 0;
+	int res;
+
+	res = regmap_clear_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+	if (res < 0)
+		return res;
+
+	/* Lock duration is usually between 300 us and 500 us */
+	res = regmap_read_poll_timeout(pll->map, pll->reg, val, val & ZX29_PLL_LOCKED, 50, 2000);
+	if (res) {
+		regmap_set_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+		dev_err(pll->dev, "%s: PLL prepare failed: %d. Config value 0x%08x\n",
+			clk_hw_get_name(&pll->hw), res, val);
+	}
+	return res;
+}
+
+static void zx29_pll_unprepare(struct clk_hw *hw)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+
+	regmap_set_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+}
+
+static int zx29_pll_is_enabled(struct clk_hw *hw)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+	int res;
+
+	res = regmap_test_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+			       ZX29_PLL_POSTDIV_OUT_DISABLE);
+	if (res < 0)
+		return res;
+
+	return !res;
+}
+
+static int zx29_pll_enable(struct clk_hw *hw)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+
+	return regmap_clear_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+				 ZX29_PLL_POSTDIV_OUT_DISABLE);
+}
+
+static void zx29_pll_disable(struct clk_hw *hw)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+
+	regmap_set_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+			ZX29_PLL_POSTDIV_OUT_DISABLE);
+}
+
+static unsigned long zx29_pll_get_rate(const struct zx29_clk_pll *pll, unsigned long parent_rate,
+				       u32 setting)
+{
+	unsigned long refdiv, fbdiv, postdiv1, postdiv2, freq;
+	const char *name = clk_hw_get_name(&pll->hw);
+	u64 vco;
+
+	refdiv = (setting & ZX29_PLL_REFDIV_MASK) >> ZX29_PLL_REFDIV_SHIFT;
+	fbdiv = (setting & ZX29_PLL_FBDIV_MASK) >> ZX29_PLL_FBDIV_SHIFT;
+	postdiv1 = (setting & ZX29_PLL_POSTDIV1_MASK) >> ZX29_PLL_POSTDIV1_SHIFT;
+	postdiv2 = (setting & ZX29_PLL_POSTDIV2_MASK) >> ZX29_PLL_POSTDIV2_SHIFT;
+	dev_dbg(pll->dev, "%s: reference clock %lu Hz, PLL setting 0x%08x\n",
+		name, parent_rate, setting);
+
+	if (!refdiv || !postdiv1 || !postdiv2) {
+		dev_err(pll->dev, "%s: divide by zero (%lu, %lu, %lu)\n", name, refdiv, postdiv1,
+			postdiv2);
+		return 0;
+	}
+
+	vco = div_u64((u64)parent_rate * fbdiv, refdiv);
+	freq = div_u64(div_u64(vco, postdiv1), postdiv2);
+	dev_dbg(pll->dev, "%s: refdiv %lu fbdiv %lu\n", name, refdiv, fbdiv);
+	dev_dbg(pll->dev, "%s: postdiv1 %lu postdiv2 %lu\n", name, postdiv1, postdiv2);
+
+	dev_dbg(pll->dev, "%s: %lu MHz\n", name, freq / HZ_PER_MHZ);
+
+	return freq;
+}
+
+static unsigned long zx29_pll_recalc_rate(struct clk_hw *hw, unsigned long parent_rate)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+	u32 val;
+	int res;
+
+	res = regmap_read(pll->map, pll->reg, &val);
+	if (res < 0) {
+		dev_err(pll->dev, "%s: Failed to read PLL settings\n", clk_hw_get_name(&pll->hw));
+		return 0;
+	}
+
+	return zx29_pll_get_rate(pll, parent_rate, val);
+}
+
+static u32 zx29_pll_calc_values(const struct zx29_clk_pll *pll, unsigned long parent_rate,
+				unsigned long rate, unsigned long min_rate, unsigned long max_rate)
+{
+	const unsigned int postdiv1_max = (1 << hweight32(ZX29_PLL_POSTDIV1_MASK)) - 1;
+	const unsigned int postdiv2_max = (1 << hweight32(ZX29_PLL_POSTDIV2_MASK)) - 1;
+	unsigned long fbdiv, refdiv, best_fbdiv = 0, best_refdiv = 0;
+	u32 postdiv1 = 0, postdiv2 = 0, i, j, setting;
+	const char *name = clk_hw_get_name(&pll->hw);
+	long best = LONG_MAX;
+
+	/*
+	 * This code produces the same VCO settings that the boot loader and stock firmware use for
+	 * the standard frequencies. It has seen only very little manual testing beyond that.
+	 *
+	 * The goal is to find a VCO setting that gets us as close as possible to the desired output
+	 * rate, while being within the VCO's operating limits and achievable with the input value
+	 * range. It is iterating over possible post-VCO divider values (1-7)*(1-7) to look for
+	 * valid VCO target frequencies and then looks for refdiv and fbdiv values to achieve the
+	 * VCO frequency from the reference frequency.
+	 */
+	for (j = 1; j <= postdiv2_max; j++) {
+		for (i = 1; i <= postdiv1_max; i++) {
+			u64 vco = (u64)rate * i * j;
+			unsigned long out;
+
+			if (vco > ZX29_PLL_VCO_MAX_FREQ || vco < ZX29_PLL_VCO_MIN_FREQ)
+				continue;
+
+			rational_best_approximation(vco, parent_rate,
+						    (1 << hweight32(ZX29_PLL_FBDIV_MASK)) - 1,
+						    (1 << hweight32(ZX29_PLL_REFDIV_MASK)) - 1,
+						    &fbdiv, &refdiv);
+			setting = fbdiv << ZX29_PLL_FBDIV_SHIFT;
+			setting |= refdiv << ZX29_PLL_REFDIV_SHIFT;
+			setting |= i << ZX29_PLL_POSTDIV1_SHIFT;
+			setting |= j << ZX29_PLL_POSTDIV2_SHIFT;
+			out = zx29_pll_get_rate(pll, parent_rate, setting);
+
+			if (out < min_rate || out > max_rate)
+				continue;
+
+			if (abs_diff(out, rate) > best)
+				continue;
+
+			if (abs_diff(out, rate) < best) {
+				postdiv1 = i;
+				postdiv2 = j;
+				best_fbdiv = fbdiv;
+				best_refdiv = refdiv;
+				best = abs_diff(out, rate);
+
+				if (!best)
+					goto search_done;
+			}
+		}
+	}
+search_done:
+
+	if (!postdiv1) {
+		dev_err(pll->dev, "Did not find a setting for %lu Hz, parent %lu Hz\n",
+			rate, parent_rate);
+		return 0;
+	}
+
+	dev_dbg(pll->dev, "%s: parent rate %lu\n", name, parent_rate);
+	dev_dbg(pll->dev, "%s: found VCO dividers %u and %u\n", name, postdiv1, postdiv2);
+	dev_dbg(pll->dev, "%s: VCO target rate %lu\n", name, rate * postdiv1 * postdiv2);
+
+	dev_dbg(pll->dev, "%s: Got fbdiv = %lu refdiv = %lu\n", name, best_fbdiv, best_refdiv);
+
+	setting = best_fbdiv << ZX29_PLL_FBDIV_SHIFT;
+	setting |= best_refdiv << ZX29_PLL_REFDIV_SHIFT;
+	setting |= postdiv1 << ZX29_PLL_POSTDIV1_SHIFT;
+	setting |= postdiv2 << ZX29_PLL_POSTDIV2_SHIFT;
+	dev_dbg(pll->dev, "%s: Final setting 0x%08x\n", name, setting);
+
+	return setting;
+}
+
+static int zx29_pll_determine_rate(struct clk_hw *hw, struct clk_rate_request *req)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+	unsigned long new_rate;
+	u32 setting;
+
+	/*
+	 * TODO: DPLL can switch between two parents, one of which is another PLL. Take this into
+	 * account when searching the config space and set req->best_parent_rate.
+	 *
+	 * In practice it shouldn't matter though. Dpll is always configured to a fixed frequency
+	 * and is the only clock with a switchable parent.
+	 */
+	if (!req->best_parent_rate) {
+		dev_err(pll->dev, "Did not expect best_parent_rate=0\n");
+		return -EINVAL;
+	}
+
+	setting = zx29_pll_calc_values(pll, req->best_parent_rate, req->rate, req->min_rate,
+				       req->max_rate);
+	if (!setting)
+		return -EINVAL;
+
+	new_rate = zx29_pll_get_rate(pll, req->best_parent_rate, setting);
+	if (new_rate != req->rate) {
+		dev_warn(pll->dev, "Did not find an exact match. Want %lu, got %lu\n",
+			 req->rate, new_rate);
+		req->rate = new_rate;
+	}
+
+	return 0;
+}
+
+static int zx29_pll_set_rate(struct clk_hw *hw, unsigned long rate,
+			     unsigned long parent_rate)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+	u32 setting;
+
+	/*
+	 * TODO: Implement gradual PLL rate change. PLLs can be changed while they are running and
+	 * downstream hardware is generally fine with that. The exception is DRAM, which reads
+	 * incorrect values if changed too fast.
+	 *
+	 * Changing the mpll rate is potentially useful for over/underclocking. Gating mpll is
+	 * unrealistic because too many devices depend on it.
+	 */
+	setting = zx29_pll_calc_values(pll, parent_rate, rate, rate, rate);
+	if (!setting)
+		return -EINVAL;
+
+	dev_info(pll->dev, "%s: Setting new configuration: 0x%08x\n", clk_hw_get_name(hw), setting);
+
+	return regmap_update_bits(pll->map, pll->reg, 0x00ffffff, setting);
+}
+
+static u8 zx29_pll_get_parent(struct clk_hw *hw)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+	u32 val;
+	int res;
+
+	res = regmap_read(pll->map, pll->reg, &val);
+	if (res < 0)
+		return 0xff;
+
+	val = (val & ZX29_PLL_PARENT_MASK) >> ZX29_PLL_PARENT_SHIFT;
+	dev_dbg(pll->dev, "%s: Parent 0x%x\n", clk_hw_get_name(hw), val);
+
+	return val;
+}
+
+static int zx29_pll_set_parent(struct clk_hw *hw, u8 index)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+	u32 idx_shift = index << ZX29_PLL_PARENT_SHIFT;
+	int res;
+	u32 val;
+
+	res = regmap_update_bits(pll->map, pll->reg, ZX29_PLL_PARENT_MASK, idx_shift);
+	if (res < 0)
+		return res;
+
+	res = regmap_read(pll->map, pll->reg, &val);
+	if (res < 0)
+		return res;
+
+	if ((val & ZX29_PLL_PARENT_MASK) != idx_shift) {
+		dev_err(pll->dev, "Hardware rejected PLL parent %u\n", index);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int zx29_pll_init(struct clk_hw *hw)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+	const char *name = clk_hw_get_name(hw);
+	int res;
+	u32 val;
+
+	dev_dbg(pll->dev, "%s: initializing\n", name);
+
+	/*
+	 * The fractional value is not yet implemented. It works at least with dpll, but I have
+	 * never seen it used on actual hardware.
+	 */
+	res = regmap_read(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET, &val);
+	if (res < 0)
+		return res;
+
+	if (val & ZX29_PLL_FRACT) {
+		dev_warn(pll->dev, "%s: unsupported nonzero fractional value 0x%08lx\n",
+			 name, val & ZX29_PLL_FRACT);
+	}
+
+	/*
+	 * Remove the bypass flag so we don't have to bother with it in enable/disable. I have never
+	 * seen it set by the earlier boot stages anyhow.
+	 */
+	res = regmap_clear_bits(pll->map, pll->reg, ZX29_PLL_BYPASS);
+	if (res < 0)
+		return res;
+
+	res = regmap_test_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+	if (res > 0) {
+		/*
+		 * Set ZX29_PLL_POSTDIV_OUT_DISABLE for PLLs that have ZX29_PLL_DISABLE for
+		 * consistency with .enable and .prepare. This ensures that .prepare doesn't
+		 * inadvertently enable PLLs without .enable being called.
+		 */
+		res = regmap_set_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+				      ZX29_PLL_POSTDIV_OUT_DISABLE);
+	}
+
+	return res;
+}
+
+static const struct clk_ops zx29_pll_ops = {
+	.init		= zx29_pll_init,
+	.is_prepared	= zx29_pll_is_prepared,
+	.prepare	= zx29_pll_prepare,
+	.unprepare	= zx29_pll_unprepare,
+	.is_enabled	= zx29_pll_is_enabled,
+	.enable		= zx29_pll_enable,
+	.disable	= zx29_pll_disable,
+	.recalc_rate	= zx29_pll_recalc_rate,
+	.determine_rate = zx29_pll_determine_rate,
+	.get_parent	= zx29_pll_get_parent,
+	.set_parent	= zx29_pll_set_parent,
+	.set_rate	= zx29_pll_set_rate,
+};
+
+/*
+ * A PLL that can be a parent of another PLL needs to produce an output signal when prepared,
+ * otherwise the downstream PLL's prepare() will fail to acquire a lock.
+ */
+static int zx29_pll_nodisable_init(struct clk_hw *hw)
+{
+	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
+	const char *name = clk_hw_get_name(hw);
+	int res;
+	u32 val;
+
+	dev_dbg(pll->dev, "%s: initializing, prepare-is-enabled\n", name);
+
+	res = regmap_read(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET, &val);
+	if (res < 0)
+		return res;
+
+	if (val & ZX29_PLL_FRACT) {
+		dev_warn(pll->dev, "%s: unsupported nonzero fractional value 0x%08lx\n",
+			 name, val & ZX29_PLL_FRACT);
+	}
+
+	res = regmap_clear_bits(pll->map, pll->reg, ZX29_PLL_BYPASS);
+	if (res < 0)
+		return res;
+
+	return regmap_clear_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
+				 ZX29_PLL_POSTDIV_OUT_DISABLE);
+}
+
+static const struct clk_ops zx29_pll_nodisable_ops = {
+	.init		= zx29_pll_nodisable_init,
+	.is_prepared	= zx29_pll_is_prepared,
+	.prepare	= zx29_pll_prepare,
+	.unprepare	= zx29_pll_unprepare,
+	.recalc_rate	= zx29_pll_recalc_rate,
+	.determine_rate = zx29_pll_determine_rate,
+	.get_parent	= zx29_pll_get_parent,
+	.set_parent	= zx29_pll_set_parent,
+	.set_rate	= zx29_pll_set_rate,
+};
+
 struct clk_hw *zx_clk_register_pll(struct device *dev, struct regmap *regmap,
-				   const struct zx_pll_desc *desc,
-				   struct clk_hw * const *clocks)
+				   const struct zx_pll_desc *desc, struct clk_hw * const *clocks)
 {
-	return ERR_PTR(-ENODEV);
+	struct clk_parent_data parents[CLK_ZX_MAX_PARENTS];
+	struct clk_init_data init = {};
+	struct zx29_clk_pll *pll;
+	unsigned int i;
+	int res;
+
+	pll = devm_kzalloc(dev, sizeof(*pll), GFP_KERNEL);
+	if (!pll)
+		return ERR_PTR(-ENOMEM);
+
+	if (desc->flags & CLK_ZX_PLL_PREPARE_IS_ENABLE)
+		init.ops = &zx29_pll_nodisable_ops;
+	else
+		init.ops = &zx29_pll_ops;
+
+	if (WARN_ON(desc->num_parents > ARRAY_SIZE(parents)))
+		return ERR_PTR(-EINVAL);
+
+	for (i = 0; i < desc->num_parents; ++i)
+		parents[i] = zx_get_parent(&desc->parents[i], clocks);
+
+	init.name = desc->name;
+	init.parent_data = parents;
+	init.num_parents = desc->num_parents;
+
+	/*
+	 * Disallow live reparent, but allow rate changes. PLLs can handle gradual changes fine, but
+	 * large ones cause a temporary incorrect output, which can break downstream devices.
+	 * See zx29_pll_set_rate for considerations on that.
+	 */
+	init.flags = CLK_SET_PARENT_GATE;
+	pll->hw.init = &init;
+	pll->dev = dev;
+	pll->map = regmap;
+	pll->reg = desc->reg;
+
+	res = devm_clk_hw_register(dev, &pll->hw);
+	if (res)
+		return ERR_PTR(res);
+
+	/*
+	 * Set the PLL rate only if the bootloader left it disabled. Keep the bootloader setup
+	 * otherwise.
+	 */
+	res = regmap_test_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
+	if (res < 0)
+		return ERR_PTR(res);
+
+	if (res > 0 && desc->rate) {
+		dev_dbg(pll->dev, "%s: setting to %lu Hz\n", desc->name, desc->rate);
+		res = clk_set_rate(pll->hw.clk, desc->rate);
+		if (res)
+			return ERR_PTR(dev_err_probe(dev, res, "%s: failed to set rate\n",
+						     desc->name));
+	}
+
+	return &pll->hw;
 }

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:40 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:40 +0300
Subject: [PATCH v9 08/12] clk: zte: Introduce a driver for zx297520v3 top
 clocks
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-8-d05530d85d28@gmail.com>

This register space controls core devices: PLLs, the AHB bus, a lot of
timers, the USB controller, the Cortex M0 processor that boots the board
and a few other devices. For some reason the LTE coprocessor is also
partially controlled by it. The main application processor and DDR
memory are not found here though.

The register to reboot the board is found here, as well as a register to
control whether watchdog expiries cause a board reset.

Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

---

Version 9:
Link internal parents by an (internal) clock ID rather than names.

There should be no difference in the built clock tree compared to
Version 8.

Version 8:
Rename the expected clock name of the fixed oscillators to clock-26m and
clock-32k. Sashiko pointed out that "oscXXm" is deprecated, although
widespread in existing drivers.

Add a define indirection for the AXI candidate=critical protection.
While it holds true in a system with running rprocs, I find it useful
for stress testing the clock tree to disable it.

Double-checked pclk and wclk bits, fixed incorrect assignments in
register 0x54.

Version 7:

The register at top+0x140 contains a series of gates that control PLL
input into the top controller and PLL and osc26m work clock forward into
matrix. This allowed further insight into the clock tree.

The PLL subdivision list was replaced with fixed factor clocks that
divide PLLs before and after the input gate (and some fixed factor divs
go into matrix in the next patch). The number of clocks exported to
Matrix is actually relatively small.

As for Sashiko's suggestion about out of bound parents for dpll and
unknownpll: I have never seen a parent != 0. If that happens on a device
I want to know about it. At this stage of development I'd rather have
clearly visible orphaned clock than silently continuing at an unexpected
frequency.

"osc26m" -> "clock-26m" as "oscXX", although widespread, is deprecated.
In doing so, change underscores in clock names to dashes (Sashiko).

Version 5:

*) Make it a MFD child driver instead of binding to the node directly
*) Correct parents for dpll
*) Correct HSIC work clock parent
---
 drivers/clk/zte/Kconfig          |  11 +
 drivers/clk/zte/Makefile         |   1 +
 drivers/clk/zte/clk-zx297520v3.c | 803 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 815 insertions(+)

diff --git a/drivers/clk/zte/Kconfig b/drivers/clk/zte/Kconfig
index 152c2423b8f0..0ccc6edf3c5b 100644
--- a/drivers/clk/zte/Kconfig
+++ b/drivers/clk/zte/Kconfig
@@ -15,3 +15,14 @@ config COMMON_CLK_ZTE
 	  of this.
 
 	  Enable this if you are building a kernel for a ZTE designed board.
+
+config CLK_ZTE_ZX297520V3
+	tristate "Clock driver for ZTE zx297520v3 based SoCs"
+	depends on COMMON_CLK_ZTE && (MFD_ZTE_ZX297520V3_CRM || COMPILE_TEST)
+	default SOC_ZX297520V3
+	help
+	  This driver supports ZTE zx297520v3 basic clocks.
+
+	  Enable this if you want to build a kernel that is able to run on
+	  boards based on this SoC. You can safely enable multiple clock
+	  drivers. The one(s) matching the device tree will be used.
diff --git a/drivers/clk/zte/Makefile b/drivers/clk/zte/Makefile
index 27db07293165..2c073512e919 100644
--- a/drivers/clk/zte/Makefile
+++ b/drivers/clk/zte/Makefile
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: GPL-2.0-only
 
 obj-$(CONFIG_COMMON_CLK_ZTE) += clk-zte.o
+obj-$(CONFIG_CLK_ZTE_ZX297520V3) += clk-zx297520v3.o
 
 clk-zte-y += clk-zx.o pll-zx.o clk-regmap.o
diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
new file mode 100644
index 000000000000..f9915582f261
--- /dev/null
+++ b/drivers/clk/zte/clk-zx297520v3.c
@@ -0,0 +1,803 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan D?singer
+ *
+ * There are no public datasheets for zx297520v3. The controls on this clock controller have been
+ * extracted from ZTE's kernel and U-Boot sources and a LOT of manual testing. Some clocks can be
+ * found in the power management (evb_cpurpm.img) and LTE (cpko.ko) blobs shipped with the routers.
+ *
+ * Because ZTE's sources are wrong or misleading in some parts and testing can be misinterpreted,
+ * this file contains a LOT of comments on how particular clocks were tested and how the impact of
+ * changes to them can be observed.
+ */
+
+#include <dt-bindings/clock/zte,zx297520v3-clk.h>
+#include <linux/errno.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+
+#include "clk-zx.h"
+
+MODULE_IMPORT_NS("ZTE_CLK");
+
+/*
+ * AXI speed is either determined by a fixed selection in matrix+0x0, bits 0:2 or by a voting system
+ * with ballots in matrix+0x120, 0x124, 0x128. The voting system is enabled by setting matrix 0x12c
+ * to 1. Otherwise the selection in 0x0 applies.
+ *
+ * The fastest selected speed wins. This holds true for values 0 (26 MHz) and 7 (6.5 MHz) too, so it
+ * isn't a case of lowest selector wins. I haven't tested what happens if mpll is changed to output
+ * 5x the normal clock, so 7 beats 0. Presumably value 0 would still win the election.
+ *
+ * AXI speed 6.5 is a child of matrix-osc26m, not a PLL. This can be seen by gating it off in
+ * topcrm+0x140.
+ *
+ * The system initializes with voting disabled and all preferences set to the lowest setting, 7.
+ * The zx297520v3_matrix_init function transfers the global mux to our vote and enables voting.
+ *
+ * Because rprocs may change their selection without notice, all possible parents of the AXI mux are
+ * marked critical. This can however obscure bugs in the clock tree. Define ZX297520V3_AXI_CANDIDATE
+ * to 0 to disable this protection for testing and either not run anything on the rprocs or make
+ * sure Linux votes for the fastest rate.
+ */
+#define ZX297520V3_AXI_CANDIDATE CLK_IS_CRITICAL
+
+/*
+ * Top and matrix clocks are chaotic - I haven't found a consistent pattern behind their register
+ * and bit locations. Generally there are two gates (pclk, wclk), one mux, two resets and sometimes
+ * one divider, but exceptions apply. For some devices there is only a reset and some general
+ * (parent) clocks need setup. This structure plus macro handles the somewhat regular parts.
+ *
+ * There are some patterns that can be observed.
+ * mux 0x3c, div 0x48, gate 0x54
+ * mux 0x40, div 0x4c, gate 0x5c
+ * mux 0x44, div 0x50, gate 0x60
+ *
+ * For a 0 - 0xc - 0x18 pattern. Muxes from 0x3c to 0x44, dividers from 0x48 to 0x50, gates 0x54 to
+ * 0x60. The pattern is broken for timer t17 though.
+ *
+ * Gates have 4 bits per clock - usually, but not always, bit 0 for wclk, bit 1 for pclk, bit 2 for
+ * something the ZTE kernel calls "gate" (the bits we use here are called "en"), which presumably
+ * enables automatic clock gating. Bit 3, if settable, seems unused. E.g. offset 0x54 accepts all
+ * bits in 0xF77F7F7F - suggesting RTC, I2C0 have an extra gate bit.
+ *
+ * The default mpll settings multiply the 26 MHz reference clock times 24. A mux selection of 26 MHz
+ * could mean using the 26 MHz oscillator directly, or passing it through the PLL and divide by 24.
+ *
+ * If a UART is set to mpll-d6 (default 104 MHz), changing the mpll multipliers does affect UART
+ * timing as it should. This does not happen when the UART is set to 26 MHz input or timers that
+ * read 26 MHz input. This suggests 26 MHz clocks use the reference clock directly.
+ *
+ * The general clock flow on this board goes from osc26m into the PLLs. The PLL output is
+ * branched into a small number of divided frequencies (mpll, mpll-d4, mpll-d5, mpll-d6, mpll-d8,
+ * gpll, gpll-d2, gpll-d4) at a high level. These divided frequencies as well as osc26m go
+ * through a series of gates at offset 0x140 in topcrm. The output of these gates provide wclks for
+ * everything.
+ *
+ * At least 3 of those gate outputs (mpll, dpll, gpll-d2) are further divided behind those gates,
+ * presumably in the matrix controller. There are multiple paths how 104 and 78 MHz are derived,
+ * some specific to topcrm (0x140 bits 1 and 4), some specific to matrix (0x140 bit 9). There are
+ * likely 3 "copies" of the 78 MHz clock and two 50 MHz ones.
+ *
+ * Pclk is not gated by the 0x140 gates at all. Likewise osc32k is fed to the timers without
+ * passing through this gate, even for consumers on matrix/LSP. Topcrm osc26m wclks are not gated
+ * either, but top-consumed PLL clocks are.
+ */
+
+enum top_clock_ids {
+	ZX_CLK_MPLL,
+	ZX_CLK_UPLL,
+	ZX_CLK_DPLL2,
+	ZX_CLK_DPLL2_D2,
+	ZX_CLK_DPLL,
+	ZX_CLK_GPLL,
+
+	ZX_CLK_OSC26M_D2,
+	ZX_CLK_TOP_MPLL_D2_PREGATE,
+	ZX_CLK_TOP_MPLL_D5_PREGATE,
+	ZX_CLK_TOP_MPLL_D6_PREGATE,
+	ZX_CLK_TOP_MPLL_D8_PREGATE,
+	ZX_CLK_TOP_UPLL_D12_PREGATE,
+	ZX_CLK_TOP_GPLL_D2_PREGATE,
+	ZX_CLK_TOP_GPLL_D4_PREGATE,
+
+	ZX_CLK_TOP_MPLL_D2,
+	ZX_CLK_TOP_MPLL_D8,
+	ZX_CLK_TOP_MPLL_D6,
+	ZX_CLK_TOMATRIX_MPLL_D5,
+	ZX_CLK_TOMATRIX_MPLL,
+	ZX_CLK_TOMATRIX_DPLL,
+	ZX_CLK_TOP_UPLL_D12,
+	ZX_CLK_TOP_UPLL,
+	ZX_CLK_TOP_GPLL_D4,
+	ZX_CLK_TOMATRIX_GPLL_D2,
+	ZX_CLK_TOMATRIX_GPLL,
+	ZX_CLK_TOMATRIX_OSC26M,
+
+	ZX_CLK_TOP_MPLL_D2_D4,
+	ZX_CLK_TOP_MPLL_D2_D13,
+	ZX_CLK_TOP_UPLL_D12_D2,
+	ZX_CLK_TOP_GPLL_D4_D2,
+
+	ZX_CLK_M0_MUX,
+	ZX_CLK_AHB_MUX,
+	ZX_CLK_TIMER_T08_MUX,
+	ZX_CLK_TIMER_T09_MUX,
+	ZX_CLK_TIMER_T12_MUX,
+	ZX_CLK_TIMER_T13_MUX,
+	ZX_CLK_TIMER_T14_MUX,
+	ZX_CLK_TIMER_T15_MUX,
+	ZX_CLK_TIMER_T16_MUX,
+	ZX_CLK_TIMER_T17_MUX,
+	ZX_CLK_WDT_T18_MUX,
+	ZX_CLK_I2C0_MUX,
+	ZX_CLK_UART0_MUX,
+	ZX_CLK_OUT0_MUX,
+	ZX_CLK_OUT1_MUX,
+	ZX_CLK_OUT2_MUX,
+	ZX_CLK_RMIIPHY_MUX,
+
+	ZX_CLK_TIMER_T08_DIV,
+	ZX_CLK_TIMER_T09_DIV,
+	ZX_CLK_TIMER_T12_DIV,
+	ZX_CLK_TIMER_T13_DIV,
+	ZX_CLK_TIMER_T14_DIV,
+	ZX_CLK_TIMER_T15_DIV,
+	ZX_CLK_TIMER_T16_DIV,
+	ZX_CLK_TIMER_T17_DIV,
+	ZX_CLK_WDT_T18_DIV,
+	ZX_CLK_USIM1_DIV,
+
+	ZX_CLK_AHB_PCLK,
+	ZX_CLK_AHB_WCLK,
+	ZX_CLK_SRAM1_PCLK,
+	ZX_CLK_SRAM2_PCLK,
+	ZX_CLK_PMM_WCLK,
+	ZX_CLK_PMM_PCLK,
+	ZX_CLK_TIMER_T08_WCLK,
+	ZX_CLK_TIMER_T08_PCLK,
+	ZX_CLK_TIMER_T09_WCLK,
+	ZX_CLK_TIMER_T09_PCLK,
+	ZX_CLK_TIMER_T12_WCLK,
+	ZX_CLK_TIMER_T12_PCLK,
+	ZX_CLK_TIMER_T13_WCLK,
+	ZX_CLK_TIMER_T13_PCLK,
+	ZX_CLK_TIMER_T14_WCLK,
+	ZX_CLK_TIMER_T14_PCLK,
+	ZX_CLK_TIMER_T15_WCLK,
+	ZX_CLK_TIMER_T15_PCLK,
+	ZX_CLK_TIMER_T16_WCLK,
+	ZX_CLK_TIMER_T16_PCLK,
+	ZX_CLK_TIMER_T17_WCLK,
+	ZX_CLK_TIMER_T17_PCLK,
+	ZX_CLK_WDT_T18_WCLK,
+	ZX_CLK_WDT_T18_PCLK,
+	ZX_CLK_I2C0_WCLK,
+	ZX_CLK_I2C0_PCLK,
+	ZX_CLK_UART0_WCLK,
+	ZX_CLK_UART0_PCLK,
+	ZX_CLK_USB_WCLK,
+	ZX_CLK_USB_PCLK,
+	ZX_CLK_HSIC_WCLK,
+	ZX_CLK_HSIC_PCLK,
+	ZX_CLK_RTC_WCLK,
+	ZX_CLK_RTC_PCLK,
+	ZX_CLK_USIM1_WCLK,
+	ZX_CLK_USIM1_PCLK,
+	ZX_CLK_LPM_GSM_WCLK,
+	ZX_CLK_LPM_GSM_PCLK,
+	ZX_CLK_LPM_LTE_WCLK,
+	ZX_CLK_LPM_LTE_PCLK,
+	ZX_CLK_LPM_TD_WCLK,
+	ZX_CLK_LPM_TD_PCLK,
+	ZX_CLK_LPM_W_WCLK,
+	ZX_CLK_LPM_W_PCLK,
+	ZX_CLK_OUT0_WCLK,
+	ZX_CLK_OUT1_WCLK,
+	ZX_CLK_OUT2_WCLK,
+	ZX_CLK_OUT32K_WCLK,
+	ZX_CLK_RMIIPHY_WCLK,
+};
+
+/* Used for gates where we don't know the parent input(s). Assume general oscillator */
+static const struct zx_parent_desc clk_unknown = PARENT_FW("osc26m");
+
+/* For clocks that are known to use the ungated osc26m input */
+static const struct zx_parent_desc clk_main[] = {
+	PARENT_FW("osc26m"),
+};
+
+static const struct zx_parent_desc dpll_parents[] = {
+	PARENT_ID(ZX_CLK_DPLL2_D2),
+	PARENT_FW("osc26m"),
+};
+
+/*
+ * AHB: The clock mux works and impact can be tested e.g. with iperf speed testing of the USB
+ * network connection. Values 2 and 3 give the same speed and depend on the same bit (3) in 0x140.
+ * The mpll-d6 rate is gated off by the same bit in 0x140 as m0's mpll-d6, but the d8 is different.
+ * This may indicate that one of the two uses a different clock rate than documented in ZTE's
+ * kernel. The M0 speed is easy to measure, so I am confident in the M0's 78 MHz rate. AHB looks
+ * like 78 rather than 52, but confidence is lower because RAM read tests are influenced by CPU
+ * overhead too.
+ */
+
+static const struct zx_parent_desc ahb_sel[] = {
+	PARENT_FW("osc26m"),
+	PARENT_ID(ZX_CLK_TOP_MPLL_D6),	/* 104 MHz */
+	PARENT_ID(ZX_CLK_TOP_MPLL_D8),	/* 78 MHz */
+	PARENT_ID(ZX_CLK_TOP_MPLL_D8),	/* 78 MHz */
+};
+
+static const struct zx_parent_desc timer_top_sel[] = {
+	PARENT_FW("osc32k"),
+	PARENT_FW("osc26m"),
+};
+
+static const struct zx_parent_desc uart_top_sel[] = {
+	PARENT_FW("osc26m"),
+	PARENT_ID(ZX_CLK_TOP_MPLL_D6),	/* 104 MHz, sharing M0's and ahb's 0x140 gate */
+};
+
+/*
+ * The Cortex M0 coprocessor. It is responsible for booting the board and runs some power management
+ * helper code on the stock firmware. The M0 rproc itself is not critical, but most of topcrm's
+ * registers become unreadable if this is gated off or become slow when clocked at 32 kHz, e.g. GPIO
+ * becomes borderline unusable at 32 kHz.
+ *
+ * There is no dedicated M0 gate. Disabling ahb-wclk looks like the M0 stops from A53's point of
+ * view, but it merely breaks the A53's ability to communicate with it, and M0's ability to
+ * communicate with the standard UART on LSP. M0 keeps running with both ahb-wclk and ahb-pclk
+ * gated off and can re-enable the gates.
+ */
+static const struct zx_parent_desc m0_sel[] = {
+	PARENT_FW("osc26m"),
+	PARENT_ID(ZX_CLK_TOP_MPLL_D6),		/* 104 MHz */
+	PARENT_ID(ZX_CLK_TOP_MPLL_D2_D4),	/* 78 MHz, but different gate than the ahb 78 MHz */
+	PARENT_FW("osc32k"),			/* Yes, tested. It is SLLLLOOOOOWWW. */
+};
+
+/*
+ * Clk-out0/1/2/32k: These clocks are exposed on GPIOs 15, 16, 17 and 18 respectively. They are used
+ * in ZTE's camera and sound code, by directly poking into the clock registers from the device
+ * drivers. Until the respective devices are supported they can safely be switched off.
+ *
+ * For clk-out1 ZTE's camera code says the following:
+ *
+ * 0 -> 20 MHz
+ * 1 -> 40 MHz
+ * 2 -> 13 MHz
+ * 3 -> 26 MHz
+ *
+ * 0 and 1 read from upll. I confirmed their rates (upll-d24 and upll-d12) by setting upll to a very
+ * low frequency and sampling the clock by GPIO reads. The outputs of 2 and 3 are way too high to
+ * test that way. Neither is reading from any PLL, they keep oscillating when all PLLs are disabled.
+ * I am fairly sure that 3 is just direct osc26m because it perfectly matches what out2 is
+ * showing in its 26 MHz setting.
+ *
+ * Clk-out2 is similar, but it has only one control bit in top 0x34 bit 8. Neither setting selects
+ * a PLL output. When setting *0x34 = 0x080, clk-out1 and clk-out2 are in lockstep, presumably
+ * running at 13 MHz. 0x1c0 also runs in lockstep, presumably 26 MHz.
+ *
+ * clk-out0 has a mux in bit 5. Value 0 most likely selects osc26m. I am not entirely sure about
+ * value 1 (which is the default), but the m0 mux has an impact on it. It looks like a debug pin
+ * that exposes some core clock.
+ */
+static const struct zx_parent_desc out0_sel[] = {
+	PARENT_FW("osc26m"),
+	PARENT_ID(ZX_CLK_M0_MUX),
+};
+
+static const struct zx_parent_desc out1_sel[] = {
+	PARENT_ID(ZX_CLK_TOP_UPLL_D12_D2),	/* 20 MHz */
+	PARENT_ID(ZX_CLK_TOP_UPLL_D12),		/* 40 MHz */
+	PARENT_ID(ZX_CLK_OSC26M_D2),		/* 13 MHz */
+	PARENT_FW("osc26m"),
+};
+
+static const struct zx_parent_desc out2_sel[] = {
+	PARENT_ID(ZX_CLK_OSC26M_D2),		/* 13 MHz */
+	PARENT_FW("osc26m"),
+};
+
+/*
+ * Clk-o is similar to clk-out*, providing a clock on GPIO 20, presumably for driving a (R)MII phy.
+ * The 50 MHz value is documented in a stray comment in ZTE's GMAC driver. I tested it similarly to
+ * the above pins. Mux setting 0 gives half the count as setting 1 and setting 1 gives gpll divided
+ * by 4 - matching the 50 MHz suggested by the ZTE comment at the default gpll settings. Gating gpll
+ * stops the clock for values 0 and 1.
+ *
+ * Settings 2 and 3 are possible, but seem to return garbage. It is generally pulsing up and down,
+ * except if both gpll and upll are stopped. I suspect it just reads random electrical fluctuation
+ * from other places in the board. Yes, I had a pull-down enabled when testing this.
+ *
+ * I am not aware of any board that uses this though. The Ethernet-equipped ones I have all run the
+ * PHY with its own oscillator.
+ */
+static const struct zx_parent_desc rmii_sel[] = {
+	PARENT_ID(ZX_CLK_TOP_GPLL_D4_D2),	/* 25 MHz */
+	PARENT_ID(ZX_CLK_TOP_GPLL_D4),		/* 50 MHz */
+};
+
+#define PLL(_name, _parents, _rate, _reg, _flags)		\
+	{							\
+		.type = ZX_CLOCK_PLL,				\
+		.pll = {					\
+			.name = _name,				\
+			.parents = _parents,			\
+			.num_parents = ARRAY_SIZE(_parents),	\
+			.rate = _rate,				\
+			.reg = _reg,				\
+			.flags = _flags,			\
+		},						\
+	}
+
+#define FIXED_DIV(_name, _parent, _div)				\
+	{							\
+		.type = ZX_CLOCK_FIXED_DIV,			\
+		.fixed_div = {					\
+			.name = _name,				\
+			.parent = _parent,			\
+			.div = _div,				\
+		},						\
+	}
+
+#define MUX(_name, _parents, _reg, _shift, _size)		\
+	{							\
+		.type = ZX_CLOCK_MUX,				\
+		.mux = {					\
+			.name = _name,				\
+			.parents = _parents,			\
+			.num_parents = ARRAY_SIZE(_parents),	\
+			.reg = _reg,				\
+			.shift = _shift,			\
+			.size = _size,				\
+		},						\
+	}
+
+#define DIV(_name, _parent, _reg, _shift, _size)		\
+	{							\
+		.type = ZX_CLOCK_DIV,				\
+		.div = {					\
+			.name = _name,				\
+			.parent = _parent,			\
+			.reg = _reg,				\
+			.shift = _shift,			\
+			.size = _size,				\
+		},						\
+	}
+
+#define GATE(_name, _parent, _reg, _shift, _flags)		\
+	{							\
+		.type = ZX_CLOCK_GATE,				\
+		.gate = {					\
+			.name = _name,				\
+			.parent = _parent,			\
+			.reg = _reg,				\
+			.flags = _flags,			\
+			.shift = _shift,			\
+		},						\
+	}
+
+static const struct zx_clock zx297520v3_top_clocks[] = {
+	/*
+	 * Default setting: 0x48040c11. 624/312/156. Only a single possible parent. This is the
+	 * PLL for pretty much everything, including CPU, RAM and USB.
+	 *
+	 * Changing this PLL makes it possible to overclock the CPU or do more fine grained
+	 * underclocking than the CPU's mux allows. It does run into two problems though: The USB
+	 * device uses this PLL's output directly and is *very* sensitive to differences. DRAM
+	 * is also fed by this clock and needs to be re-trained on larger changes, which needs to
+	 * be done by the stage 1 boot loader.
+	 */
+	[ZX_CLK_MPLL]              = PLL("mpll", clk_main, 0, 0x8, 0),
+
+	/*
+	 * ZTE's code calls this PLL "upll". The only possible consumers I found are clk-out1,
+	 * which outputs this clock on GPIO 16 and HSIC. HSIC doesn't have anything connected to
+	 * it on the devices I tested. The device that consumes clk-out1 seems to be an SPI
+	 * camera, which I haven't seen in any device so far. ZTE manipulates this PLL directly out
+	 * of the camera code, so it is unlikely to have another consumer.
+	 *
+	 * Long story short, shut it off.
+	 */
+	[ZX_CLK_UPLL]              = PLL("upll", clk_main, 0, 0x10, 0),
+
+	/*
+	 * Default value 0x4834902d. Feeds dpll. 46.08 MHz. Bit 25 can be set, so two parents are
+	 * possible. It looks like both values select the 26 MHz oscillator though.
+	 *
+	 * Since dpll's prepare may depend on this clock generating a usable signal, it needs to be
+	 * enabled when prepared.
+	 *
+	 * It isn't named anywhere in ZTE's code, but cpko.ko has the string "dpll2" somewhere in
+	 * its clock tables.
+	 */
+	[ZX_CLK_DPLL2]             = PLL("dpll2", clk_main, 0, 0x100, CLK_ZX_PLL_PREPARE_IS_ENABLE),
+	[ZX_CLK_DPLL2_D2]          = FIXED_DIV("dpll2-d2", PARENT_ID(ZX_CLK_DPLL2), 2),
+
+	/*
+	 * The documentation says 491.52 MHz and measurement with the LSP TDM device supports this.
+	 * The default value is 0x480c2011, but not all boot loaders set it up. To get to 491.52
+	 * with these settings it needs a 23.04 MHz reference clock, which matches dpll2-d2. If
+	 * dpll2 is disabled, dpll loses its lock. We set the frequency on this PLL if we find it is
+	 * not prepared by the boot loader.
+	 *
+	 * The proprietary LTE driver and/or rproc prepare and unprepare it. TDM and I2S can use it.
+	 *
+	 * It accepts parent values 0, 1, 2 and 3. Parent 0 is dpll2-d2. The others look like
+	 * osc26m. With a parent != 0 dpll never loses its lock even when all other PLLs are off
+	 * and the TDM counter register increases at a rate consistent with a 26.0/23.04 clock
+	 * increase.
+	 */
+	[ZX_CLK_DPLL]              = PLL("dpll", dpll_parents, 491520000, 0x18, 0),
+
+	/*
+	 * "g" is either for "general" or "gigahertz". The VCO runs at 1 GHz. Output clocks are 200,
+	 * 100, 50, 25, ... MHz. It is used optionally by SDIO and QSPI and can drive a GPIO clock
+	 * output for RMII, so it doesn't seem very general.
+	 */
+	[ZX_CLK_GPLL]              = PLL("gpll", clk_main, 0, 0x110, 0),
+
+	/* osc26m division for clk_outX */
+	[ZX_CLK_OSC26M_D2]         = FIXED_DIV("osc26m-d2", clk_main[0], 2),
+
+	/* Pre-0x140 distribution fixed divs */
+	[ZX_CLK_TOP_MPLL_D2_PREGATE]  = FIXED_DIV("mpll-d2", PARENT_ID(ZX_CLK_MPLL), 2),
+	[ZX_CLK_TOP_MPLL_D5_PREGATE]  = FIXED_DIV("mpll-d5", PARENT_ID(ZX_CLK_MPLL), 5),
+	[ZX_CLK_TOP_MPLL_D6_PREGATE]  = FIXED_DIV("mpll-d6", PARENT_ID(ZX_CLK_MPLL), 6),
+	[ZX_CLK_TOP_MPLL_D8_PREGATE]  = FIXED_DIV("mpll-d8", PARENT_ID(ZX_CLK_MPLL), 8),
+	[ZX_CLK_TOP_UPLL_D12_PREGATE] = FIXED_DIV("upll-d12", PARENT_ID(ZX_CLK_UPLL), 12),
+	[ZX_CLK_TOP_GPLL_D2_PREGATE]  = FIXED_DIV("gpll-d2", PARENT_ID(ZX_CLK_GPLL), 2),
+	[ZX_CLK_TOP_GPLL_D4_PREGATE]  = FIXED_DIV("gpll-d4", PARENT_ID(ZX_CLK_GPLL), 4),
+
+	/*
+	 * topcrm distribution gates. These are complicated, but shed a lot of light on the board's
+	 * clock distribution.
+	 *
+	 * Settable mask: 0x017333FF. Generally one nibble per PLL, except for MPLL that has more.
+	 * One for osc26m work clock. pclk and osc32k are not gated globally here or anywhere else
+	 * as far as I can see.
+	 *
+	 * If you want to test your understanding of this board, try to manually configure it into a
+	 * setup where bit 24 or bit 9 are off without crashing it.
+	 */
+
+	/* Bit 0: No known consumer */
+	/* Bit 1: M0's 78 MHz selection, but somehow also involved in USB too */
+	[ZX_CLK_TOP_MPLL_D2]       = GATE("top-mpll-d2", PARENT_ID(ZX_CLK_TOP_MPLL_D2_PREGATE),
+					  0x140, 1, 0),
+	/* Bit 2: No known consumer, named "mpll_48m_clk" in ZTE's cpko.ko blob */
+	[ZX_CLK_TOP_MPLL_D8]       = GATE("top-mpll-d8", PARENT_ID(ZX_CLK_TOP_MPLL_D8_PREGATE),
+					  0x140, 3, 0),
+	[ZX_CLK_TOP_MPLL_D6]       = GATE("top-mpll-d6", PARENT_ID(ZX_CLK_TOP_MPLL_D6_PREGATE),
+					  0x140, 4, 0),
+	/* Bit 5: No known consumer. "mpll_156m_clk" in cpko.ko */
+	[ZX_CLK_TOMATRIX_MPLL_D5]  = GATE("matrix-mpll-d5", PARENT_ID(ZX_CLK_TOP_MPLL_D5_PREGATE),
+					  0x140, 6, ZX297520V3_AXI_CANDIDATE),
+	/* Bit 7: No known consumer */
+	/*
+	 * Bit 8: Has a consumer, LTE depends on it. But unknown what and how. Most likely one of
+	 * the gsm clocks in topcrm+0x64 listed in cpko.ko.
+	 */
+	/*
+	 * Bit 9: Big MPLL provider for Matrix. It is an AXI voting candidate, but it is protected
+	 * in Matrix's 0x118 gates.
+	 */
+	[ZX_CLK_TOMATRIX_MPLL]     = GATE("matrix-mpll", PARENT_ID(ZX_CLK_MPLL), 0x140, 9, 0),
+	/* Bit 10: Always 0 */
+	/* Bit 11: Always 0 */
+
+	/* Bit 12: No known consumer. Existing patterns indicate a dpll consumer in top itself */
+	[ZX_CLK_TOMATRIX_DPLL]     = GATE("matrix-dpll", PARENT_ID(ZX_CLK_DPLL), 0x140, 13, 0),
+	/* Bit 14: Always 0 */
+	/* Bit 15: Always 0 */
+
+	[ZX_CLK_TOP_UPLL_D12]      = GATE("top-upll-d12", PARENT_ID(ZX_CLK_TOP_UPLL_D12_PREGATE),
+					  0x140, 16, 0),
+	[ZX_CLK_TOP_UPLL]          = GATE("top-upll", PARENT_ID(ZX_CLK_UPLL), 0x140, 17, 0),
+	/* Bit 18: Always 0 */
+	/* Bit 19: Always 0 */
+
+	[ZX_CLK_TOP_GPLL_D4]       = GATE("top-gpll-d4", PARENT_ID(ZX_CLK_TOP_GPLL_D4_PREGATE),
+					  0x140, 20, 0),
+	[ZX_CLK_TOMATRIX_GPLL_D2]  = GATE("matrix-gpll-d2", PARENT_ID(ZX_CLK_TOP_GPLL_D2_PREGATE),
+					  0x140, 21, 0),
+	[ZX_CLK_TOMATRIX_GPLL]     = GATE("matrix-gpll", PARENT_ID(ZX_CLK_GPLL), 0x140, 22, 0),
+	/* Bit 23: Always 0 */
+
+	/*
+	 * Bit 24 covers all 26 MHz on matrix, but not pclks. Osc26m consumers on topcrm do not
+	 * use this, they get the ungated osc26m. It is an AXI voting candidate, but does not
+	 * need protection because it is the slowest speed. It will only be used if every processor,
+	 * including us, votes for it.
+	 */
+	[ZX_CLK_TOMATRIX_OSC26M]   = GATE("matrix-osc26m", clk_main[0], 0x140, 24, 0),
+
+	/*
+	 * Post-140 divisions. They enter through the same bit and can be selected by different mux
+	 * values of their consumers or get piped to multiple consumers with different documented
+	 * rates - in particular, USB wclk (24 MHz according to ZTE) shares bit 1 with M0's 78 MHz.
+	 */
+	[ZX_CLK_TOP_MPLL_D2_D4]    = FIXED_DIV("top-mpll-d2-d4", PARENT_ID(ZX_CLK_TOP_MPLL_D2), 4),
+	[ZX_CLK_TOP_MPLL_D2_D13]   = FIXED_DIV("top-mpll-d2-d13", PARENT_ID(ZX_CLK_TOP_MPLL_D2),
+					       13),
+	[ZX_CLK_TOP_UPLL_D12_D2]   = FIXED_DIV("top-upll-d12-d2", PARENT_ID(ZX_CLK_TOP_UPLL_D12),
+					       2),
+	[ZX_CLK_TOP_GPLL_D4_D2]    = FIXED_DIV("top-gpll-d4-d2", PARENT_ID(ZX_CLK_TOP_GPLL_D4), 2),
+
+	/* Device-specific Muxes */
+	[ZX_CLK_M0_MUX]            = MUX("m0-mux",        m0_sel,            0x38,  0, 2),
+	[ZX_CLK_AHB_MUX]           = MUX("ahb-mux",       ahb_sel,           0x3c,  4, 2),
+	[ZX_CLK_TIMER_T08_MUX]     = MUX("timer-t08-mux", timer_top_sel,     0x40,  1, 1),
+	[ZX_CLK_TIMER_T09_MUX]     = MUX("timer-t09-mux", timer_top_sel,     0x40,  0, 1),
+	[ZX_CLK_TIMER_T12_MUX]     = MUX("timer-t12-mux", timer_top_sel,     0x3c,  0, 1),
+	[ZX_CLK_TIMER_T13_MUX]     = MUX("timer-t13-mux", timer_top_sel,     0x44,  0, 1),
+	[ZX_CLK_TIMER_T14_MUX]     = MUX("timer-t14-mux", timer_top_sel,     0x44,  1, 1),
+	[ZX_CLK_TIMER_T15_MUX]     = MUX("timer-t15-mux", timer_top_sel,     0x3c,  3, 1),
+	[ZX_CLK_TIMER_T16_MUX]     = MUX("timer-t16-mux", timer_top_sel,     0x44,  2, 1),
+	[ZX_CLK_TIMER_T17_MUX]     = MUX("timer-t17-mux", timer_top_sel,    0x120,  0, 1),
+	[ZX_CLK_WDT_T18_MUX]       = MUX("wdt-t18-mux",   timer_top_sel,     0x3c,  6, 1),
+	[ZX_CLK_I2C0_MUX]          = MUX("i2c0-mux",      uart_top_sel,      0x3c,  1, 1),
+	[ZX_CLK_UART0_MUX]         = MUX("uart0-mux",     uart_top_sel,      0x40,  2, 1),
+	[ZX_CLK_OUT0_MUX]          = MUX("out0-mux",      out0_sel,          0x34,  5, 1),
+	[ZX_CLK_OUT1_MUX]          = MUX("out1-mux",      out1_sel,          0x34,  6, 2),
+	[ZX_CLK_OUT2_MUX]          = MUX("out2-mux",      out2_sel,          0x34,  8, 1),
+	[ZX_CLK_RMIIPHY_MUX]       = MUX("rmiiphy-mux",   rmii_sel,         0x11c,  0, 2),
+
+	/* Device-specific dividers */
+	[ZX_CLK_TIMER_T08_DIV]     = DIV("timer-t08-div", PARENT_ID(ZX_CLK_TIMER_T08_MUX),
+					 0x4c, 8, 4),
+	[ZX_CLK_TIMER_T09_DIV]     = DIV("timer-t09-div", PARENT_ID(ZX_CLK_TIMER_T09_MUX),
+					 0x4c, 0, 4),
+	[ZX_CLK_TIMER_T12_DIV]     = DIV("timer-t12-div", PARENT_ID(ZX_CLK_TIMER_T12_MUX),
+					 0x48, 0, 4),
+	[ZX_CLK_TIMER_T13_DIV]     = DIV("timer-t13-div", PARENT_ID(ZX_CLK_TIMER_T13_MUX),
+					 0x50, 0, 4),
+	[ZX_CLK_TIMER_T14_DIV]     = DIV("timer-t14-div", PARENT_ID(ZX_CLK_TIMER_T14_MUX),
+					 0x50, 4, 4),
+	[ZX_CLK_TIMER_T15_DIV]     = DIV("timer-t15-div", PARENT_ID(ZX_CLK_TIMER_T15_MUX),
+					 0x48, 4, 4),
+	[ZX_CLK_TIMER_T16_DIV]     = DIV("timer-t16-div", PARENT_ID(ZX_CLK_TIMER_T16_MUX),
+					 0x50, 8, 4),
+	[ZX_CLK_TIMER_T17_DIV]     = DIV("timer-t17-div", PARENT_ID(ZX_CLK_TIMER_T17_MUX),
+					 0x124, 0, 4),
+	[ZX_CLK_WDT_T18_DIV]       = DIV("wdt-t18-div", PARENT_ID(ZX_CLK_WDT_T18_MUX),
+					 0x48, 8, 4),
+	[ZX_CLK_USIM1_DIV]         = DIV("usim1-div", clk_unknown,
+					 0x48, 12, 1),
+
+	/* Device specific gates */
+
+	/*
+	 * AHB clock gates: Disabling these cuts off the A53 from register access into 0x130000/
+	 * 0x140000, including the topcrm region, so it can't be re-enabled. Mark them critical for
+	 * that reason.
+	 *
+	 * Gating this off does not stop the M0 rproc, nor does it stop M0 from accessing topcrm. It
+	 * does however gate off M0 from the AXI interconnect, so it can't read RAM, Matrix, etc. So
+	 * these bits here merely gate the bus, not the actual devices.
+	 */
+	[ZX_CLK_AHB_PCLK]          = GATE("ahb-pclk", PARENT_ID(ZX_CLK_AHB_MUX), 0x54, 12,
+					  CLK_IS_CRITICAL),
+	[ZX_CLK_AHB_WCLK]          = GATE("ahb-wclk", PARENT_ID(ZX_CLK_M0_MUX), 0x54, 13,
+					  CLK_IS_CRITICAL),
+
+	/* SRAM1 and 2 clocks. Leave them on for now, as turning them off carelessly hangs the M0 */
+	[ZX_CLK_SRAM1_PCLK]        = GATE("sram1-pclk", clk_main[0], 0x54, 18, CLK_IS_CRITICAL),
+	[ZX_CLK_SRAM2_PCLK]        = GATE("sram2-pclk", clk_main[0], 0x54, 16, CLK_IS_CRITICAL),
+
+	/*
+	 * Pinmux (AON, TOP, IOCFG but not PDCFG). Critical as well until we have a driver that
+	 * consumes it. I don't think we'll realistically shut this off ever.
+	 *
+	 * Setting either bit 0 or 1 in register 0x58 makes the device work.
+	 */
+	[ZX_CLK_PMM_WCLK]          = GATE("pmm-wclk", clk_main[0], 0x58, 0, CLK_IS_CRITICAL),
+	[ZX_CLK_PMM_PCLK]          = GATE("pmm-pclk", clk_main[0], 0x58, 1, CLK_IS_CRITICAL),
+
+	/*
+	 * Timers. We don't use any of them, just shut them off. The timers are named and sorted
+	 * by the IO address of the main timer controls. Some of the controls are documented in
+	 * ZTE's kernel, the others I found by trial and error. Timers in register 0x54 have the
+	 * pclk first, the others have the wclk first.
+	 *
+	 * Timer T17 is used by the ZSP firmware. The rproc driver will enable it as needed.
+	 */
+	[ZX_CLK_TIMER_T08_WCLK]    = GATE("timer-t08-wclk", PARENT_ID(ZX_CLK_TIMER_T08_DIV),
+					  0x5c, 8, 0),
+	[ZX_CLK_TIMER_T08_PCLK]    = GATE("timer-t08-pclk", clk_main[0], 0x5c, 9, 0),
+	[ZX_CLK_TIMER_T09_WCLK]    = GATE("timer-t09-wclk", PARENT_ID(ZX_CLK_TIMER_T09_DIV),
+					  0x5c, 4, 0),
+	[ZX_CLK_TIMER_T09_PCLK]    = GATE("timer-t09-pclk", clk_main[0], 0x5c, 5, 0),
+	[ZX_CLK_TIMER_T12_WCLK]    = GATE("timer-t12-wclk", PARENT_ID(ZX_CLK_TIMER_T12_DIV),
+					  0x54, 5, 0),
+	[ZX_CLK_TIMER_T12_PCLK]    = GATE("timer-t12-pclk", clk_main[0], 0x54, 4, 0),
+	[ZX_CLK_TIMER_T13_WCLK]    = GATE("timer-t13-wclk", PARENT_ID(ZX_CLK_TIMER_T13_DIV),
+					  0x60, 0, 0),
+	[ZX_CLK_TIMER_T13_PCLK]    = GATE("timer-t13-pclk", clk_main[0], 0x60, 1, 0),
+	[ZX_CLK_TIMER_T14_WCLK]    = GATE("timer-t14-wclk", PARENT_ID(ZX_CLK_TIMER_T14_DIV),
+					  0x60, 4, 0),
+	[ZX_CLK_TIMER_T14_PCLK]    = GATE("timer-t14-pclk", clk_main[0], 0x60, 5, 0),
+	[ZX_CLK_TIMER_T15_WCLK]    = GATE("timer-t15-wclk", PARENT_ID(ZX_CLK_TIMER_T15_DIV),
+					  0x54, 21, 0),
+	[ZX_CLK_TIMER_T15_PCLK]    = GATE("timer-t15-pclk", clk_main[0], 0x54, 20, 0),
+	[ZX_CLK_TIMER_T16_WCLK]    = GATE("timer-t16-wclk", PARENT_ID(ZX_CLK_TIMER_T16_DIV),
+					  0x60, 8, 0),
+	[ZX_CLK_TIMER_T16_PCLK]    = GATE("timer-t16-pclk", clk_main[0], 0x60, 9, 0),
+	[ZX_CLK_TIMER_T17_WCLK]    = GATE("timer-t17-wclk", PARENT_ID(ZX_CLK_TIMER_T17_DIV),
+					  0x128, 0, 0),
+	[ZX_CLK_TIMER_T17_PCLK]    = GATE("timer-t17-pclk", clk_main[0], 0x128, 1, 0),
+
+	/*
+	 * This watchdog is set up by ZTE's bootloader and in ZTE's OS the m0 firmware will feed the
+	 * dog. The m0 firmware in turn wants to be fed in its own way. Since we normally don't run
+	 * any m0 firmware we shut it off by default and expose it to userspace via the watchdog
+	 * driver.
+	 */
+	[ZX_CLK_WDT_T18_WCLK]     = GATE("wdt-t18-wclk", PARENT_ID(ZX_CLK_WDT_T18_DIV),
+					 0x54, 25, 0),
+	[ZX_CLK_WDT_T18_PCLK]     = GATE("wdt-t18-pclk", clk_main[0], 0x54, 24, 0),
+
+	[ZX_CLK_I2C0_WCLK]        = GATE("i2c0-wclk", PARENT_ID(ZX_CLK_I2C0_MUX), 0x54, 9, 0),
+	[ZX_CLK_I2C0_PCLK]        = GATE("i2c0-pclk", clk_main[0], 0x54, 8, 0),
+	[ZX_CLK_UART0_WCLK]       = GATE("uart0-wclk", PARENT_ID(ZX_CLK_UART0_MUX), 0x5c, 12, 0),
+	[ZX_CLK_UART0_PCLK]       = GATE("uart0-pclk", clk_main[0], 0x5c, 13, 0),
+
+	/*
+	 * ZTE says the USB input is a 24 MHz clock based on mpll. Testing shows that upll is not
+	 * involved. The USB register space can be accessed with ahb-pclk gated, but not with
+	 * ahb-wclk gated. ZTE also lists ahb-wclk as parent for the second clock.
+	 *
+	 * There doesn't seem to be a separate PHY clock. usb-wclk stops controller registers from
+	 * updating, but doesn't cause the USB device to disconnect like asserting the PHY reset
+	 * does. Bit 0 in this register can't be set, so no PHY clock is hiding there either.
+	 */
+	[ZX_CLK_USB_WCLK]         = GATE("usb-wclk", PARENT_ID(ZX_CLK_TOP_MPLL_D2_D13), 0x6c, 3, 0),
+	[ZX_CLK_USB_PCLK]         = GATE("usb-pclk", PARENT_ID(ZX_CLK_AHB_WCLK), 0x6c, 4, 0),
+	/*
+	 * The HSIC hardware is listed in ZTE's code with a physical address of 0x01600000. Unlike
+	 * the USB controller, it does consume upll. ZTE says 240 MHz, but 480 is the usual one for
+	 * HSIC.
+	 */
+	[ZX_CLK_HSIC_WCLK]        = GATE("hsic-wclk", PARENT_ID(ZX_CLK_TOP_UPLL), 0x6c, 1, 0),
+	[ZX_CLK_HSIC_PCLK]        = GATE("hsic-pclk", PARENT_ID(ZX_CLK_AHB_WCLK), 0x6c, 2, 0),
+
+	/*
+	 * How does this RTC work? I don't know, the ZTE kernel does not talk to it. The actual RTC
+	 * is on the I2C-connected PMIC.
+	 */
+	[ZX_CLK_RTC_WCLK]         = GATE("rtc-wclk", clk_unknown, 0x54, 1, 0),
+	[ZX_CLK_RTC_PCLK]         = GATE("rtc-pclk", clk_main[0], 0x54, 0, 0),
+
+	[ZX_CLK_USIM1_WCLK]       = GATE("usim1-wclk", PARENT_ID(ZX_CLK_USIM1_DIV), 0x54, 29, 0),
+	[ZX_CLK_USIM1_PCLK]       = GATE("usim1-pclk", clk_main[0], 0x54, 28, 0),
+
+	/*
+	 * LTE: gate only as far as I can see. I looked for resets and did not find any. There may
+	 * be mux/div, but without understanding the behavior of this hardware it is impossible to
+	 * tell. They are sorted by physical MMIO address of the devices, which happens to be the
+	 * inverse order of the bits.
+	 *
+	 * I don't know what "LPM", "TD" and "W" mean. I copied them from ZTE's names.
+	 *
+	 * Like PMM in this register, either bit enables register access, whether they control pclk
+	 * or wclk cannot be distinguished.
+	 */
+	[ZX_CLK_LPM_GSM_WCLK]     = GATE("lpm-gsm-wclk", clk_unknown, 0x58, 10, 0),
+	[ZX_CLK_LPM_GSM_PCLK]     = GATE("lpm-gsm-pclk", clk_main[0], 0x58, 11, 0),
+	[ZX_CLK_LPM_LTE_WCLK]     = GATE("lpm-lte-wclk", clk_unknown, 0x58, 8, 0),
+	[ZX_CLK_LPM_LTE_PCLK]     = GATE("lpm-lte-pclk", clk_main[0], 0x58, 9, 0),
+	[ZX_CLK_LPM_TD_WCLK]      = GATE("lpm-td-wclk", clk_unknown, 0x58, 6, 0),
+	[ZX_CLK_LPM_TD_PCLK]      = GATE("lpm-td-pclk", clk_main[0], 0x58, 7, 0),
+	[ZX_CLK_LPM_W_WCLK]       = GATE("lpm-w-wclk", clk_unknown, 0x58, 4, 0),
+	[ZX_CLK_LPM_W_PCLK]       = GATE("lpm-w-pclk", clk_main[0], 0x58, 5, 0),
+
+	[ZX_CLK_OUT0_WCLK]        = GATE("out0-wclk", PARENT_ID(ZX_CLK_OUT0_MUX), 0x34, 0, 0),
+	[ZX_CLK_OUT1_WCLK]        = GATE("out1-wclk", PARENT_ID(ZX_CLK_OUT1_MUX), 0x90, 2, 0),
+	[ZX_CLK_OUT2_WCLK]        = GATE("out2-wclk", PARENT_ID(ZX_CLK_OUT2_MUX), 0x94, 2, 0),
+	[ZX_CLK_OUT32K_WCLK]      = GATE("out32k-wclk", PARENT_FW("osc32k"), 0x34, 1, 0),
+	[ZX_CLK_RMIIPHY_WCLK]     = GATE("rmiiphy-wclk", PARENT_ID(ZX_CLK_RMIIPHY_MUX),
+					 0x11c, 2, 0),
+};
+
+static unsigned int zx297520v3_top_exports[] = {
+	[ZX297520V3_M0_WCLK] =		ZX_CLK_M0_MUX,
+	[ZX297520V3_SRAM1_PCLK] =	ZX_CLK_SRAM1_PCLK,
+	[ZX297520V3_SRAM2_PCLK] =	ZX_CLK_SRAM2_PCLK,
+	[ZX297520V3_UART0_WCLK] =	ZX_CLK_UART0_WCLK,
+	[ZX297520V3_UART0_PCLK] =	ZX_CLK_UART0_PCLK,
+	[ZX297520V3_I2C0_WCLK] =	ZX_CLK_I2C0_WCLK,
+	[ZX297520V3_I2C0_PCLK] =	ZX_CLK_I2C0_PCLK,
+	[ZX297520V3_RTC_WCLK] =		ZX_CLK_RTC_WCLK,
+	[ZX297520V3_RTC_PCLK] =		ZX_CLK_RTC_PCLK,
+	[ZX297520V3_LPM_GSM_WCLK] =	ZX_CLK_LPM_GSM_WCLK,
+	[ZX297520V3_LPM_GSM_PCLK] =	ZX_CLK_LPM_GSM_PCLK,
+	[ZX297520V3_LPM_LTE_WCLK] =	ZX_CLK_LPM_LTE_WCLK,
+	[ZX297520V3_LPM_LTE_PCLK] =	ZX_CLK_LPM_LTE_PCLK,
+	[ZX297520V3_LPM_TD_WCLK] =	ZX_CLK_LPM_TD_WCLK,
+	[ZX297520V3_LPM_TD_PCLK] =	ZX_CLK_LPM_TD_PCLK,
+	[ZX297520V3_LPM_W_WCLK] =	ZX_CLK_LPM_W_WCLK,
+	[ZX297520V3_LPM_W_PCLK] =	ZX_CLK_LPM_W_PCLK,
+	[ZX297520V3_TIMER_T08_WCLK] =	ZX_CLK_TIMER_T08_WCLK,
+	[ZX297520V3_TIMER_T08_PCLK] =	ZX_CLK_TIMER_T08_PCLK,
+	[ZX297520V3_TIMER_T09_WCLK] =	ZX_CLK_TIMER_T09_WCLK,
+	[ZX297520V3_TIMER_T09_PCLK] =	ZX_CLK_TIMER_T09_PCLK,
+	[ZX297520V3_MPLL] =		ZX_CLK_TOMATRIX_MPLL,
+	[ZX297520V3_MPLL_D5] =		ZX_CLK_TOMATRIX_MPLL_D5,
+	[ZX297520V3_DPLL] =		ZX_CLK_TOMATRIX_DPLL,
+	[ZX297520V3_GPLL] =		ZX_CLK_TOMATRIX_GPLL,
+	[ZX297520V3_GPLL_D2] =		ZX_CLK_TOMATRIX_GPLL_D2,
+	[ZX297520V3_GATED_OSC26M] =	ZX_CLK_TOMATRIX_OSC26M,
+	[ZX297520V3_PMM_WCLK] =		ZX_CLK_PMM_WCLK,
+	[ZX297520V3_PMM_PCLK] =		ZX_CLK_PMM_PCLK,
+	[ZX297520V3_OUT0_WCLK] =	ZX_CLK_OUT0_WCLK,
+	[ZX297520V3_OUT1_WCLK] =	ZX_CLK_OUT1_WCLK,
+	[ZX297520V3_OUT2_WCLK] =	ZX_CLK_OUT2_WCLK,
+	[ZX297520V3_OUT32K_WCLK] =	ZX_CLK_OUT32K_WCLK,
+	[ZX297520V3_RMIIPHY_WCLK] =	ZX_CLK_RMIIPHY_WCLK,
+	[ZX297520V3_TIMER_T12_WCLK] =	ZX_CLK_TIMER_T12_WCLK,
+	[ZX297520V3_TIMER_T12_PCLK] =	ZX_CLK_TIMER_T12_PCLK,
+	[ZX297520V3_TIMER_T13_WCLK] =	ZX_CLK_TIMER_T13_WCLK,
+	[ZX297520V3_TIMER_T13_PCLK] =	ZX_CLK_TIMER_T13_PCLK,
+	[ZX297520V3_TIMER_T14_WCLK] =	ZX_CLK_TIMER_T14_WCLK,
+	[ZX297520V3_TIMER_T14_PCLK] =	ZX_CLK_TIMER_T14_PCLK,
+	[ZX297520V3_TIMER_T15_WCLK] =	ZX_CLK_TIMER_T15_WCLK,
+	[ZX297520V3_TIMER_T15_PCLK] =	ZX_CLK_TIMER_T15_PCLK,
+	[ZX297520V3_TIMER_T16_WCLK] =	ZX_CLK_TIMER_T16_WCLK,
+	[ZX297520V3_TIMER_T16_PCLK] =	ZX_CLK_TIMER_T16_PCLK,
+	[ZX297520V3_TIMER_T17_WCLK] =	ZX_CLK_TIMER_T17_WCLK,
+	[ZX297520V3_TIMER_T17_PCLK] =	ZX_CLK_TIMER_T17_PCLK,
+	[ZX297520V3_WDT_T18_WCLK] =	ZX_CLK_WDT_T18_WCLK,
+	[ZX297520V3_WDT_T18_PCLK] =	ZX_CLK_WDT_T18_PCLK,
+	[ZX297520V3_USIM1_WCLK] =	ZX_CLK_USIM1_WCLK,
+	[ZX297520V3_USIM1_PCLK] =	ZX_CLK_USIM1_PCLK,
+	[ZX297520V3_AHB_WCLK] =		ZX_CLK_AHB_WCLK,
+	[ZX297520V3_AHB_PCLK] =		ZX_CLK_AHB_PCLK,
+	[ZX297520V3_USB_WCLK] =		ZX_CLK_USB_WCLK,
+	[ZX297520V3_USB_PCLK] =		ZX_CLK_USB_PCLK,
+	[ZX297520V3_HSIC_WCLK] =	ZX_CLK_HSIC_WCLK,
+	[ZX297520V3_HSIC_PCLK] =	ZX_CLK_HSIC_PCLK,
+};
+
+static const struct zx_clk_data zx297520v3_topclk_data = {
+	.clocks = zx297520v3_top_clocks,
+	.num_clocks = ARRAY_SIZE(zx297520v3_top_clocks),
+	.exports = zx297520v3_top_exports,
+	.num_exports = ARRAY_SIZE(zx297520v3_top_exports),
+};
+
+static int clk_zx297520v3_probe(struct platform_device *pdev)
+{
+	const struct platform_device_id *id = platform_get_device_id(pdev);
+
+	if (!id)
+		return -ENODEV;
+
+	return zx_clk_common_probe(&pdev->dev, pdev->dev.parent->of_node,
+				   (const struct zx_clk_data *)id->driver_data);
+}
+
+static const struct platform_device_id clk_zx297520v3_ids[] = {
+	{
+		.name = "zx297520v3-topclk",
+		.driver_data = (kernel_ulong_t)&zx297520v3_topclk_data,
+	},
+	{ }
+};
+MODULE_DEVICE_TABLE(platform, clk_zx297520v3_ids);
+
+static struct platform_driver clk_zx297520v3 = {
+	.probe = clk_zx297520v3_probe,
+	.driver = {
+		.name = "clk-zx297520v3",
+	},
+	.id_table = clk_zx297520v3_ids,
+};
+module_platform_driver(clk_zx297520v3);
+
+MODULE_AUTHOR("Stefan D?singer <stefandoesinger at gmail.com>");
+MODULE_DESCRIPTION("ZTE zx297520v3 clock driver");
+MODULE_LICENSE("GPL");

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:41 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:41 +0300
Subject: [PATCH v9 09/12] clk: zte: Introduce a driver for zx297520v3
 matrix clocks
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-9-d05530d85d28@gmail.com>

This clock controller controls high speed devices: CPU, DMA, RAM, SDIO,
Ethernet. The only non-clock, non-reset registers I have spotted here
are hardware spinlocks.

Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

---

Version 9:
Link internal parents by an (internal) clock ID rather than names.

There are 3 intentional changes vs version 8 in the clock tree:
"gmac-ahb" depends on the ahb clock imported from topcrm. This was
already the case before, but it was missing from the bindings and the
global name matching obscured it.

When re-testing other devices for potential topcrm->ahb dependencies I
noticed that the two SDIO devices take their pclk from topcrm->ahb as
well.

@Sashiko: Any possible other clock tree change compared to version 8 is
unintentional and a likely bug.

Version 8:
Add another distribution gate register: 0x118 gates off matrix-generated
divisions of mpll, dpll and gpll. It nicely fits the top->matrix forward
picture.

AXI 6.5 MHz is not an mpll child. I must have done something wrong when
originally testing this

Add explanations about additional ARM architected timer controls.

Add gates for unknown LTE hardware at offset 0x60. They are not exported
in the bindings because the actual name of the consumer isn't clear -
but if dpll is unprepared and feeds a bypass 26 MHz signal, this device
starts spamming IRQs (although the 0x118 and top+0x140 gates also
prevent this usually)

Double-check pclk and wclk of all devices. The SDIO and EDCP clocks were
flipped in previous submissions.

Version 7:
Reduced the input clocks a lot. Most MPLL subdivisions are children of
the same gate in top, so put the fixed factor clocks here.

Added a few more clocks: DDR_CTRL, AXI bus, ARM architected timer.
They are needed to tell CCF to keep the top gates enabled and marked
critical because they aren't consumed by a DT node.

The AXI bus is determined by a voting system. I've set up the driver to
write to the ballot register ZTE's kernel uses and enable voting. This
should make life easier with the LTE rproc blob later.

Version 5:
Make it an MFD child device, remove aux device
Fix zx297529 -> zx297520 typos
Fix AHB_wclk -> ahb_wclk. That was a leftover of my old composite structs
Add SRAM0 and GSM_CFG clocks
---
 drivers/clk/zte/clk-zx297520v3.c | 483 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 483 insertions(+)

diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
index f9915582f261..6c4a7a1c3a9b 100644
--- a/drivers/clk/zte/clk-zx297520v3.c
+++ b/drivers/clk/zte/clk-zx297520v3.c
@@ -769,6 +769,485 @@ static const struct zx_clk_data zx297520v3_topclk_data = {
 	.num_exports = ARRAY_SIZE(zx297520v3_top_exports),
 };
 
+enum matrix_clock_ids {
+	ZX_CLK_MATRIX_OSC26M_D4,
+	ZX_CLK_MATRIX_MPLL_D2_PREGATE,
+	ZX_CLK_MATRIX_MPLL_D4_PREGATE,
+	ZX_CLK_MATRIX_MPLL_D6_PREGATE,
+	ZX_CLK_MATRIX_MPLL_D8_PREGATE,
+	ZX_CLK_MATRIX_MPLL_D12_PREGATE,
+	ZX_CLK_MATRIX_MPLL_D16_PREGATE,
+	ZX_CLK_MATRIX_GPLL_D2_D2_PREGATE,
+	ZX_CLK_MATRIX_GPLL_D2_D4_PREGATE,
+	ZX_CLK_MATRIX_DPLL_D6_PREGATE,
+	ZX_CLK_MATRIX_DPLL_D4_PREGATE,
+
+	ZX_CLK_MATRIX_MPLL_D16,
+	ZX_CLK_MATRIX_MPLL_D12,
+	ZX_CLK_MATRIX_MPLL_D8,
+	ZX_CLK_MATRIX_MPLL_D6,
+	ZX_CLK_MATRIX_MPLL_D4,
+	ZX_CLK_MATRIX_MPLL_D2,
+	ZX_CLK_MATRIX_DPLL_D6,
+	ZX_CLK_MATRIX_DPLL_D4,
+	ZX_CLK_MATRIX_GPLL_D2_D4,
+	ZX_CLK_MATRIX_GPLL_D2_D2,
+
+	ZX_CLK_CPU_MUX,
+	ZX_CLK_ZSP_MUX,
+	ZX_CLK_DDR_CTRL_MUX,
+	ZX_CLK_SD0_MUX,
+	ZX_CLK_SD1_MUX,
+	ZX_CLK_NAND_MUX,
+	ZX_CLK_EDCP_MUX,
+	ZX_CLK_TDM_MUX,
+	ZX_CLK_AXI_MUX,
+
+	ZX_CLK_SYS_TIMER_WCLK,
+	ZX_CLK_CPU_WCLK,
+	ZX_CLK_CPU_PCLK,
+	ZX_CLK_DDR_CTRL_PCLK,
+	ZX_CLK_DDR_CTRL_WCLK,
+	ZX_CLK_ZSP_WCLK,
+	ZX_CLK_SD0_PCLK,
+	ZX_CLK_SD0_WCLK,
+	ZX_CLK_SD0_CDET,
+	ZX_CLK_SD1_PCLK,
+	ZX_CLK_SD1_WCLK,
+	ZX_CLK_SD1_CDET,
+	ZX_CLK_NAND_WCLK,
+	ZX_CLK_NAND_PCLK,
+	ZX_CLK_LTE_TPU_WCLK,
+	ZX_CLK_LTE_TPU_PCLK,
+	ZX_CLK_EDCP_PCLK,
+	ZX_CLK_EDCP_WCLK,
+	ZX_CLK_SSC_WCLK,
+	ZX_CLK_SSC_PCLK,
+	ZX_CLK_PDCFG_WCLK,
+	ZX_CLK_PDCFG_PCLK,
+	ZX_CLK_MBOX_PCLK,
+	ZX_CLK_SRAM0_PCLK,
+	ZX_CLK_GSM_CFG_PCLK,
+	ZX_CLK_GMAC_WCLK,
+	ZX_CLK_GMAC_PCLK,
+	ZX_CLK_GMAC_AHB,
+	ZX_CLK_AXI_WCLK,
+	ZX_CLK_DMA_PCLK,
+	ZX_CLK_VOU_WCLK,
+	ZX_CLK_VOU_PCLK,
+	ZX_CLK_TOLSP_MPLL_D5_WCLK,
+	ZX_CLK_TOLSP_MPLL_D4_WCLK,
+	ZX_CLK_TOLSP_MPLL_D6_WCLK,
+	ZX_CLK_TOLSP_MPLL_D8_WCLK,
+	ZX_CLK_TOLSP_MPLL_D12_WCLK,
+	ZX_CLK_TOLSP_OSC26M_WCLK,
+	ZX_CLK_TOLSP_OSC32K_WCLK,
+	ZX_CLK_TOLSP_PCLK,
+	ZX_CLK_TOLSP_TDM_WCLK,
+	ZX_CLK_TOLSP_DPLL_D4_WCLK,
+};
+
+/*
+ * For devices which have a working driver the work clock can be figured out by gating off registers
+ * in topcrm+0x140. This is used for devices where I can't interpret the register contents yet.
+ */
+static const struct zx_parent_desc matrix_unk = PARENT_FW("wclk-osc26m");
+
+static const struct zx_parent_desc cpu_sel[] = {
+	PARENT_FW("wclk-osc26m"),
+	PARENT_FW("mpll"),			/* 624 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D2),	/* 312 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D4),	/* 156 MHz */
+};
+
+/* Low confidence on the actual value, but at least I know it is bit 13 in top+0x140 */
+static const struct zx_parent_desc zsp_sel[] = {
+	PARENT_FW("wclk-osc26m"),
+	PARENT_FW("dpll"),			/* 491.52 MHz */
+};
+
+/*
+ * We can't realistically change DDR speed while running an OS out of DDR (it reads garbage for a
+ * short while on transition), but we need to know if we have to keep gpll alive. 32 MB devices use
+ * 200 MHz, while 64/128 MB ones use 156 MHz.
+ */
+static const struct zx_parent_desc ddr_ctrl_sel[] = {
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D4),	/* 156 MHz */
+	PARENT_FW("gpll"),			/* 200 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D6),	/* 104 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D8),	/* 78 MHz */
+};
+
+static const struct zx_parent_desc sd0_sel[] = {
+	PARENT_FW("wclk-osc26m"),
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D4),	/* 156 MHz */
+	PARENT_FW("gpll-d2"),			/* 100 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D8),	/* 78 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_GPLL_D2_D2),	/* 50 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_GPLL_D2_D4),	/* 25 MHz */
+};
+
+static const struct zx_parent_desc sd1_sel[] = {
+	PARENT_FW("wclk-osc26m"),
+	PARENT_FW("gpll-d2"),			/* 100 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D8),	/* 78 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_GPLL_D2_D2),	/* 50 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D16),	/* 39 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_GPLL_D2_D4),	/* 25 MHz */
+};
+
+/*
+ * ZTE's kernel puts the high frequency first, but this is against the usual convention on this SoC.
+ * I don't have any device that has a raw NAND controller though.
+ */
+static const struct zx_parent_desc nand_sel[] = {
+	PARENT_FW("wclk-osc26m"),
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D4),	/* 156 MHz */
+};
+
+static const struct zx_parent_desc edcp_sel[] = {
+	PARENT_FW("wclk-osc26m"),
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D4),	/* 156 MHz */
+	PARENT_FW("mpll-d5"),			/* 124.8 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D6),	/* 104 MHz */
+};
+
+static const struct zx_parent_desc tdm_sel[] = {
+	PARENT_FW("wclk-osc26m"),
+	PARENT_ID(ZX_CLK_MATRIX_DPLL_D4),	/* 122.88 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D6),	/* 104 MHz */
+};
+
+/*
+ * ZTE's firmware uses 0x128 as the PHY's vote and names 0x120 as "PS" and 0x124 as "AP" vote.
+ * However, their cpufreq driver ultimately uses the "PS" define and thus writes to 0x120, which can
+ * be observed in the running system.
+ */
+#define ZX297520V3_AXI_AP_VOTE 0x120
+static const struct zx_parent_desc axi_sel[] = {
+	PARENT_FW("wclk-osc26m"),
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D4),	/* 156 MHz */
+	PARENT_FW("mpll-d5"),			/* 124.8 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D6),	/* 104 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D8),	/* 78 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D12),	/* 52 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_MPLL_D16),	/* 39 MHz */
+	PARENT_ID(ZX_CLK_MATRIX_OSC26M_D4),	/* 6.5 MHz */
+};
+
+static const struct zx_clock zx297520v3_matrix_clocks[] = {
+	[ZX_CLK_MATRIX_OSC26M_D4] = FIXED_DIV("matrix-osc26m-d4", PARENT_FW("wclk-osc26m"), 4),
+	[ZX_CLK_MATRIX_MPLL_D2_PREGATE] = FIXED_DIV("matrix-mpll-d2-pregate", PARENT_FW("mpll"), 2),
+	[ZX_CLK_MATRIX_MPLL_D4_PREGATE] = FIXED_DIV("matrix-mpll-d4-pregate", PARENT_FW("mpll"), 4),
+	/* div 5: provided as a separate line from topcrm */
+	[ZX_CLK_MATRIX_MPLL_D6_PREGATE] = FIXED_DIV("matrix-mpll-d6-pregate", PARENT_FW("mpll"), 6),
+	[ZX_CLK_MATRIX_MPLL_D8_PREGATE] = FIXED_DIV("matrix-mpll-d8-pregate", PARENT_FW("mpll"), 8),
+	[ZX_CLK_MATRIX_MPLL_D12_PREGATE] = FIXED_DIV("matrix-mpll-d12-pregate", PARENT_FW("mpll"),
+						     12),
+	[ZX_CLK_MATRIX_MPLL_D16_PREGATE] = FIXED_DIV("matrix-mpll-d16-pregate", PARENT_FW("mpll"),
+						     16),
+
+	[ZX_CLK_MATRIX_GPLL_D2_D2_PREGATE] = FIXED_DIV("matrix-gpll-d2-d2-pregate",
+						       PARENT_FW("gpll-d2"), 2),
+	[ZX_CLK_MATRIX_GPLL_D2_D4_PREGATE] = FIXED_DIV("matrix-gpll-d2-d4-pregate",
+						       PARENT_FW("gpll-d2"), 4),
+
+	[ZX_CLK_MATRIX_DPLL_D6_PREGATE] = FIXED_DIV("matrix-dpll-d6-pregate", PARENT_FW("dpll"), 6),
+	[ZX_CLK_MATRIX_DPLL_D4_PREGATE] = FIXED_DIV("matrix-dpll-d4-pregate", PARENT_FW("dpll"), 4),
+
+	/*
+	 * Matrix distribution gates: Every matrix-generated subdivision of the input PLLs
+	 * has a gate in 0x118. The original inputs from topcrm (mpll 624, mpll 124.8, gpll 200,
+	 * gpll 100, dpll 491.52) do not.
+	 *
+	 * I do not know of any user of dpll-d6 and do not know the divisor. An 81.92 MHz clock is
+	 * listed as a root clock without a consumer in ZTE's code. Most likely it is a possible
+	 * parent of the ZSP mux.
+	 */
+	[ZX_CLK_MATRIX_MPLL_D16] = GATE("matrix-mpll-d16",
+					PARENT_ID(ZX_CLK_MATRIX_MPLL_D16_PREGATE), 0x118, 0,
+					ZX297520V3_AXI_CANDIDATE),
+	[ZX_CLK_MATRIX_MPLL_D12] = GATE("matrix-mpll-d12",
+					PARENT_ID(ZX_CLK_MATRIX_MPLL_D12_PREGATE), 0x118, 1,
+					ZX297520V3_AXI_CANDIDATE),
+	[ZX_CLK_MATRIX_MPLL_D8] = GATE("matrix-mpll-d8",
+				       PARENT_ID(ZX_CLK_MATRIX_MPLL_D8_PREGATE), 0x118, 2,
+				       ZX297520V3_AXI_CANDIDATE),
+	[ZX_CLK_MATRIX_MPLL_D6] = GATE("matrix-mpll-d6",
+				       PARENT_ID(ZX_CLK_MATRIX_MPLL_D6_PREGATE), 0x118, 3,
+				       ZX297520V3_AXI_CANDIDATE),
+	[ZX_CLK_MATRIX_MPLL_D4] = GATE("matrix-mpll-d4",
+				       PARENT_ID(ZX_CLK_MATRIX_MPLL_D4_PREGATE), 0x118, 4,
+				       ZX297520V3_AXI_CANDIDATE),
+	[ZX_CLK_MATRIX_MPLL_D2] = GATE("matrix-mpll-d2",
+					PARENT_ID(ZX_CLK_MATRIX_MPLL_D2_PREGATE), 0x118, 5, 0),
+
+	[ZX_CLK_MATRIX_DPLL_D6] = GATE("matrix-dpll-d6",
+				       PARENT_ID(ZX_CLK_MATRIX_DPLL_D6_PREGATE), 0x118, 8, 0),
+	[ZX_CLK_MATRIX_DPLL_D4] = GATE("matrix-dpll-d4",
+				       PARENT_ID(ZX_CLK_MATRIX_DPLL_D4_PREGATE), 0x118, 9, 0),
+
+	[ZX_CLK_MATRIX_GPLL_D2_D4] = GATE("matrix-gpll-d2-d4",
+					  PARENT_ID(ZX_CLK_MATRIX_GPLL_D2_D4_PREGATE),
+					  0x118, 12, 0),
+	[ZX_CLK_MATRIX_GPLL_D2_D2] = GATE("matrix-gpll-d2-d2",
+					  PARENT_ID(ZX_CLK_MATRIX_GPLL_D2_D2_PREGATE),
+					  0x118, 13, 0),
+
+	[ZX_CLK_CPU_MUX]          = MUX("cpu-mux", cpu_sel, 0x20, 0, 2),
+	[ZX_CLK_ZSP_MUX]          = MUX("zsp-mux", zsp_sel, 0x30, 0, 2),
+	[ZX_CLK_DDR_CTRL_MUX]     = MUX("ddr-ctrl-mux", ddr_ctrl_sel, 0x50, 0, 2),
+	[ZX_CLK_SD0_MUX]          = MUX("sd0-mux", sd0_sel, 0x50, 4, 3),
+	[ZX_CLK_SD1_MUX]          = MUX("sd1-mux", sd1_sel, 0x50, 8, 3),
+	[ZX_CLK_NAND_MUX]         = MUX("nand-mux", nand_sel, 0x50, 12, 2),
+	[ZX_CLK_EDCP_MUX]         = MUX("edcp-mux", edcp_sel, 0x50, 16, 2),
+	[ZX_CLK_TDM_MUX]          = MUX("tdm-mux", tdm_sel, 0x50, 24, 2),
+	[ZX_CLK_AXI_MUX]          = MUX("axi-mux", axi_sel, ZX297520V3_AXI_AP_VOTE, 0, 3),
+
+	/*
+	 * This bit cuts off the clock signal to the ARM architected timer, which the kernel uses
+	 * as its main timer. It isn't critical per se - there are plenty of proprietary timers
+	 * available that could be used - but the arm arch timer binding does not accept a clock, so
+	 * this CCF driver won't know if the timer is in use.
+	 *
+	 * This clock is fed by the osc26m gate in topcrm+0x140 - this critical clock here protects
+	 * its parent.
+	 *
+	 * Registers 0x148, 0x14c, 0x150, 0x154 allow setting and clearing the timer counter. To
+	 * set the timer, load a value into 0x148 (high 32 bits) and 0x14c (low 32 bits), then
+	 * toggle 0x150 to 1. 0x150 will automatically reset to 0. 0x154 sets the timer to some
+	 * "clear" value, but this value isn't 0.
+	 */
+	[ZX_CLK_SYS_TIMER_WCLK]   = GATE("sys-timer-wclk", PARENT_FW("wclk-osc26m"), 0x144, 1,
+					 CLK_IS_CRITICAL),
+
+	/*
+	 * Both 0x24 and 0x28 bits 1 and 2 stop the CPU. There is also a bit in topcrm+0x138, which
+	 * ZTE's uboot calls "A53 reset", which also stops the CPU. I can't really tell the
+	 * difference between matrix+0x28 and top+0x138. The clock (matrix+0x24) can be disabled
+	 * and enabled from the Cortex M0 and it will nicely stop and restart the A53, retaining
+	 * all state.
+	 */
+	[ZX_CLK_CPU_WCLK]         = GATE("cpu-wclk", PARENT_ID(ZX_CLK_CPU_MUX), 0x24, 1,
+					 CLK_IS_CRITICAL),
+	[ZX_CLK_CPU_PCLK]         = GATE("cpu-pclk", clk_main[0], 0x24, 2, CLK_IS_CRITICAL),
+
+	/*
+	 * There are a lot more controls in matrix+0x100. 13-16 appear to be 4 different AXI
+	 * channels for different priorities. Bit 19 appears to be the DDR PHY wclk.
+	 *
+	 * The important task is to keep gpll enabled if the bootloader selected a gpll-based rate
+	 * for RAM.
+	 */
+	[ZX_CLK_DDR_CTRL_PCLK]    = GATE("ddr-ctrl-pclk", clk_main[0], 0x100, 17, CLK_IS_CRITICAL),
+	[ZX_CLK_DDR_CTRL_WCLK]    = GATE("ddr-ctrl-wclk", PARENT_ID(ZX_CLK_DDR_CTRL_MUX), 0x100, 18,
+					 CLK_IS_CRITICAL),
+
+	[ZX_CLK_ZSP_WCLK]         = GATE("zsp-wclk", PARENT_ID(ZX_CLK_ZSP_MUX), 0x3c, 0, 0),
+
+	/* Both SDIO controllers depend on the ahb-pclk for register access. */
+	[ZX_CLK_SD0_PCLK]         = GATE("sd0-pclk", PARENT_FW("ahb"), 0x54, 12, 0),
+	[ZX_CLK_SD0_WCLK]         = GATE("sd0-wclk", PARENT_ID(ZX_CLK_SD0_MUX), 0x54, 13, 0),
+	[ZX_CLK_SD0_CDET]         = GATE("sd0-cdet", PARENT_FW("osc32k"), 0x54, 14, 0),
+	[ZX_CLK_SD1_PCLK]         = GATE("sd1-pclk", PARENT_FW("ahb"), 0x54, 4, 0),
+	[ZX_CLK_SD1_WCLK]         = GATE("sd1-wclk", PARENT_ID(ZX_CLK_SD1_MUX), 0x54, 5, 0),
+	/*
+	 * I don't know how the cdet clock works. Card detection in the way the dwc,mmc driver uses
+	 * it appears broken no matter this clock's setting.
+	 */
+	[ZX_CLK_SD1_CDET]         = GATE("sd1-cdet", PARENT_FW("osc32k"), 0x54, 6, 0),
+
+	/* This is some "denali" NAND, not the qspi connected one */
+	[ZX_CLK_NAND_WCLK]        = GATE("nand-wclk", PARENT_ID(ZX_CLK_NAND_MUX), 0x54, 20, 0),
+	[ZX_CLK_NAND_PCLK]        = GATE("nand-pclk", clk_main[0], 0x54, 21, 0),
+
+	/*
+	 * There is a set of gates for an unknown device at matrix+0x60. It is some LTE related
+	 * device. If it is fed an incorrect clock, e.g. because dpll is not locked and outputs
+	 * 26 MHz, it will spam IRQs at SPI 68. This is likely a distress notification that some
+	 * internal PLL did not lock or similar
+	 *
+	 * ZTE's kernel has two defines for SPI 64: "LTE_TPU_INT_PS_INT" and "VOU_OSD_INT". The
+	 * latter is unlikely because VOU related clocks sit in matrix+0x168.
+	 *
+	 * Bit 9 in register 0x118 turns it off, providing a hint who the parent is.
+	 */
+	[ZX_CLK_LTE_TPU_WCLK]     = GATE("lte-tpu-wclk", PARENT_ID(ZX_CLK_MATRIX_DPLL_D4),
+					 0x60, 0, 0),
+	[ZX_CLK_LTE_TPU_PCLK]     = GATE("lte-tpu-pclk", clk_main[0],
+					 0x60, 2, 0),
+
+	[ZX_CLK_EDCP_PCLK]        = GATE("edcp-pclk", clk_main[0], 0x64, 1, 0),
+	[ZX_CLK_EDCP_WCLK]        = GATE("edcp-wclk", PARENT_ID(ZX_CLK_EDCP_MUX), 0x64, 2, 0),
+
+	/*
+	 * This seems to be another SPI-Like device. ZTE's firmware operates it and from testing
+	 * it looks like the matrix-osc26m gate in top+0x140 stops this device. This would be
+	 * consistent with the other SPI controllers too.
+	 */
+	[ZX_CLK_SSC_WCLK]         = GATE("ssc-wclk", PARENT_FW("wclk-osc26m"), 0x84, 1, 0),
+	[ZX_CLK_SSC_PCLK]         = GATE("ssc-pclk", clk_main[0], 0x84, 2, 0),
+
+	/*
+	 * PDCFG. Like PMM, either clock bit will allow the device to function. Probably there is
+	 * no wclk line at all and the two bits are just an artifact of generally having two bits
+	 * per device.
+	 */
+	[ZX_CLK_PDCFG_WCLK]       = GATE("pdcfg-wclk", matrix_unk, 0x88, 0, CLK_IS_CRITICAL),
+	[ZX_CLK_PDCFG_PCLK]       = GATE("pdcfg-pclk", clk_main[0], 0x88, 1, CLK_IS_CRITICAL),
+	[ZX_CLK_MBOX_PCLK]        = GATE("mbox-pclk", clk_main[0], 0x88, 2, 0),
+	[ZX_CLK_SRAM0_PCLK]       = GATE("sram0-pclk", clk_main[0], 0x88, 4, 0),
+	[ZX_CLK_GSM_CFG_PCLK]     = GATE("gsm-cfg-pclk", clk_main[0], 0x88, 8, 0),
+
+	/*
+	 * ZTE's driver has a statement to the effect of *(top->base+0x11c) = 5, with a comment
+	 * suggesting that this sets a 50 MHz clock. The clock code itself lists GMAC clocks in
+	 * matrix+0x110 and lists the parents of these clocks as 50 MHz gpll output, but
+	 * the downstream ZTE GMAC driver never enables the clocks. It turns out ZTE's code is
+	 * highly misleading.
+	 *
+	 * The GMAC's work clock is definitely not any gpll output because it keeps working fine
+	 * with gpll disabled. Gating off matrix-osc26m breaks GMAC, so it must be its parent.
+	 *
+	 * The GMAC Gates are left enabled by the boot loader and are required for the GMAC to work.
+	 *
+	 * As for the 50 MHz comment: See rmiiphy-wclk.
+	 *
+	 * The gmac-ahb clock is a child of topcrm's ahb-pclk and is required for DMA transfers in
+	 * and out of GMAC. Without it, GMAC still receives ethernet frames, but can't drain them
+	 * to memory and will eventually set the RX FIFO overflow flag.
+	 */
+	[ZX_CLK_GMAC_WCLK]        = GATE("gmac-wclk", PARENT_FW("wclk-osc26m"), 0x110, 0, 0),
+	[ZX_CLK_GMAC_PCLK]        = GATE("gmac-pclk", clk_main[0], 0x110, 1, 0),
+	[ZX_CLK_GMAC_AHB]         = GATE("gmac-ahb", PARENT_FW("ahb"), 0x110, 2, 0),
+
+	/*
+	 * Is there an AXI bus gate? The symptom of cutting off the AXI mux selection in top+0x140
+	 * is that matrixcrm becomes unreadable from m0 and A53 hangs. Inside matrix itself only 3
+	 * bits fit that bill: 0x8c bits 5, 6, 7. It seems a bit self-defeating to have a clock
+	 * gate that shuts off access to itself though. I expect a clock gate for the bus
+	 * somewhere, and the mux exists, so exposing one AXI clock in the bindings is the correct
+	 * thing to do. It also serves to tell the kernel to keep the mux's parent enabled.
+	 *
+	 * Register 0x8c has 12 settable bits (0xfff). Ultimately it doesn't matter much which
+	 * do-not-remove bit we don't remove. Other bits in this register behave like gates (e.g
+	 * bits 11:8 cut off USB temporarily), so I think we are looking in the right place.
+	 *
+	 * If an explanation for the remaining bits surfaces and they are further gates and/or
+	 * resets, add them to the bindings.
+	 */
+	[ZX_CLK_AXI_WCLK]         = GATE("axi-wclk", PARENT_ID(ZX_CLK_AXI_MUX), 0x8c, 5,
+					 CLK_IS_CRITICAL),
+
+	[ZX_CLK_DMA_PCLK]         = GATE("dma-pclk", clk_main[0], 0x94, 3, 0),
+
+	/*
+	 * There are a lot more VOU related controls in these registers, but turning off the main
+	 * clock seems to shut off the entire VOU MMIO range.
+	 */
+	[ZX_CLK_VOU_WCLK]         = GATE("vou-wclk", matrix_unk, 0x168, 0, 0),
+	[ZX_CLK_VOU_PCLK]         = GATE("vou-pclk", clk_main[0], 0x168, 1, 0),
+
+	/*
+	 * LSP uplink clocks. The pclk is fairly obvious (disabling it shuts off the entire LSP
+	 * register area). The wclk speeds were deduced by setting timers and qspi muxes to a
+	 * specific speed and seeing which bit in matrix+0x7c needs to be enabled for the device
+	 * to work.
+	 *
+	 * Due to the timers I am certain about the 26 MHz and 32 kHz clocks. I cannot directly
+	 * observe the qspi mux frequency, so the clock rates depend on ZTE's qspi mux selection
+	 * being correct.
+	 *
+	 * Two additional bits are specific to sound components - the mux for the LSP's TDM IP is
+	 * in matrixcrm and gets passed down. I2S has a mux in LSP, which can select the dpll-d4
+	 * clock.
+	 */
+	[ZX_CLK_TOLSP_MPLL_D5_WCLK]  = GATE("lsp-mpll-d5", PARENT_FW("mpll-d5"), 0x7c, 0, 0),
+	[ZX_CLK_TOLSP_MPLL_D4_WCLK]  = GATE("lsp-mpll-d4", PARENT_ID(ZX_CLK_MATRIX_MPLL_D4),
+					    0x7c, 1, 0),
+	[ZX_CLK_TOLSP_MPLL_D6_WCLK]  = GATE("lsp-mpll-d6", PARENT_ID(ZX_CLK_MATRIX_MPLL_D6),
+					    0x7c, 2, 0),
+	[ZX_CLK_TOLSP_MPLL_D8_WCLK]  = GATE("lsp-mpll-d8", PARENT_ID(ZX_CLK_MATRIX_MPLL_D8),
+					    0x7c, 3, 0),
+	[ZX_CLK_TOLSP_MPLL_D12_WCLK] = GATE("lsp-mpll-d12", PARENT_ID(ZX_CLK_MATRIX_MPLL_D12),
+					    0x7c, 4, 0),
+	[ZX_CLK_TOLSP_OSC26M_WCLK]   = GATE("lsp-osc26m", PARENT_FW("wclk-osc26m"), 0x7c, 5, 0),
+	[ZX_CLK_TOLSP_OSC32K_WCLK]   = GATE("lsp-osc32k", PARENT_FW("osc32k"), 0x7c, 6, 0),
+	[ZX_CLK_TOLSP_PCLK]          = GATE("lsp-pclk", clk_main[0], 0x7c, 7, 0),
+	[ZX_CLK_TOLSP_TDM_WCLK]      = GATE("lsp-tdm-wclk", PARENT_ID(ZX_CLK_TDM_MUX), 0x7c, 8, 0),
+	[ZX_CLK_TOLSP_DPLL_D4_WCLK]  = GATE("lsp-dpll-d4", PARENT_ID(ZX_CLK_MATRIX_DPLL_D4),
+					    0x7c, 9, 0),
+};
+
+static int zx297520v3_matrix_init(struct regmap *map)
+{
+	u32 axi_val;
+	int res;
+
+	/* Read the global AXI speed selection, insert it into our ballot and enable voting */
+	res = regmap_read(map, 0x0, &axi_val);
+	if (res)
+		return res;
+
+	axi_val &= 0x7;
+	res = regmap_write(map, ZX297520V3_AXI_AP_VOTE, axi_val);
+	if (res)
+		return res;
+
+	return regmap_write(map, 0x12c, 1);
+}
+
+static unsigned int zx297520v3_matrix_exports[] = {
+	[ZX297520V3_SYS_TIMER_WCLK] =		ZX_CLK_SYS_TIMER_WCLK,
+	[ZX297520V3_CPU_WCLK] =			ZX_CLK_CPU_WCLK,
+	[ZX297520V3_CPU_PCLK] =			ZX_CLK_CPU_PCLK,
+	[ZX297520V3_ZSP_WCLK] =			ZX_CLK_ZSP_WCLK,
+	[ZX297520V3_DDR_CTRL_PCLK] =		ZX_CLK_DDR_CTRL_PCLK,
+	[ZX297520V3_DDR_CTRL_WCLK] =		ZX_CLK_DDR_CTRL_WCLK,
+	[ZX297520V3_EDCP_WCLK] =		ZX_CLK_EDCP_WCLK,
+	[ZX297520V3_EDCP_PCLK] =		ZX_CLK_EDCP_PCLK,
+	[ZX297520V3_SD0_WCLK] =			ZX_CLK_SD0_WCLK,
+	[ZX297520V3_SD0_PCLK] =			ZX_CLK_SD0_PCLK,
+	[ZX297520V3_SD0_CDET] =			ZX_CLK_SD0_CDET,
+	[ZX297520V3_SD1_WCLK] =			ZX_CLK_SD1_WCLK,
+	[ZX297520V3_SD1_PCLK] =			ZX_CLK_SD1_PCLK,
+	[ZX297520V3_SD1_CDET] =			ZX_CLK_SD1_CDET,
+	[ZX297520V3_NAND_WCLK] =		ZX_CLK_NAND_WCLK,
+	[ZX297520V3_NAND_PCLK] =		ZX_CLK_NAND_PCLK,
+	[ZX297520V3_DMA_PCLK] =			ZX_CLK_DMA_PCLK,
+	[ZX297520V3_MBOX_PCLK] =		ZX_CLK_MBOX_PCLK,
+	[ZX297520V3_PDCFG_WCLK] =		ZX_CLK_PDCFG_WCLK,
+	[ZX297520V3_PDCFG_PCLK] =		ZX_CLK_PDCFG_PCLK,
+	[ZX297520V3_SSC_WCLK] =			ZX_CLK_SSC_WCLK,
+	[ZX297520V3_SSC_PCLK] =			ZX_CLK_SSC_PCLK,
+	[ZX297520V3_AXI_WCLK] =			ZX_CLK_AXI_WCLK,
+	[ZX297520V3_GMAC_WCLK] =		ZX_CLK_GMAC_WCLK,
+	[ZX297520V3_GMAC_PCLK] =		ZX_CLK_GMAC_PCLK,
+	[ZX297520V3_GMAC_AHB] =			ZX_CLK_GMAC_AHB,
+	[ZX297520V3_VOU_WCLK] =			ZX_CLK_VOU_WCLK,
+	[ZX297520V3_VOU_PCLK] =			ZX_CLK_VOU_PCLK,
+	[ZX297520V3_LSP_MPLL_D5_WCLK] =		ZX_CLK_TOLSP_MPLL_D5_WCLK,
+	[ZX297520V3_LSP_MPLL_D4_WCLK] =		ZX_CLK_TOLSP_MPLL_D4_WCLK,
+	[ZX297520V3_LSP_MPLL_D6_WCLK] =		ZX_CLK_TOLSP_MPLL_D6_WCLK,
+	[ZX297520V3_LSP_MPLL_D8_WCLK] =		ZX_CLK_TOLSP_MPLL_D8_WCLK,
+	[ZX297520V3_LSP_MPLL_D12_WCLK] =	ZX_CLK_TOLSP_MPLL_D12_WCLK,
+	[ZX297520V3_LSP_OSC26M_WCLK] =		ZX_CLK_TOLSP_OSC26M_WCLK,
+	[ZX297520V3_LSP_OSC32K_WCLK] =		ZX_CLK_TOLSP_OSC32K_WCLK,
+	[ZX297520V3_LSP_PCLK] =			ZX_CLK_TOLSP_PCLK,
+	[ZX297520V3_LSP_TDM_WCLK] =		ZX_CLK_TOLSP_TDM_WCLK,
+	[ZX297520V3_LSP_DPLL_D4_WCLK] =		ZX_CLK_TOLSP_DPLL_D4_WCLK,
+	[ZX297520V3_SRAM0_PCLK] =		ZX_CLK_SRAM0_PCLK,
+	[ZX297520V3_GSM_CFG_PCLK] =		ZX_CLK_GSM_CFG_PCLK,
+};
+
+static const struct zx_clk_data zx297520v3_matrixclk_data = {
+	.init = zx297520v3_matrix_init,
+	.clocks = zx297520v3_matrix_clocks,
+	.num_clocks = ARRAY_SIZE(zx297520v3_matrix_clocks),
+	.exports = zx297520v3_matrix_exports,
+	.num_exports = ARRAY_SIZE(zx297520v3_matrix_exports),
+};
+
 static int clk_zx297520v3_probe(struct platform_device *pdev)
 {
 	const struct platform_device_id *id = platform_get_device_id(pdev);
@@ -785,6 +1264,10 @@ static const struct platform_device_id clk_zx297520v3_ids[] = {
 		.name = "zx297520v3-topclk",
 		.driver_data = (kernel_ulong_t)&zx297520v3_topclk_data,
 	},
+	{
+		.name = "zx297520v3-matrixclk",
+		.driver_data = (kernel_ulong_t)&zx297520v3_matrixclk_data,
+	},
 	{ }
 };
 MODULE_DEVICE_TABLE(platform, clk_zx297520v3_ids);

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:42 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:42 +0300
Subject: [PATCH v9 10/12] clk: zte: Introduce a driver for zx297520v3 LSP
 clocks
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-10-d05530d85d28@gmail.com>

"LSP" is ZTE's term for this part of the SoC, I suspect it stands for
"low speed peripherals". The main UART is here, together with the flash
controller and more surplus proprietary timers.

It also has one more I2C controller that supposedly connects to a
battery charger, SPI for displays and I2S for analog telephones. The
boards I have don't have any of these components though.

Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

---

Version 9:
Link internal parents by an (internal) clock ID rather than names.

One intentional clock tree change: The PWM device's parent is the 104
MHz mpll-d6 input. With some manual experimentation I managed to make
the WiFi led blink with PWM, so I could confirm which matrixcrm bit
gates off its parent.

Update the comments to say that the device at lspcrm+0x4000 is indeed a
Pulse Width Modulator.

@Sashiko: If you find any other clock tree changes compared to version
8, they are unintentional and likely bugs.

Version 8:
Double-checked pclk and wclk. They were the wrong way around in previous
submissions. Pclk is bit 0, wclk is bit 1.

Version 7:
Underscore in clock names to dashes

Version 6:
use MFD for LSP too

Version 5:
Removed unused tdm_lsp_sel. Fix zx297529 vs zx297520 typo.
Adjusted .compatible to zte,zx297520v3-lspcrm
---
 drivers/clk/zte/clk-zx297520v3.c | 285 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 285 insertions(+)

diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
index 6c4a7a1c3a9b..286eaedffce9 100644
--- a/drivers/clk/zte/clk-zx297520v3.c
+++ b/drivers/clk/zte/clk-zx297520v3.c
@@ -1248,6 +1248,287 @@ static const struct zx_clk_data zx297520v3_matrixclk_data = {
 	.num_exports = ARRAY_SIZE(zx297520v3_matrix_exports),
 };
 
+/*
+ * LSP clock entries have a common pattern: Bit 0 for pclk, Bit 1 for wclk. Bit 4 (and sometimes
+ * more) for wclk mux.
+ *
+ * Bit 8 and 9 are resets handled by the reset-zte-zx297520v3 driver.
+ *
+ * Bits 15:12 can be a divisor, but not all clocks have it. Some clocks have a divisor in 19:16.
+ *
+ * The ID given in this table is the first register in the device's MMIO space. ZTE's drivers
+ * usually call this a version register, but it looks more like a device identifier.
+ *
+ * It looks like the registers map to devices like this:
+ *
+ * CRM Reg	function	div	dev offset(lsp + xxxx)	ID
+ * 0x0: Read-only, probably device identifier			0x00752100
+ * 0x4:		timer_l1	Y	0x1000			0x02020000
+ * 0x8:		watchdog_l2	Y	0x2000			0x02020000
+ * 0xc:		watchdog_l3	Y	0x3000			0x02020000
+ * 0x10:	pwm		N	0x4000			0x01020000
+ * 0x14:	i2s0		Yh	0x5000			0x01030000
+ * 0x18:	always 0	-	-			-
+ * 0x1c:	i2s1		Yh	0x6000			0x01030000
+ * 0x20:	always 0	-	-			-
+ * 0x24:	qspi		N	0x7000			0x01040000
+ * 0x28:	uart1		N	0x8000			0x01060000
+ * 0x2c:	i2c1		N	0x9000			0x01020000
+ * 0x30:	spi0		Y	0xa000			0x01040000
+ * 0x34:	timer_lb	Y	0xb000			0x02020000
+ * 0x38:	timer_lc	Y	0xc000			0x02020000
+ * 0x3c:	uart2		N	0xd000			0x01060000
+ * 0x40:	watchdog_le	Y	0xe000			0x02020000
+ * 0x44:	timer_lf	Y	0xf000			0x02020000
+ * 0x48:	spi1		Y	0x10000			0x01040000
+ * 0x4c:	timer_l11	Y	0x11000			0x02020000
+ * 0x50:	tdm		Yh	0x12000			0x01040000
+ *
+ * Registers 0x58, 0x5c, 0x60, 0x64, 0x68 seem to contain more controls for i2s and tdm.
+ *
+ * The device at offset 0x4000 (clk reg 0x10) is a PWM device. The ID matches that of i2c, it has a
+ * larger register set. ZTE's kernel does not operate it at all. The old upstream pwm-zx.c driver
+ * provided enough instructions to blink the Wifi led and isolate the parent.
+ */
+
+enum lsp_clock_ids {
+	ZX_CLK_TIMER_L1_MUX,
+	ZX_CLK_WDT_L2_MUX,
+	ZX_CLK_WDT_L3_MUX,
+	ZX_CLK_I2S0_MUX,
+	ZX_CLK_I2S1_MUX,
+	ZX_CLK_QSPI_MUX,
+	ZX_CLK_UART1_MUX,
+	ZX_CLK_I2C1_MUX,
+	ZX_CLK_SPI0_MUX,
+	ZX_CLK_TIMER_LB_MUX,
+	ZX_CLK_TIMER_LC_MUX,
+	ZX_CLK_UART2_MUX,
+	ZX_CLK_WDT_LE_MUX,
+	ZX_CLK_TIMER_LF_MUX,
+	ZX_CLK_SPI1_MUX,
+	ZX_CLK_TIMER_L11_MUX,
+
+	ZX_CLK_TIMER_L1_DIV,
+	ZX_CLK_WDT_L2_DIV,
+	ZX_CLK_WDT_L3_DIV,
+	ZX_CLK_I2S0_DIV,
+	ZX_CLK_I2S1_DIV,
+	ZX_CLK_SPI0_DIV,
+	ZX_CLK_TIMER_LB_DIV,
+	ZX_CLK_TIMER_LC_DIV,
+	ZX_CLK_WDT_LE_DIV,
+	ZX_CLK_TIMER_LF_DIV,
+	ZX_CLK_SPI1_DIV,
+	ZX_CLK_TIMER_L11_DIV,
+	ZX_CLK_TDM_DIV,
+
+	ZX_CLK_TIMER_L1_PCLK,
+	ZX_CLK_TIMER_L1_WCLK,
+	ZX_CLK_WDT_L2_PCLK,
+	ZX_CLK_WDT_L2_WCLK,
+	ZX_CLK_WDT_L3_PCLK,
+	ZX_CLK_WDT_L3_WCLK,
+	ZX_CLK_PWM_PCLK,
+	ZX_CLK_PWM_WCLK,
+	ZX_CLK_I2S0_PCLK,
+	ZX_CLK_I2S0_WCLK,
+	ZX_CLK_I2S1_PCLK,
+	ZX_CLK_I2S1_WCLK,
+	ZX_CLK_QSPI_PCLK,
+	ZX_CLK_QSPI_WCLK,
+	ZX_CLK_UART1_PCLK,
+	ZX_CLK_UART1_WCLK,
+	ZX_CLK_I2C1_PCLK,
+	ZX_CLK_I2C1_WCLK,
+	ZX_CLK_SPI0_PCLK,
+	ZX_CLK_SPI0_WCLK,
+	ZX_CLK_TIMER_LB_PCLK,
+	ZX_CLK_TIMER_LB_WCLK,
+	ZX_CLK_TIMER_LC_PCLK,
+	ZX_CLK_TIMER_LC_WCLK,
+	ZX_CLK_UART2_PCLK,
+	ZX_CLK_UART2_WCLK,
+	ZX_CLK_WDT_LE_PCLK,
+	ZX_CLK_WDT_LE_WCLK,
+	ZX_CLK_TIMER_LF_PCLK,
+	ZX_CLK_TIMER_LF_WCLK,
+	ZX_CLK_SPI1_PCLK,
+	ZX_CLK_SPI1_WCLK,
+	ZX_CLK_TIMER_L11_PCLK,
+	ZX_CLK_TIMER_L11_WCLK,
+	ZX_CLK_TDM_PCLK,
+	ZX_CLK_TDM_WCLK,
+};
+
+static const struct zx_parent_desc timer_lsp_sel[] = {
+	PARENT_FW("osc32k"),
+	PARENT_FW("osc26m"),
+};
+
+static const struct zx_parent_desc uart_lsp_sel[] = {
+	PARENT_FW("osc26m"),
+	PARENT_FW("mpll-d6"),
+};
+
+static const struct zx_parent_desc i2s_lsp_sel[] = {
+	PARENT_FW("osc26m"),
+	PARENT_FW("dpll-d4"),
+	PARENT_FW("mpll-d6"),
+	/* Unknown */
+};
+
+static const struct zx_parent_desc spi_lsp_sel[] = {
+	PARENT_FW("osc26m"),
+	PARENT_FW("mpll-d4"),
+	PARENT_FW("mpll-d6"),
+	/* Unknown */
+};
+
+static const struct zx_parent_desc qspi_lsp_sel[] = {
+	PARENT_FW("osc26m"),
+	PARENT_FW("mpll-d4"),
+	PARENT_FW("mpll-d5"),
+	PARENT_FW("mpll-d6"),
+	PARENT_FW("mpll-d8"),
+	PARENT_FW("mpll-d12"),
+	PARENT_FW("osc26m"),
+	PARENT_FW("osc26m"),
+};
+
+static const struct zx_clock zx297520v3_lsp_clocks[] = {
+	[ZX_CLK_TIMER_L1_MUX]     = MUX("timer-l1-mux",   timer_lsp_sel,    0x04,  4, 1),
+	[ZX_CLK_WDT_L2_MUX]       = MUX("wdt-l2-mux",     timer_lsp_sel,    0x08,  4, 1),
+	[ZX_CLK_WDT_L3_MUX]       = MUX("wdt-l3-mux",     timer_lsp_sel,    0x0c,  4, 1),
+	/* PWM: No mux bit can be set */
+	[ZX_CLK_I2S0_MUX]         = MUX("i2s0-mux",       i2s_lsp_sel,      0x14,  4, 2),
+	/* 0x18: Always 0 */
+	[ZX_CLK_I2S1_MUX]         = MUX("i2s1-mux",       i2s_lsp_sel,      0x1c,  4, 2),
+	/* 0x20: Always 0 */
+	[ZX_CLK_QSPI_MUX]         = MUX("qspi-mux",       qspi_lsp_sel,     0x24,  4, 3),
+	[ZX_CLK_UART1_MUX]        = MUX("uart1-mux",      uart_lsp_sel,     0x28,  4, 1),
+	[ZX_CLK_I2C1_MUX]         = MUX("i2c1-mux",       uart_lsp_sel,     0x2c,  4, 1),
+	[ZX_CLK_SPI0_MUX]         = MUX("spi0-mux",       spi_lsp_sel,      0x30,  4, 2),
+	[ZX_CLK_TIMER_LB_MUX]     = MUX("timer-lb-mux",   timer_lsp_sel,    0x34,  4, 1),
+	[ZX_CLK_TIMER_LC_MUX]     = MUX("timer-lc-mux",   timer_lsp_sel,    0x38,  4, 1),
+	[ZX_CLK_UART2_MUX]        = MUX("uart2-mux",      uart_lsp_sel,     0x3c,  4, 1),
+	[ZX_CLK_WDT_LE_MUX]       = MUX("wdt-le-mux",     timer_lsp_sel,    0x40,  4, 1),
+	[ZX_CLK_TIMER_LF_MUX]     = MUX("timer-lf-mux",   timer_lsp_sel,    0x44,  4, 1),
+	[ZX_CLK_SPI1_MUX]         = MUX("spi1-mux",       spi_lsp_sel,      0x48,  4, 2),
+	[ZX_CLK_TIMER_L11_MUX]    = MUX("timer-l11-mux",  timer_lsp_sel,    0x4c,  4, 1),
+	/* TDM: No mux in LSP. Instead, it is in matrix with a separate clk line to LSP */
+
+	[ZX_CLK_TIMER_L1_DIV]     = DIV("timer-l1-div", PARENT_ID(ZX_CLK_TIMER_L1_MUX),
+					0x04, 12, 4),
+	[ZX_CLK_WDT_L2_DIV]       = DIV("wdt-l2-div", PARENT_ID(ZX_CLK_WDT_L2_MUX), 0x08, 12, 4),
+	[ZX_CLK_WDT_L3_DIV]       = DIV("wdt-l3-div", PARENT_ID(ZX_CLK_WDT_L3_MUX), 0x0c, 12, 4),
+	/* PWM: No div */
+	[ZX_CLK_I2S0_DIV]         = DIV("i2s0-div", PARENT_ID(ZX_CLK_I2S0_MUX), 0x14, 16, 4),
+	/* 0x18: Always 0 */
+	[ZX_CLK_I2S1_DIV]         = DIV("i2s1-div", PARENT_ID(ZX_CLK_I2S1_MUX), 0x1c, 16, 4),
+	/* 0x20: Always 0 */
+	/* qspi, uart1, i2c1: No div */
+	[ZX_CLK_SPI0_DIV]         = DIV("spi0-div", PARENT_ID(ZX_CLK_SPI0_MUX), 0x30, 12, 4),
+	[ZX_CLK_TIMER_LB_DIV]     = DIV("timer-lb-div", PARENT_ID(ZX_CLK_TIMER_LB_MUX),
+					0x34, 12, 4),
+	[ZX_CLK_TIMER_LC_DIV]     = DIV("timer-lc-div", PARENT_ID(ZX_CLK_TIMER_LC_MUX),
+					0x38, 12, 4),
+	/* uart2: No div */
+	[ZX_CLK_WDT_LE_DIV]       = DIV("wdt-le-div", PARENT_ID(ZX_CLK_WDT_LE_MUX), 0x40, 12, 4),
+	[ZX_CLK_TIMER_LF_DIV]     = DIV("timer-lf-div", PARENT_ID(ZX_CLK_TIMER_LF_MUX),
+					0x44, 12, 4),
+	[ZX_CLK_SPI1_DIV]         = DIV("spi1-div", PARENT_ID(ZX_CLK_SPI1_MUX), 0x48, 12, 4),
+	[ZX_CLK_TIMER_L11_DIV]    = DIV("timer-l11-div", PARENT_ID(ZX_CLK_TIMER_L11_MUX),
+					0x4c, 12, 4),
+	[ZX_CLK_TDM_DIV]        = DIV("tdm-div", PARENT_FW("tdm-wclk"), 0x50, 16, 4),
+
+	[ZX_CLK_TIMER_L1_PCLK]  = GATE("timer-l1-pclk", PARENT_FW("pclk"), 0x04, 0, 0),
+	[ZX_CLK_TIMER_L1_WCLK]  = GATE("timer-l1-wclk", PARENT_ID(ZX_CLK_TIMER_L1_DIV), 0x04, 1, 0),
+	[ZX_CLK_WDT_L2_PCLK]    = GATE("wdt-l2-pclk", PARENT_FW("pclk"), 0x08, 0, 0),
+	[ZX_CLK_WDT_L2_WCLK]    = GATE("wdt-l2-wclk", PARENT_ID(ZX_CLK_WDT_L2_DIV), 0x08, 1, 0),
+	[ZX_CLK_WDT_L3_PCLK]    = GATE("wdt-l3-pclk", PARENT_FW("pclk"), 0x0c, 0, 0),
+	[ZX_CLK_WDT_L3_WCLK]    = GATE("wdt-l3-wclk", PARENT_ID(ZX_CLK_WDT_L3_DIV), 0x0c, 1, 0),
+	[ZX_CLK_PWM_PCLK]       = GATE("pwm-pclk", PARENT_FW("pclk"), 0x10, 0, 0),
+	[ZX_CLK_PWM_WCLK]       = GATE("pwm-wclk", PARENT_FW("mpll-d6"), 0x10, 1, 0),
+	[ZX_CLK_I2S0_PCLK]      = GATE("i2s0-pclk", PARENT_FW("pclk"), 0x14, 0, 0),
+	[ZX_CLK_I2S0_WCLK]      = GATE("i2s0-wclk", PARENT_ID(ZX_CLK_I2S0_DIV), 0x14, 1, 0),
+	/* 0x18: Always 0 */
+	[ZX_CLK_I2S1_PCLK]      = GATE("i2s1-pclk", PARENT_FW("pclk"), 0x1c, 0, 0),
+	[ZX_CLK_I2S1_WCLK]      = GATE("i2s1-wclk", PARENT_ID(ZX_CLK_I2S1_DIV), 0x1c, 1, 0),
+	/* 0x20: Always 0 */
+	[ZX_CLK_QSPI_PCLK]      = GATE("qspi-pclk", PARENT_FW("pclk"), 0x24, 0, 0),
+	[ZX_CLK_QSPI_WCLK]      = GATE("qspi-wclk", PARENT_ID(ZX_CLK_QSPI_MUX), 0x24, 1, 0),
+	[ZX_CLK_UART1_PCLK]     = GATE("uart1-pclk", PARENT_FW("pclk"), 0x28, 0, 0),
+	[ZX_CLK_UART1_WCLK]     = GATE("uart1-wclk", PARENT_ID(ZX_CLK_UART1_MUX), 0x28, 1, 0),
+	[ZX_CLK_I2C1_PCLK]      = GATE("i2c1-pclk", PARENT_FW("pclk"), 0x2c, 0, 0),
+	[ZX_CLK_I2C1_WCLK]      = GATE("i2c1-wclk", PARENT_ID(ZX_CLK_I2C1_MUX), 0x2c, 1, 0),
+	[ZX_CLK_SPI0_PCLK]      = GATE("spi0-pclk", PARENT_FW("pclk"), 0x30, 0, 0),
+	[ZX_CLK_SPI0_WCLK]      = GATE("spi0-wclk", PARENT_ID(ZX_CLK_SPI0_DIV), 0x30, 1, 0),
+	[ZX_CLK_TIMER_LB_PCLK]  = GATE("timer-lb-pclk", PARENT_FW("pclk"), 0x34, 0, 0),
+	[ZX_CLK_TIMER_LB_WCLK]  = GATE("timer-lb-wclk", PARENT_ID(ZX_CLK_TIMER_LB_DIV), 0x34, 1, 0),
+	[ZX_CLK_TIMER_LC_PCLK]  = GATE("timer-lc-pclk", PARENT_FW("pclk"), 0x38, 0, 0),
+	[ZX_CLK_TIMER_LC_WCLK]  = GATE("timer-lc-wclk", PARENT_ID(ZX_CLK_TIMER_LC_DIV), 0x38, 1, 0),
+	[ZX_CLK_UART2_PCLK]     = GATE("uart2-pclk", PARENT_FW("pclk"), 0x3c, 0, 0),
+	[ZX_CLK_UART2_WCLK]     = GATE("uart2-wclk", PARENT_ID(ZX_CLK_UART2_MUX), 0x3c, 1, 0),
+	[ZX_CLK_WDT_LE_PCLK]    = GATE("wdt-le-pclk", PARENT_FW("pclk"), 0x40, 0, 0),
+	[ZX_CLK_WDT_LE_WCLK]    = GATE("wdt-le-wclk", PARENT_ID(ZX_CLK_WDT_LE_DIV), 0x40, 1, 0),
+	[ZX_CLK_TIMER_LF_PCLK]  = GATE("timer-lf-pclk", PARENT_FW("pclk"), 0x44, 0, 0),
+	[ZX_CLK_TIMER_LF_WCLK]  = GATE("timer-lf-wclk", PARENT_ID(ZX_CLK_TIMER_LF_DIV), 0x44, 1, 0),
+	[ZX_CLK_SPI1_PCLK]      = GATE("spi1-pclk", PARENT_FW("pclk"), 0x48, 0, 0),
+	[ZX_CLK_SPI1_WCLK]      = GATE("spi1-wclk", PARENT_ID(ZX_CLK_SPI1_DIV), 0x48, 1, 0),
+	[ZX_CLK_TIMER_L11_PCLK] = GATE("timer-l11-pclk", PARENT_FW("pclk"), 0x4c, 0, 0),
+	[ZX_CLK_TIMER_L11_WCLK] = GATE("timer-l11-wclk", PARENT_ID(ZX_CLK_TIMER_L11_DIV),
+				       0x4c, 1, 0),
+	[ZX_CLK_TDM_PCLK]       = GATE("tdm-pclk", PARENT_FW("pclk"), 0x50, 0, 0),
+	[ZX_CLK_TDM_WCLK]       = GATE("tdm-wclk", PARENT_ID(ZX_CLK_TDM_DIV), 0x50, 1, 0),
+};
+
+static unsigned int zx297520v3_lsp_exports[] = {
+	[ZX297520V3_TIMER_L1_WCLK] =	ZX_CLK_TIMER_L1_WCLK,
+	[ZX297520V3_TIMER_L1_PCLK] =	ZX_CLK_TIMER_L1_PCLK,
+	[ZX297520V3_WDT_L2_WCLK] =	ZX_CLK_WDT_L2_WCLK,
+	[ZX297520V3_WDT_L2_PCLK] =	ZX_CLK_WDT_L2_PCLK,
+	[ZX297520V3_WDT_L3_WCLK] =	ZX_CLK_WDT_L3_WCLK,
+	[ZX297520V3_WDT_L3_PCLK] =	ZX_CLK_WDT_L3_PCLK,
+	[ZX297520V3_PWM_WCLK] =		ZX_CLK_PWM_WCLK,
+	[ZX297520V3_PWM_PCLK] =		ZX_CLK_PWM_PCLK,
+	[ZX297520V3_I2S0_WCLK] =	ZX_CLK_I2S0_WCLK,
+	[ZX297520V3_I2S0_PCLK] =	ZX_CLK_I2S0_PCLK,
+	[ZX297520V3_I2S1_WCLK] =	ZX_CLK_I2S1_WCLK,
+	[ZX297520V3_I2S1_PCLK] =	ZX_CLK_I2S1_PCLK,
+	[ZX297520V3_QSPI_WCLK] =	ZX_CLK_QSPI_WCLK,
+	[ZX297520V3_QSPI_PCLK] =	ZX_CLK_QSPI_PCLK,
+	[ZX297520V3_UART1_WCLK] =	ZX_CLK_UART1_WCLK,
+	[ZX297520V3_UART1_PCLK] =	ZX_CLK_UART1_PCLK,
+	[ZX297520V3_I2C1_WCLK] =	ZX_CLK_I2C1_WCLK,
+	[ZX297520V3_I2C1_PCLK] =	ZX_CLK_I2C1_PCLK,
+	[ZX297520V3_SPI0_WCLK] =	ZX_CLK_SPI0_WCLK,
+	[ZX297520V3_SPI0_PCLK] =	ZX_CLK_SPI0_PCLK,
+	[ZX297520V3_TIMER_LB_WCLK] =	ZX_CLK_TIMER_LB_WCLK,
+	[ZX297520V3_TIMER_LB_PCLK] =	ZX_CLK_TIMER_LB_PCLK,
+	[ZX297520V3_TIMER_LC_WCLK] =	ZX_CLK_TIMER_LC_WCLK,
+	[ZX297520V3_TIMER_LC_PCLK] =	ZX_CLK_TIMER_LC_PCLK,
+	[ZX297520V3_UART2_WCLK] =	ZX_CLK_UART2_WCLK,
+	[ZX297520V3_UART2_PCLK] =	ZX_CLK_UART2_PCLK,
+	[ZX297520V3_WDT_LE_WCLK] =	ZX_CLK_WDT_LE_WCLK,
+	[ZX297520V3_WDT_LE_PCLK] =	ZX_CLK_WDT_LE_PCLK,
+	[ZX297520V3_TIMER_LF_WCLK] =	ZX_CLK_TIMER_LF_WCLK,
+	[ZX297520V3_TIMER_LF_PCLK] =	ZX_CLK_TIMER_LF_PCLK,
+	[ZX297520V3_SPI1_WCLK] =	ZX_CLK_SPI1_WCLK,
+	[ZX297520V3_SPI1_PCLK] =	ZX_CLK_SPI1_PCLK,
+	[ZX297520V3_TIMER_L11_WCLK] =	ZX_CLK_TIMER_L11_WCLK,
+	[ZX297520V3_TIMER_L11_PCLK] =	ZX_CLK_TIMER_L11_PCLK,
+	[ZX297520V3_TDM_WCLK] =		ZX_CLK_TDM_WCLK,
+	[ZX297520V3_TDM_PCLK] =		ZX_CLK_TDM_PCLK,
+};
+
+static const struct zx_clk_data zx297520v3_lspclk_data = {
+	.clocks = zx297520v3_lsp_clocks,
+	.num_clocks = ARRAY_SIZE(zx297520v3_lsp_clocks),
+	.exports = zx297520v3_lsp_exports,
+	.num_exports = ARRAY_SIZE(zx297520v3_lsp_exports),
+};
+
 static int clk_zx297520v3_probe(struct platform_device *pdev)
 {
 	const struct platform_device_id *id = platform_get_device_id(pdev);
@@ -1268,6 +1549,10 @@ static const struct platform_device_id clk_zx297520v3_ids[] = {
 		.name = "zx297520v3-matrixclk",
 		.driver_data = (kernel_ulong_t)&zx297520v3_matrixclk_data,
 	},
+	{
+		.name = "zx297520v3-lspclk",
+		.driver_data = (kernel_ulong_t)&zx297520v3_lspclk_data,
+	},
 	{ }
 };
 MODULE_DEVICE_TABLE(platform, clk_zx297520v3_ids);

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:43 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:43 +0300
Subject: [PATCH v9 11/12] reset: zte: Add a zx297520v3 reset driver
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-11-d05530d85d28@gmail.com>

This drives the MFD child devices created by the zte-zx297520v3-crm
driver.

Reviewed-by: Philipp Zabel <p.zabel at pengutronix.de>
Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>
---

v9:
Remove HSIC and USB PHY resets, the phy driver will access them through
the shared regmap directly
Sort include files alphabetically
Depend on HAS_IOMEM due to "select MFD_SYSCON" (Sashiko). This has some,
but not consistent, precedent in the same file

v8: Select MFD_SYSCON for COMPILE_TESTing this driver without the MFD
parent driver.

v7:
Add MODULE_DEVICE_TABLE (Philipp)
Add DDR control and LSP resets

v6:
Add a comment wrt checking all bits in .status() (Philipp)
include cleanup (Philipp, Sashiko)

v5:
Make top and matrix MFD children instead of aux devices
Split USB PHY reset into its own reset ID
Remove USB reset wait code - this will be handled via syscon from a
future minimal phy-zx29-usb driver
---
 MAINTAINERS                          |   1 +
 drivers/reset/Kconfig                |  13 ++
 drivers/reset/Makefile               |   1 +
 drivers/reset/reset-zte-zx297520v3.c | 236 +++++++++++++++++++++++++++++++++++
 4 files changed, 251 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 77fa86c671b8..eab0cdf8efac 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3917,6 +3917,7 @@ F:	arch/arm/boot/dts/zte/
 F:	arch/arm/mach-zte/
 F:	drivers/clk/zte/
 F:	drivers/mfd/zte-zx297520v3-crm.c
+F:	drivers/reset/reset-zte-zx297520v3.c
 F:	include/dt-bindings/clock/zte,zx297520v3-clk.h
 F:	include/dt-bindings/phy/zte,zx297520v3-topcrm.h
 F:	include/dt-bindings/reset/zte,zx297520v3-reset.h
diff --git a/drivers/reset/Kconfig b/drivers/reset/Kconfig
index d009eb0849a3..b92bd0c16517 100644
--- a/drivers/reset/Kconfig
+++ b/drivers/reset/Kconfig
@@ -404,6 +404,19 @@ config RESET_UNIPHIER_GLUE
 	  on UniPhier SoCs. Say Y if you want to control reset signals
 	  provided by the glue layer.
 
+config RESET_ZTE_ZX297520V3
+	tristate "ZTE zx297520v3 Reset Driver"
+	depends on HAS_IOMEM
+	depends on MFD_ZTE_ZX297520V3_CRM || COMPILE_TEST
+	default SOC_ZX297520V3
+	select MFD_SYSCON
+	help
+	  This enables the reset controllers for ZTE zx297520v3 SoCs. The reset
+	  controllers are required for booting this board and are part of the
+	  clock and reset management controllers on this board, so you will also
+	  need MFD_ZTE_ZX297520V3_CRM. Enable this if you are building a kernel
+	  for a ZTE zx297520v3 based board.
+
 config RESET_ZYNQ
 	bool "ZYNQ Reset Driver" if COMPILE_TEST
 	default ARCH_ZYNQ
diff --git a/drivers/reset/Makefile b/drivers/reset/Makefile
index 3e52569bd276..9a8a48d44dc4 100644
--- a/drivers/reset/Makefile
+++ b/drivers/reset/Makefile
@@ -50,5 +50,6 @@ obj-$(CONFIG_RESET_TI_TPS380X) += reset-tps380x.o
 obj-$(CONFIG_RESET_TN48M_CPLD) += reset-tn48m.o
 obj-$(CONFIG_RESET_UNIPHIER) += reset-uniphier.o
 obj-$(CONFIG_RESET_UNIPHIER_GLUE) += reset-uniphier-glue.o
+obj-$(CONFIG_RESET_ZTE_ZX297520V3) += reset-zte-zx297520v3.o
 obj-$(CONFIG_RESET_ZYNQ) += reset-zynq.o
 obj-$(CONFIG_RESET_ZYNQMP) += reset-zynqmp.o
diff --git a/drivers/reset/reset-zte-zx297520v3.c b/drivers/reset/reset-zte-zx297520v3.c
new file mode 100644
index 000000000000..bc2801d43d10
--- /dev/null
+++ b/drivers/reset/reset-zte-zx297520v3.c
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Stefan D?singer
+ */
+#include <dt-bindings/reset/zte,zx297520v3-reset.h>
+
+#include <linux/bits.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/reset-controller.h>
+#include <linux/types.h>
+
+/* Most devices on the zx297520v3 SoC have one reset bit per clock line. As a rule of thumb, the
+ * lower bit disconnects the device from the bus, similarly to turning off PCLK - registers read 0
+ * or hang indefinitely. Unlike PCLK, this reset may have a lingering effect after deasserting.
+ * E.g. timers will be disabled, but retain their counter value.
+ *
+ * The other bit resets the actual device registers.
+ *
+ * For some devices, e.g. GMAC, both reset bits behave in the same way: They disconnect the device
+ * and registers will have their default state after deasserting. For devices that have two reset
+ * bits, both need to be deasserted for the device to function.
+ */
+struct zte_reset_reg {
+	u32 mask;
+	u16 reg;
+};
+
+struct zte_reset_data {
+	const struct zte_reset_reg *resets;
+	unsigned int num;
+};
+
+struct zte_reset {
+	struct reset_controller_dev rcdev;
+	struct regmap *map;
+	const struct zte_reset_reg *resets;
+};
+
+static inline struct zte_reset *to_zte_reset(struct reset_controller_dev *rcdev)
+{
+	return container_of(rcdev, struct zte_reset, rcdev);
+}
+
+static int zx29_rst_assert(struct reset_controller_dev *rcdev, unsigned long id)
+{
+	struct zte_reset *rst = to_zte_reset(rcdev);
+
+	return regmap_clear_bits(rst->map, rst->resets[id].reg, rst->resets[id].mask);
+}
+
+static int zx29_rst_deassert(struct reset_controller_dev *rcdev, unsigned long id)
+{
+	struct zte_reset *rst = to_zte_reset(rcdev);
+
+	return regmap_set_bits(rst->map, rst->resets[id].reg, rst->resets[id].mask);
+}
+
+static int zx29_rst_status(struct reset_controller_dev *rcdev, unsigned long id)
+{
+	struct zte_reset *rst = to_zte_reset(rcdev);
+	int res;
+
+	/* Devices with two reset bits need both deasserted to work. So only report them as
+	 * deasserted if both bits are set.
+	 *
+	 * assert()/deassert() will always clear/set both. The only reason a device might be in a
+	 * hybrid state is an unexpected handover state from the bootloader.
+	 */
+	res = regmap_test_bits(rst->map, rst->resets[id].reg, rst->resets[id].mask);
+	if (res < 0)
+		return res;
+
+	return !res;
+}
+
+static const struct reset_control_ops zx29_rst_ops = {
+	.assert		= zx29_rst_assert,
+	.deassert	= zx29_rst_deassert,
+	.status		= zx29_rst_status,
+};
+
+static const struct zte_reset_reg zx297520v3_top_resets[] = {
+	/* This bit is set by ZTE's cpko.ko blob, it looks like a reset bit for the LTE DSP
+	 * coprocessor. Clocks for it are in matrixcrm.
+	 */
+	[ZX297520V3_ZSP_RESET]       = { .reg = 0x13c, .mask = BIT(0)            },
+
+	[ZX297520V3_UART0_RESET]     = { .reg =  0x78, .mask = BIT(6)  | BIT(7)  },
+	[ZX297520V3_I2C0_RESET]      = { .reg =  0x74, .mask = BIT(8)  | BIT(9)  },
+	/* Only one reset. Bit 5 is settable but does not do anything observable */
+	[ZX297520V3_RTC_RESET]       = { .reg =  0x74, .mask = BIT(4)            },
+	[ZX297520V3_TIMER_T08_RESET] = { .reg =  0x78, .mask = BIT(4)  | BIT(5)  },
+	[ZX297520V3_TIMER_T09_RESET] = { .reg =  0x78, .mask = BIT(2)  | BIT(3)  },
+	/* Only one reset. Bit 0 is settable but does not do anything observable */
+	[ZX297520V3_PMM_RESET]       = { .reg =  0x74, .mask = BIT(1)            },
+
+	/* I haven't found any clocks for GPIO. It probably wouldn't make much
+	 * sense anyway. Only one reset bit per controller.
+	 */
+	[ZX297520V3_GPIO_RESET]      = { .reg =  0x74, .mask = BIT(3)            },
+	[ZX297520V3_GPIO8_RESET]     = { .reg =  0x74, .mask = BIT(2)            },
+
+	[ZX297520V3_TIMER_T12_RESET] = { .reg =  0x74, .mask = BIT(6)  | BIT(7)  },
+	[ZX297520V3_TIMER_T13_RESET] = { .reg =  0x7c, .mask = BIT(0)  | BIT(1)  },
+	[ZX297520V3_TIMER_T14_RESET] = { .reg =  0x7c, .mask = BIT(2)  | BIT(3)  },
+	[ZX297520V3_TIMER_T15_RESET] = { .reg =  0x74, .mask = BIT(10) | BIT(11) },
+	[ZX297520V3_TIMER_T16_RESET] = { .reg =  0x7c, .mask = BIT(4)  | BIT(5)  },
+	[ZX297520V3_TIMER_T17_RESET] = { .reg = 0x12c, .mask = BIT(0)  | BIT(1)  },
+	[ZX297520V3_WDT_T18_RESET]   = { .reg =  0x74, .mask = BIT(12) | BIT(13) },
+	[ZX297520V3_USIM1_RESET]     = { .reg =  0x74, .mask = BIT(14) | BIT(15) },
+	[ZX297520V3_AHB_RESET]       = { .reg =  0x70, .mask = BIT(0)  | BIT(1)  },
+
+	/* USB reset. 0x84 returns the USB device status (0x1 for HSIC up, 0x2 for USB up, but
+	 * all 3 bits (PCLK, WCLK, PHY) need to be deasserted for the device to report ready.
+	 * Thus polling the status is the responsibility of the USB PHY driver.
+	 */
+	[ZX297520V3_USB_RESET]       = { .reg =  0x80, .mask = BIT(4) | BIT(5)   },
+	[ZX297520V3_HSIC_RESET]      = { .reg =  0x80, .mask = BIT(1) | BIT(2)   },
+};
+
+static const struct zte_reset_data zx297520v3_topreset_data = {
+	.resets = zx297520v3_top_resets,
+	.num = ARRAY_SIZE(zx297520v3_top_resets),
+};
+
+static const struct zte_reset_reg zx297520v3_matrix_resets[] = {
+	[ZX297520V3_CPU_RESET]       = { .reg =  0x28, .mask = BIT(1)            },
+	[ZX297520V3_DDR_CTRL_RESET]  = { .reg = 0x100, .mask = BIT(10) | BIT(11) },
+	[ZX297520V3_EDCP_RESET]      = { .reg =  0x68, .mask = BIT(0)            },
+	[ZX297520V3_SD0_RESET]       = { .reg =  0x58, .mask = BIT(1)            },
+	[ZX297520V3_SD1_RESET]       = { .reg =  0x58, .mask = BIT(0)            },
+	[ZX297520V3_NAND_RESET]      = { .reg =  0x58, .mask = BIT(4)            },
+	[ZX297520V3_PDCFG_RESET]     = { .reg =  0x94, .mask = BIT(20)           },
+	[ZX297520V3_SSC_RESET]       = { .reg =  0x94, .mask = BIT(24)           },
+	[ZX297520V3_GMAC_RESET]      = { .reg = 0x114, .mask = BIT(0)  | BIT(1)  },
+	[ZX297520V3_VOU_RESET]       = { .reg = 0x16c, .mask = BIT(0)            },
+	[ZX297520V3_LSP_RESET]       = { .reg =  0x80, .mask = BIT(0)            },
+};
+
+static const struct zte_reset_data zx297520v3_matrixreset_data = {
+	.resets = zx297520v3_matrix_resets,
+	.num = ARRAY_SIZE(zx297520v3_matrix_resets),
+};
+
+static const struct zte_reset_reg zx297520v3_lsp_resets[] = {
+	[ZX297520V3_TIMER_L1_RESET]  = { .reg = 0x04,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_WDT_L2_RESET]    = { .reg = 0x08,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_WDT_L3_RESET]    = { .reg = 0x0c,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_PWM_RESET]       = { .reg = 0x10,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_I2S0_RESET]      = { .reg = 0x14,  .mask = BIT(8)  | BIT(9)  },
+	/* 0x18: Not writeable */
+	[ZX297520V3_I2S1_RESET]      = { .reg = 0x1c,  .mask = BIT(8)  | BIT(9)  },
+	/* 0x20: Not writeable */
+	[ZX297520V3_QSPI_RESET]      = { .reg = 0x24,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_UART1_RESET]     = { .reg = 0x28,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_I2C1_RESET]      = { .reg = 0x2c,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_SPI0_RESET]      = { .reg = 0x30,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_TIMER_LB_RESET]  = { .reg = 0x34,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_TIMER_LC_RESET]  = { .reg = 0x38,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_UART2_RESET]     = { .reg = 0x3c,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_WDT_LE_RESET]    = { .reg = 0x40,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_TIMER_LF_RESET]  = { .reg = 0x44,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_SPI1_RESET]      = { .reg = 0x48,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_TIMER_L11_RESET] = { .reg = 0x4c,  .mask = BIT(8)  | BIT(9)  },
+	[ZX297520V3_TDM_RESET]       = { .reg = 0x50,  .mask = BIT(8)  | BIT(9)  },
+};
+
+static const struct zte_reset_data zx297520v3_lspreset_data = {
+	.resets = zx297520v3_lsp_resets,
+	.num = ARRAY_SIZE(zx297520v3_lsp_resets),
+};
+
+static int reset_zx297520v3_probe(struct platform_device *pdev)
+{
+	const struct platform_device_id *id = platform_get_device_id(pdev);
+	struct device *dev = &pdev->dev;
+	struct device_node *of_node = dev->parent->of_node;
+	const struct zte_reset_data *data;
+	struct zte_reset *rst;
+
+	if (!id)
+		return -ENODEV;
+	data = (const struct zte_reset_data *)id->driver_data;
+
+	rst = devm_kzalloc(dev, sizeof(*rst), GFP_KERNEL);
+	if (!rst)
+		return -ENOMEM;
+
+	rst->resets = data->resets;
+	rst->rcdev.owner = THIS_MODULE;
+	rst->rcdev.nr_resets = data->num;
+	rst->rcdev.ops = &zx29_rst_ops;
+	rst->rcdev.of_node = of_node;
+	rst->rcdev.dev = dev;
+
+	rst->map = device_node_to_regmap(of_node);
+	if (IS_ERR(rst->map))
+		return dev_err_probe(dev, PTR_ERR(rst->map), "Cannot get parent syscon regmap\n");
+
+	return devm_reset_controller_register(dev, &rst->rcdev);
+}
+
+static const struct platform_device_id reset_zx297520v3_ids[] = {
+	{
+		.name = "zx297520v3-topreset",
+		.driver_data = (kernel_ulong_t)&zx297520v3_topreset_data,
+	},
+	{
+		.name = "zx297520v3-matrixreset",
+		.driver_data = (kernel_ulong_t)&zx297520v3_matrixreset_data,
+	},
+	{
+		.name = "zx297520v3-lspreset",
+		.driver_data = (kernel_ulong_t)&zx297520v3_lspreset_data,
+	},
+	{ }
+};
+MODULE_DEVICE_TABLE(platform, reset_zx297520v3_ids);
+
+static struct platform_driver reset_zx297520v3 = {
+	.probe = reset_zx297520v3_probe,
+	.driver = {
+		.name = "reset-zx297520v3",
+	},
+	.id_table = reset_zx297520v3_ids,
+};
+module_platform_driver(reset_zx297520v3);
+
+MODULE_AUTHOR("Stefan D?singer <stefandoesinger at gmail.com>");
+MODULE_DESCRIPTION("ZTE zx297520v3 reset driver");
+MODULE_LICENSE("GPL");

-- 
2.54.0



From stefandoesinger at gmail.com  Sun Aug  2 13:33:44 2026
From: stefandoesinger at gmail.com (=?utf-8?q?Stefan_D=C3=B6singer?=)
Date: Sun, 02 Aug 2026 23:33:44 +0300
Subject: [PATCH v9 12/12] ARM: dts: zte: Declare zx297520v3 CRM device
 nodes
In-Reply-To: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
Message-ID: <20260802-zx29clk-v9-12-d05530d85d28@gmail.com>

This makes use of the driver added in the previous patches. It wires up
the uart clocks and resets and allows getting rid of the placeholder
uartclk node.

Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

---

Version 9:
Sort new nodes by mmio address
rename syscon-reboot to "reboot"
adjust to changes in bindings

Version 7:
Add phy nodes to topcrm
Remove syscon from matrixclk again
Adapt remove now-removed extra clk inputs from matrix
Add reset to LSP
osc26m -> clock-26m for the fixed rate clocks, likewise for 32k

Version 6:
Add syscon to matrixclk

Version 5:
Re-name from *clk to *crm
Add the syscon-reboot node here because the binding requires it
Re-add accidentally dropped uart2 IRQ
---
 arch/arm/boot/dts/zte/zx297520v3.dtsi | 81 +++++++++++++++++++++++++++++++----
 1 file changed, 73 insertions(+), 8 deletions(-)

diff --git a/arch/arm/boot/dts/zte/zx297520v3.dtsi b/arch/arm/boot/dts/zte/zx297520v3.dtsi
index a16c30a164bb..579ad45028f6 100644
--- a/arch/arm/boot/dts/zte/zx297520v3.dtsi
+++ b/arch/arm/boot/dts/zte/zx297520v3.dtsi
@@ -3,7 +3,9 @@
  * Copyright (c) 2026 Stefan D?singer <stefandoesinger at gmail.com>
  */
 
+#include <dt-bindings/clock/zte,zx297520v3-clk.h>
 #include <dt-bindings/interrupt-controller/arm-gic.h>
+#include <dt-bindings/reset/zte,zx297520v3-reset.h>
 
 / {
 	#address-cells = <1>;
@@ -20,13 +22,16 @@ cpu at 0 {
 		};
 	};
 
-	/* Base bus clock and default for the UART. It will be replaced once a clock driver has
-	 * been added.
-	 */
-	uartclk: uartclk-26000000 {
-		#clock-cells = <0>;
+	osc26m: clock-26m {
 		compatible = "fixed-clock";
 		clock-frequency = <26000000>;
+		#clock-cells = <0>;
+	};
+
+	osc32k: clock-32k {
+		compatible = "fixed-clock";
+		clock-frequency = <32768>;
+		#clock-cells = <0>;
 	};
 
 	timer {
@@ -75,18 +80,77 @@ uart0: serial at 131000 {
 			arm,primecell-periphid = <0x0018c011>;
 			reg = <0x00131000 0x1000>;
 			interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>;
-			clocks = <&uartclk>, <&uartclk>;
+			clocks = <&topcrm ZX297520V3_UART0_WCLK>, <&topcrm ZX297520V3_UART0_PCLK>;
 			clock-names = "uartclk", "apb_pclk";
+			resets = <&topcrm ZX297520V3_UART0_RESET>;
 			status = "disabled";
 		};
 
+		topcrm: clock-controller at 13b000 {
+			compatible = "zte,zx297520v3-topcrm", "syscon";
+			reg = <0x0013b000 0x400>;
+			clocks = <&osc26m>, <&osc32k>;
+			clock-names = "osc26m", "osc32k";
+			interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>,
+				     <GIC_SPI 43 IRQ_TYPE_EDGE_RISING>,
+				     <GIC_SPI 45 IRQ_TYPE_EDGE_RISING>,
+				     <GIC_SPI 46 IRQ_TYPE_EDGE_RISING>;
+			interrupt-names = "usb-up", "usb-down", "hsic-up", "hsic-down";
+			#clock-cells = <1>;
+			#phy-cells = <1>;
+			#reset-cells = <1>;
+
+			reboot {
+				compatible = "syscon-reboot";
+				offset = <0x0>;
+				value = <0x1>;
+				mask = <0x1>;
+			};
+		};
+
+		matrixcrm: clock-controller at 1306000 {
+			compatible = "zte,zx297520v3-matrixcrm";
+			reg = <0x01306000 0x400>;
+			clocks = <&osc26m>, <&osc32k>, <&topcrm ZX297520V3_AHB_PCLK>,
+				 <&topcrm ZX297520V3_MPLL>, <&topcrm ZX297520V3_MPLL_D5>,
+				 <&topcrm ZX297520V3_DPLL>, <&topcrm ZX297520V3_GPLL>,
+				 <&topcrm ZX297520V3_GPLL_D2>, <&topcrm ZX297520V3_GATED_OSC26M>;
+			clock-names = "osc26m", "osc32k", "ahb", "mpll", "mpll-d5", "dpll",
+				      "gpll", "gpll-d2", "wclk-osc26m";
+			#clock-cells = <1>;
+			#hwlock-cells = <1>;
+			#reset-cells = <1>;
+		};
+
+		lspcrm: clock-controller at 1400000 {
+			compatible = "zte,zx297520v3-lspcrm";
+			reg = <0x01400000 0x100>;
+			#clock-cells = <1>;
+			#reset-cells = <1>;
+
+			clocks = <&matrixcrm ZX297520V3_LSP_MPLL_D5_WCLK>,
+				 <&matrixcrm ZX297520V3_LSP_MPLL_D4_WCLK>,
+				 <&matrixcrm ZX297520V3_LSP_MPLL_D6_WCLK>,
+				 <&matrixcrm ZX297520V3_LSP_MPLL_D8_WCLK>,
+				 <&matrixcrm ZX297520V3_LSP_MPLL_D12_WCLK>,
+				 <&matrixcrm ZX297520V3_LSP_OSC26M_WCLK>,
+				 <&matrixcrm ZX297520V3_LSP_OSC32K_WCLK>,
+				 <&matrixcrm ZX297520V3_LSP_PCLK>,
+				 <&matrixcrm ZX297520V3_LSP_TDM_WCLK>,
+				 <&matrixcrm ZX297520V3_LSP_DPLL_D4_WCLK>;
+			clock-names = "mpll-d5", "mpll-d4", "mpll-d6", "mpll-d8", "mpll-d12",
+				      "osc26m", "osc32k", "pclk", "tdm-wclk", "dpll-d4";
+			resets = <&matrixcrm ZX297520V3_LSP_RESET>;
+		};
+
 		uart1: serial at 1408000 {
 			compatible = "arm,pl011", "arm,primecell";
 			arm,primecell-periphid = <0x0018c011>;
 			reg = <0x01408000 0x1000>;
 			interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
-			clocks = <&uartclk>, <&uartclk>;
+			clocks = <&lspcrm ZX297520V3_UART1_WCLK>, <&lspcrm ZX297520V3_UART1_PCLK>;
 			clock-names = "uartclk", "apb_pclk";
+			resets = <&lspcrm ZX297520V3_UART1_RESET>;
 			status = "disabled";
 		};
 
@@ -95,8 +159,9 @@ uart2: serial at 140d000 {
 			arm,primecell-periphid = <0x0018c011>;
 			reg = <0x0140d000 0x1000>;
 			interrupts = <GIC_SPI 2 IRQ_TYPE_LEVEL_HIGH>;
-			clocks = <&uartclk>, <&uartclk>;
+			clocks = <&lspcrm ZX297520V3_UART2_WCLK>, <&lspcrm ZX297520V3_UART2_PCLK>;
 			clock-names = "uartclk", "apb_pclk";
+			resets = <&lspcrm ZX297520V3_UART2_RESET>;
 			status = "disabled";
 		};
 	};

-- 
2.54.0



From sashiko-bot at kernel.org  Sun Aug  2 13:40:02 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 20:40:02 +0000
Subject: [PATCH v9 03/12] dt-bindings: clk: zte: Add zx297520v3 LSP
 clock and reset controller
In-Reply-To: <20260802-zx29clk-v9-3-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-3-d05530d85d28@gmail.com>
Message-ID: <20260802204003.216441F00A3D@smtp.kernel.org>

> The clock controller of the zx297520v3 Low Speed Peripherals is
> relatively clean. One register per device with gates, muxes and resets
> and for some devices a divider. There are even bits in the matrix
> controller to control propagation of clock lines down to LSP.
> 
> The clocks are sorted by register address and I am convinced that the
> device list is complete. There are, however, a few more registers that
> are likely extra dividers for TDM and I2S devices.
> 
> Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski at oss.qualcomm.com>
> Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=3



From sashiko-bot at kernel.org  Sun Aug  2 13:42:37 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 20:42:37 +0000
Subject: [PATCH v9 02/12] dt-bindings: clk: zte: Add zx297520v3 matrix
 clock and reset controller
In-Reply-To: <20260802-zx29clk-v9-2-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-2-d05530d85d28@gmail.com>
Message-ID: <20260802204238.1AEB51F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Overlapping macro definitions in shared dt-bindings headers create a naming collision risk.
--

--- Patch [2]: [PATCH v9 02/12] dt-bindings: clk: zte: Add zx297520v3 matrix clock and reset controller ---
commit 504f3c5393f4a8144508453d38c306eb0f25c3f4
Author: Stefan D?singer <stefandoesinger at gmail.com>

dt-bindings: clk: zte: Add zx297520v3 matrix clock and reset controller

This commit adds device tree bindings for the zx297520v3 matrix clock and
reset controller. It introduces new macros for clocks and resets in the
existing zte,zx297520v3 headers to support the matrixcrm block.

> diff --git a/include/dt-bindings/clock/zte,zx297520v3-clk.h b/include/dt-bindings/clock/zte,zx297520v3-clk.h
> index 3aed94ccc26fe..a8477e9b2a149 100644
> --- a/include/dt-bindings/clock/zte,zx297520v3-clk.h
> +++ b/include/dt-bindings/clock/zte,zx297520v3-clk.h
> @@ -63,4 +63,45 @@
>  #define ZX297520V3_HSIC_WCLK			54
>  #define ZX297520V3_HSIC_PCLK			55
>  
> +#define ZX297520V3_SYS_TIMER_WCLK		0
> +#define ZX297520V3_CPU_WCLK			1

[Severity: Medium]
Could this naming convention lead to silent misconfigurations in device trees?

These new matrixcrm macros share the exact same ZX297520V3_ prefix as the
existing topcrm macros in this header, and their indices start over at 0.

Since both sets of macros resolve to valid integers within the same namespace,
accidentally referencing a topcrm macro instead of a matrixcrm macro in a
device tree will not trigger a compiler error, making hardware misconfigurations
difficult to debug.

[ ... ]

> diff --git a/include/dt-bindings/reset/zte,zx297520v3-reset.h b/include/dt-bindings/reset/zte,zx297520v3-reset.h
> index f60aaccf0f4d7..d79fa601afa68 100644
> --- a/include/dt-bindings/reset/zte,zx297520v3-reset.h
> +++ b/include/dt-bindings/reset/zte,zx297520v3-reset.h
> @@ -27,4 +27,16 @@
>  #define ZX297520V3_USB_RESET			18
>  #define ZX297520V3_HSIC_RESET			19
>  
> +#define ZX297520V3_CPU_RESET			0
> +#define ZX297520V3_DDR_CTRL_RESET		1

[Severity: Medium]
Does this carry the same namespace collision risk as the clock macros above?

Since the matrixcrm reset indices also start at 0 and use the identical
ZX297520V3_ prefix as the existing topcrm reset definitions, there is no built-in
protection against mixing them up when writing the device tree.

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=2


From sashiko-bot at kernel.org  Sun Aug  2 13:42:48 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 20:42:48 +0000
Subject: [PATCH v9 01/12] dt-bindings: clk: zte: Add zx297520v3 top
 clock and reset controller
In-Reply-To: <20260802-zx29clk-v9-1-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-1-d05530d85d28@gmail.com>
Message-ID: <20260802204249.3716E1F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The schema omits the `resets` property while restricting additional properties, which will cause DT validation failures.
--

--- Patch [1]: [PATCH v9 01/12] dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller ---
commit aac89094dd71d6499e06c62f3e14a6df6d5efac5
Author: Stefan D?singer <stefandoesinger at gmail.com>

dt-bindings: clk: zte: Add zx297520v3 top clock and reset controller

This commit adds the device tree binding schema for the ZTE zx297520v3 top
clock and reset controller. It defines clocks, resets, and PHY properties
for the topcrm node, as well as a syscon reboot child node.

> diff --git a/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
> new file mode 100644
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
[ ... ]
> +properties:
> +  compatible:
> +    items:
> +      - const: zte,zx297520v3-topcrm
> +      - const: syscon
[ ... ]
> +  "#reset-cells":
> +    const: 1
> +
> +  reboot:
> +    type: object
> +    $ref: /schemas/power/reset/syscon-reboot.yaml#
> +    unevaluatedProperties: false
> +    description:
> +      Reboot method for the SoC.
> +
> +required:
> +  - compatible
> +  - reg
> +  - clocks
> +  - clock-names
> +  - interrupts
> +  - interrupt-names
> +  - reboot
> +  - "#clock-cells"
> +  - "#phy-cells"
> +  - "#reset-cells"
> +
> +additionalProperties: false

[Severity: Medium]
Should the resets property be explicitly added to the properties block?

The commit message mentions a self-referencing reset property being used on
this node, but because additionalProperties is set to false, omitting resets
from the schema will cause dtbs_check to fail when validating device trees
that include it.

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=1


From sashiko-bot at kernel.org  Sun Aug  2 13:45:16 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 20:45:16 +0000
Subject: [PATCH v9 04/12] mfd: zx297520v3: Add a clock and reset MFD
 driver
In-Reply-To: <20260802-zx29clk-v9-4-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-4-d05530d85d28@gmail.com>
Message-ID: <20260802204517.5126D1F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Low] Direct cast from `const void *` to an `enum` triggers a `-Wpointer-to-int-cast` compiler warning on 64-bit architectures.
- [Low] The Kconfig symbol `MFD_ZTE_ZX297520V3_CRM` lacks an architecture-specific dependency, unconditionally exposing a highly SoC-specific driver to all architectures.
- [Low] The enum `zx297520v3_mfd_type` includes the string 'mfd', violating the MFD subsystem naming conventions.
--

--- Patch [4]: [PATCH v9 04/12] mfd: zx297520v3: Add a clock and reset MFD driver ---
commit 39c5ccee8d6ebff3fee809871f3a8191107ab75b
Author: Stefan D?singer <stefandoesinger at gmail.com>

mfd: zx297520v3: Add a clock and reset MFD driver

This driver registers child devices for the zx297520v3 clock and reset
controllers. The clk-zx297520v3 and reset-zte-zx297520v3 submitted in
the next patches will drive the respective functionalities.

> diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
> index 35f6e9b76d056..c0c7e83487f40 100644
> --- a/drivers/mfd/Kconfig
> +++ b/drivers/mfd/Kconfig
[ ... ]
> @@ -2572,5 +2572,16 @@ config MFD_MAX7360
>  	  additional drivers must be enabled in order to use the functionality
>  	  of the device.
>  
> +config MFD_ZTE_ZX297520V3_CRM
> +	tristate "ZTE zx297520v3 Clock and Reset Manager"
> +	select MFD_CORE
> +	default SOC_ZX297520V3

[Severity: Low]
Should this include an architecture dependency, such as
depends on ARCH_ZTE || COMPILE_TEST? Without it, this highly SoC-specific
driver will be presented in the configuration options for all architectures.

> +	help
> +	  Say yes here to enable the driver for the ZTE zx297520v3 clock and
> +	  reset manager MFD driver. This driver provides the host device for
> +	  the clock and reset drivers and is required to boot the SoC. You
> +	  will also need to enable CLK_ZTE_ZX297520V3 and RESET_ZTE_ZX297520V3
> +	  to build the actual clock and reset child drivers.
> +
>  endmenu
>  endif
> diff --git a/drivers/mfd/zte-zx297520v3-crm.c b/drivers/mfd/zte-zx297520v3-crm.c
> new file mode 100644
> index 0000000000000..37cd0c14680eb
> --- /dev/null
> +++ b/drivers/mfd/zte-zx297520v3-crm.c
> @@ -0,0 +1,117 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (C) 2026 Stefan D?singer
> + */
> +
> +#include <linux/clk.h>
> +#include <linux/err.h>
> +#include <linux/errno.h>
> +#include <linux/mfd/core.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/platform_device.h>
> +#include <linux/reset.h>
> +
> +enum zx297520v3_mfd_type {
> +	ZX297520V3_MFD_INVALID = 0,
> +	ZX297520V3_MFD_TOPCRM,
> +	ZX297520V3_MFD_MATRIXCRM,
> +	ZX297520V3_MFD_LSPCRM,
> +};

[Severity: Low]
This isn't a bug, but does including the string mfd in the enum name violate
the MFD subsystem naming conventions? The guidelines suggest avoiding
hard-coding implementation details like mfd in driver, struct, or device
names.

[ ... ]

> +static int zx297520v3_crm_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	enum zx297520v3_mfd_type type;
> +	const struct mfd_cell *cells;
> +	struct reset_control *rst;
> +	unsigned int num_cells;
> +	struct clk *pclk;
> +
> +	type = (enum zx297520v3_mfd_type)of_device_get_match_data(dev);

[Severity: Low]
Could this direct cast from a const void * pointer to an enum trigger a
-Wpointer-to-int-cast compiler warning on 64-bit architectures? Often an
intermediate cast to uintptr_t or kernel_ulong_t is used to safely preserve
the size before casting to the enum type.

> +	switch (type) {
> +	case ZX297520V3_MFD_TOPCRM:
> +		cells = zx297520v3_topcrm_cells;
> +		num_cells = ARRAY_SIZE(zx297520v3_topcrm_cells);
> +		break;

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=4


From sashiko-bot at kernel.org  Sun Aug  2 13:49:46 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 20:49:46 +0000
Subject: [PATCH v9 05/12] clk: zte: Add Clock registration
 infrastructure
In-Reply-To: <20260802-zx29clk-v9-5-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-5-d05530d85d28@gmail.com>
Message-ID: <20260802204947.442E41F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Incomplete refactoring of clock exports causing missing const, dead code, and unsafe aliasing for sparse DT IDs
--

--- Patch [5]: [PATCH v9 05/12] clk: zte: Add Clock registration infrastructure ---
commit c095bc91f84da44c659bbf8482a8f9e0d8e6f977
Author: Stefan D?singer <stefandoesinger at gmail.com>

clk: zte: Add Clock registration infrastructure

This commit introduces the common clock registration infrastructure for ZTE
SoCs. It implements clock validation, initialization, and MFD child
integration.

> diff --git a/drivers/clk/zte/clk-zx.c b/drivers/clk/zte/clk-zx.c
> --- /dev/null
> +++ b/drivers/clk/zte/clk-zx.c
[ ... ]
> +	exports = devm_kzalloc(dev, struct_size(exports, hws, data->num_exports), GFP_KERNEL);
> +	if (!exports)
> +		return -ENOMEM;
> +	exports->num = data->num_exports;
> +
> +	for (i = 0; i < data->num_exports; ++i) {
> +		if (data->exports[i] >= data->num_clocks)
> +			return dev_err_probe(dev, -EINVAL,
> +					     "Export %u points to out of range clock %u\n",
> +					     i, data->exports[i]);
> +
> +		exports->hws[i] = clocks[data->exports[i]];

[Severity: Medium]
If the DT binding IDs are sparse, won't the gaps in the static exports array
default to 0? This would cause the uninitialized gaps to improperly alias to
the first internal clock (clocks[0]) instead of returning an error when
consumer drivers request an unused clock ID.

> +	}
> +
> +	devm_kfree(dev, clocks);
> +
> +	return devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, exports);
> +}
[ ... ]
> diff --git a/drivers/clk/zte/clk-zx.h b/drivers/clk/zte/clk-zx.h
> --- /dev/null
> +++ b/drivers/clk/zte/clk-zx.h
[ ... ]
> +struct clk_hw *zx_clk_register_gate(struct device *dev, struct regmap *regmap,
> +				    const struct zx_gate_desc *desc,
> +				    struct clk_hw * const *clocks);
> +
> +struct zx_clk_export {
> +	unsigned int priv;
> +	unsigned int binding;
> +};

[Severity: Medium]
Is struct zx_clk_export still needed? It appears to be unused dead code,
perhaps left over from an earlier iteration of the clock exports refactoring.

> +
> +struct zx_clk_data {
> +	int (*init)(struct regmap *map);
> +	const struct zx_clock *clocks;
> +	unsigned int num_clocks;
> +	unsigned int *exports;

[Severity: Medium]
Could the exports array pointer be marked as const? Without the const
qualifier, it prevents the static clock data from being placed in .rodata.

> +	unsigned int num_exports;
> +};
> +
> +int zx_clk_common_probe(struct device *dev, struct device_node *of_node,
> +			const struct zx_clk_data *data);
> +
> +#endif /* __DRV_CLK_ZX_H */

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=5


From sashiko-bot at kernel.org  Sun Aug  2 13:52:45 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 20:52:45 +0000
Subject: [PATCH v9 07/12] clk: zte: Add zx PLL support infrastructure
In-Reply-To: <20260802-zx29clk-v9-7-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-7-d05530d85d28@gmail.com>
Message-ID: <20260802205245.95DFF1F000E9@smtp.kernel.org>

> I am guessing how much of this is reusable among other zx chips or even
> differently named ZTE platforms (if there are any). From reading the old
> zx2967 code, I think the PLL code would be reusable there, maybe with
> platform-specific bitmasks but otherwise the same logic.
> 
> Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=7



From sashiko-bot at kernel.org  Sun Aug  2 13:55:48 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 20:55:48 +0000
Subject: [PATCH v9 06/12] clk: zte: Add regmap-based clocks
In-Reply-To: <20260802-zx29clk-v9-6-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-6-d05530d85d28@gmail.com>
Message-ID: <20260802205549.57B331F000E9@smtp.kernel.org>

> This is based on meson/clk-regmap.c, although slightly simplified. I
> have kept the copyright lines at the top of the file to indicate its
> origin.
> 
> I see that numerous clock drivers have their own incarnation of regmap-
> based mux/div/gate clocks. If there is any version of it that is likely
> to be elevated to shared code like clk-gate.c I'll copy that and try to
> use it as unmodified as possible.
> 
> Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=6



From sashiko-bot at kernel.org  Sun Aug  2 13:59:21 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 20:59:21 +0000
Subject: [PATCH v9 10/12] clk: zte: Introduce a driver for zx297520v3
 LSP clocks
In-Reply-To: <20260802-zx29clk-v9-10-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-10-d05530d85d28@gmail.com>
Message-ID: <20260802205922.554AA1F000E9@smtp.kernel.org>

> "LSP" is ZTE's term for this part of the SoC, I suspect it stands for
> "low speed peripherals". The main UART is here, together with the flash
> controller and more surplus proprietary timers.
> 
> It also has one more I2C controller that supposedly connects to a
> battery charger, SPI for displays and I2S for analog telephones. The
> boards I have don't have any of these components though.
> 
> Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=10



From sashiko-bot at kernel.org  Sun Aug  2 13:59:48 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 20:59:48 +0000
Subject: [PATCH v9 08/12] clk: zte: Introduce a driver for zx297520v3
 top clocks
In-Reply-To: <20260802-zx29clk-v9-8-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-8-d05530d85d28@gmail.com>
Message-ID: <20260802205949.700C31F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Using `const` struct variables to initialize fields of a static array causes a build failure.
--

--- Patch [8]: [PATCH v9 08/12] clk: zte: Introduce a driver for zx297520v3 top clocks ---
commit 0a378a36878d451d9e58d093344b7ead86ae6042
Author: Stefan D?singer <stefandoesinger at gmail.com>
clk: zte: Introduce a driver for zx297520v3 top clocks

This commit introduces a new clock driver for the ZTE zx297520v3 SoC's
top and matrix controllers. It adds definitions for PLLs, fixed dividers,
muxes, and gates used by core devices on the board.

> diff --git a/drivers/clk/zte/clk-zx297520v3.c b/drivers/clk/zte/clk-zx297520v3.c
> new file mode 100644
> index 0000000000000..f9915582f261a
> --- /dev/null
> +++ b/drivers/clk/zte/clk-zx297520v3.c
[ ... ]
> +/* Used for gates where we don't know the parent input(s). Assume general oscillator */
> +static const struct zx_parent_desc clk_unknown = PARENT_FW("osc26m");
> +
> +/* For clocks that are known to use the ungated osc26m input */
> +static const struct zx_parent_desc clk_main[] = {
> +	PARENT_FW("osc26m"),
> +};
[ ... ]
> +static const struct zx_clock zx297520v3_top_clocks[] = {
[ ... ]
> +	/* osc26m division for clk_outX */
> +	[ZX_CLK_OSC26M_D2]         = FIXED_DIV("osc26m-d2", clk_main[0], 2),

[Severity: Low]
Does this code cause a compiler error about initializer elements not being
constant?

In C, const qualified struct variables and array elements like clk_main[0]
are not considered constant expressions. Since zx297520v3_top_clocks has static
storage duration, using them here might result in a build failure.

[ ... ]
> +	/*
> +	 * How does this RTC work? I don't know, the ZTE kernel does not talk to it. The actual RTC
> +	 * is on the I2C-connected PMIC.
> +	 */
> +	[ZX_CLK_RTC_WCLK]         = GATE("rtc-wclk", clk_unknown, 0x54, 1, 0),

[Severity: Low]
Is this initialization also affected by the same "initializer element is
not constant" compiler error when using clk_unknown?

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=8


From sashiko-bot at kernel.org  Sun Aug  2 14:03:36 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 21:03:36 +0000
Subject: [PATCH v9 12/12] ARM: dts: zte: Declare zx297520v3 CRM device
 nodes
In-Reply-To: <20260802-zx29clk-v9-12-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-12-d05530d85d28@gmail.com>
Message-ID: <20260802210337.3A3E31F000E9@smtp.kernel.org>

> This makes use of the driver added in the previous patches. It wires up
> the uart clocks and resets and allows getting rid of the placeholder
> uartclk node.
> 
> Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=12



From sashiko-bot at kernel.org  Sun Aug  2 14:04:47 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 21:04:47 +0000
Subject: [PATCH v9 11/12] reset: zte: Add a zx297520v3 reset driver
In-Reply-To: <20260802-zx29clk-v9-11-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-11-d05530d85d28@gmail.com>
Message-ID: <20260802210448.47CC01F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Mismatch between v9 changelog and driver code regarding removed resets.
--

--- Patch [11]: [PATCH v9 11/12] reset: zte: Add a zx297520v3 reset driver ---
commit eead351b367c470bc2a440f3b28b22580a7eb179
Author: Stefan D?singer <stefandoesinger at gmail.com>

reset: zte: Add a zx297520v3 reset driver

This commit adds a new reset driver for the ZTE zx297520v3 SoCs. It manages
the resets for MFD child devices created by the zte-zx297520v3-crm driver.

> diff --git a/drivers/reset/reset-zte-zx297520v3.c b/drivers/reset/reset-zte-zx297520v3.c
> new file mode 100644
> index 0000000000000..bc2801d43d10d
> --- /dev/null
> +++ b/drivers/reset/reset-zte-zx297520v3.c

[ ... ]

> +static const struct zte_reset_reg zx297520v3_top_resets[] = {

[ ... ]

> +	/* USB reset. 0x84 returns the USB device status (0x1 for HSIC up, 0x2 for USB up, but
> +	 * all 3 bits (PCLK, WCLK, PHY) need to be deasserted for the device to report ready.
> +	 * Thus polling the status is the responsibility of the USB PHY driver.
> +	 */
> +	[ZX297520V3_USB_RESET]       = { .reg =  0x80, .mask = BIT(4) | BIT(5)   },
> +	[ZX297520V3_HSIC_RESET]      = { .reg =  0x80, .mask = BIT(1) | BIT(2)   },
> +};

[Severity: Medium]
The v9 changelog mentions "Remove HSIC and USB PHY resets, the phy driver
will access them through the shared regmap directly", but the USB and HSIC 
resets are still defined in zx297520v3_top_resets here.

Should these definitions be removed to match the changelog and prevent
potential concurrent modifications through the reset API while the PHY driver
accesses them via regmap?

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=11


From sashiko-bot at kernel.org  Sun Aug  2 14:26:03 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Sun, 02 Aug 2026 21:26:03 +0000
Subject: [PATCH v9 09/12] clk: zte: Introduce a driver for zx297520v3
 matrix clocks
In-Reply-To: <20260802-zx29clk-v9-9-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-9-d05530d85d28@gmail.com>
Message-ID: <20260802212603.D401F1F000E9@smtp.kernel.org>

> This clock controller controls high speed devices: CPU, DMA, RAM, SDIO,
> Ethernet. The only non-clock, non-reset registers I have spotted here
> are hardware spinlocks.
> 
> Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260802-zx29clk-v9-0-d05530d85d28 at gmail.com?part=9



From chaoyi.chen at rock-chips.com  Sun Aug  2 18:38:17 2026
From: chaoyi.chen at rock-chips.com (Chaoyi Chen)
Date: Mon, 3 Aug 2026 09:38:17 +0800
Subject: [PATCH v5 1/6] drm/bridge: Implement generic USB Type-C DP HPD
 bridge
In-Reply-To: <amyp3C_yzxpKzigR@venus>
References: <20260730013349.51-1-kernel@airkyi.com>
 <20260730013349.51-2-kernel@airkyi.com> <amyp3C_yzxpKzigR@venus>
Message-ID: <c6728974-3098-4232-927f-40911f0b4e39@rock-chips.com>

Hi Sebastian,

On 7/31/2026 10:20 PM, Sebastian Reichel wrote:
> Hi,
> 
> On Thu, Jul 30, 2026 at 09:33:44AM +0800, Chaoyi Chen wrote:
>> From: Chaoyi Chen <chaoyi.chen at rock-chips.com>
>>
>> The HPD function of Type-C DP is implemented through
>> drm_connector_oob_hotplug_event(). For embedded DP, it is required
>> that the DRM connector fwnode corresponds to the Type-C port fwnode.
>>
>> To describe the relationship between the DP controller and the Type-C
>> port device, we usually using drm_bridge to build a bridge chain.
>>
>> Now several USB-C controller drivers have already implemented the DP
>> HPD bridge function provided by aux-hpd-bridge.c, it will build a DP
>> HPD bridge on USB-C connector port device.
>>
>> But this requires the USB-C controller driver to manually register the
>> HPD bridge. If the driver does not implement this feature, the bridge
>> will not be create.
>>
>> So this patch implements a generic DP HPD bridge based on
>> aux-hpd-bridge.c. It will monitor Type-C bus events, and when a
>> Type-C port device containing the DP svid is registered, it will
>> create an HPD bridge for it without the need for the USB-C controller
>> driver to implement it.
>>
>> Signed-off-by: Chaoyi Chen <chaoyi.chen at rock-chips.com>
>> Reviewed-by: Heikki Krogerus <heikki.krogerus at linux.intel.com>
>> Reviewed-by: Nicolas Frattaroli <nicolas.frattaroli at collabora.com>
>> ---
>>
>> (no changes since v5)
>>
>> Changes in v4:
>> - Scan the entire typec_bus and attempt to register the hpd bridge,
>>   so as not to miss devices that were already added during initialization.
>>
>> (no changes since v3)
>>
>> Changes in v2:
>> - Add copyright text.
>> - Remove useless goto.
>> ---
>>  drivers/gpu/drm/bridge/Kconfig                | 10 +++
>>  drivers/gpu/drm/bridge/Makefile               |  1 +
>>  .../gpu/drm/bridge/aux-hpd-typec-dp-bridge.c  | 64 +++++++++++++++++++
>>  3 files changed, 75 insertions(+)
>>  create mode 100644 drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c
>>
>> diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig
>> index 4a57d49b4c6d..9739b2a19758 100644
>> --- a/drivers/gpu/drm/bridge/Kconfig
>> +++ b/drivers/gpu/drm/bridge/Kconfig
>> @@ -30,6 +30,16 @@ config DRM_AUX_HPD_BRIDGE
>>  	  Simple bridge that terminates the bridge chain and provides HPD
>>  	  support.
>>  
>> +if DRM_AUX_HPD_BRIDGE
>> +config DRM_AUX_HPD_TYPEC_BRIDGE
>> +	tristate
>> +	depends on TYPEC || !TYPEC
>> +	default TYPEC
>> +	help
>> +	  Simple bridge that terminates the bridge chain and provides HPD
>> +	  support. It build bridge on each USB-C connector device node.
>> +endif
>> +
>>  menu "Display Interface Bridges"
>>  	depends on DRM && DRM_BRIDGE
>>  
>> diff --git a/drivers/gpu/drm/bridge/Makefile b/drivers/gpu/drm/bridge/Makefile
>> index 15cc821d85b7..d88a9e1ccc9a 100644
>> --- a/drivers/gpu/drm/bridge/Makefile
>> +++ b/drivers/gpu/drm/bridge/Makefile
>> @@ -1,6 +1,7 @@
>>  # SPDX-License-Identifier: GPL-2.0
>>  obj-$(CONFIG_DRM_AUX_BRIDGE) += aux-bridge.o
>>  obj-$(CONFIG_DRM_AUX_HPD_BRIDGE) += aux-hpd-bridge.o
>> +obj-$(CONFIG_DRM_AUX_HPD_TYPEC_BRIDGE) += aux-hpd-typec-dp-bridge.o
>>  obj-$(CONFIG_DRM_CHIPONE_ICN6211) += chipone-icn6211.o
>>  obj-$(CONFIG_DRM_CHRONTEL_CH7033) += chrontel-ch7033.o
>>  obj-$(CONFIG_DRM_CROS_EC_ANX7688) += cros-ec-anx7688.o
>> diff --git a/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c b/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c
>> new file mode 100644
>> index 000000000000..43af3ea20f20
>> --- /dev/null
>> +++ b/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c
>> @@ -0,0 +1,64 @@
>> +// SPDX-License-Identifier: GPL-2.0+
>> +/*
>> + * Copyright (C) 2026 Rockchip Electronics Co., Ltd.
>> + *
>> + * Author: Chaoyi Chen <chaoyi.chen at rock-chips.com>
>> + */
>> +#include <linux/of.h>
>> +#include <linux/usb/typec_altmode.h>
>> +#include <linux/usb/typec_dp.h>
>> +
>> +#include <drm/bridge/aux-bridge.h>
>> +
>> +static int drm_typec_bus_event(struct notifier_block *nb,
>> +			       unsigned long action, void *data)
>> +{
>> +	struct device *dev = (struct device *)data;
>> +	struct typec_altmode *alt = to_typec_altmode(dev);
>> +
>> +	if (action != BUS_NOTIFY_ADD_DEVICE)
>> +		return NOTIFY_OK;
>> +
>> +	/*
>> +	 * alt->dev.parent->parent : USB-C controller device
>> +	 * alt->dev.parent         : USB-C connector device
>> +	 */
>> +	if (is_typec_port_altmode(&alt->dev) && alt->svid == USB_TYPEC_DP_SID)
>> +		drm_dp_hpd_bridge_register(alt->dev.parent->parent,
>> +					   to_of_node(alt->dev.parent->fwnode));
> 
> So there are 3 ways to end up with duplicated hpd bridges now:
> 
> 1. TypeC controller driver already registered one manually
> 2. There is a race in drm_aux_hpd_typec_dp_bridge_module_init, if a
>    device appears between registering the notifier and looping
>    through all pre-existing devices
> 3. Reloading the aux-hpd-typec-dp-bridge module re-registers the
>    bridges
> 
> It's not a huge problem as the system works with the duplicated HPD
> bridges, but it's also quite ugly.
> 
> I think it can be trivially avoided by adding this function to
> drivers/gpu/drm/bridge/aux-hpd-bridge.c and then making use of it
> here as an additional check (code untested):
> 
> bool drm_device_has_dp_hpd_bridge(struct device *parent)
> {
> 	struct device *hpd_bridge = device_find_child_by_name(parent, "dp_hpd_bridge");
> 
> 	if (!hpd_bridge)
> 		return false;
> 
> 	put_device(hpd_bridge);
> 	return true;
> }
> EXPORT_SYMBOL_GPL(drm_dev_has_dp_hpd_bridge);
>

I think deduplication during registration is a good idea. Detecting it
during alloc seems a bit premature.

Thanks again for your suggestion. I'll try to add it in the next version.

> That would solve all of the above, but leaves one problem when this
> module races against manual device creation in a TypeC controller
> driver. That could be avoided by doing the check in
> devm_drm_dp_hpd_bridge_alloc() under a lock, but that's probably not
> worth the trouble (it would require updating all users to handle a
> new error code like -EEXIST) considering manual registration can be
> removed from all drivers anyways.
> 
> Greetings,
> 
> -- Sebastian
> 
>> +
>> +	return NOTIFY_OK;
>> +}
>> +
>> +static struct notifier_block drm_typec_event_nb = {
>> +	.notifier_call = drm_typec_bus_event,
>> +};
>> +
>> +static int check_device_already_added(struct device *dev, void *data)
>> +{
>> +	drm_typec_bus_event(NULL, BUS_NOTIFY_ADD_DEVICE, dev);
>> +	return 0;
>> +}
>> +
>> +static void drm_aux_hpd_typec_dp_bridge_module_exit(void)
>> +{
>> +	bus_unregister_notifier(&typec_bus, &drm_typec_event_nb);
>> +}
>> +
>> +static int __init drm_aux_hpd_typec_dp_bridge_module_init(void)
>> +{
>> +	bus_register_notifier(&typec_bus, &drm_typec_event_nb);
>> +	/*
>> +	 * Before module initialization, some devices may have already been added.
>> +         * Register the HPD bridge for these devices.
>> +	 */
>> +	bus_for_each_dev(&typec_bus, NULL, NULL, check_device_already_added);
>> +	return 0;
>> +}
>> +
>> +module_init(drm_aux_hpd_typec_dp_bridge_module_init);
>> +module_exit(drm_aux_hpd_typec_dp_bridge_module_exit);
>> +
>> +MODULE_AUTHOR("Chaoyi Chen <chaoyi.chen at rock-chips.com>");
>> +MODULE_DESCRIPTION("DRM TYPEC DP HPD BRIDGE");
>> +MODULE_LICENSE("GPL");
>> -- 
>> 2.53.0
>>

-- 
Best, 
Chaoyi


From mukesh.ojha at oss.qualcomm.com  Mon Aug  3 02:14:37 2026
From: mukesh.ojha at oss.qualcomm.com (Mukesh Ojha)
Date: Mon,  3 Aug 2026 14:44:37 +0530
Subject: [PATCH v4 0/4] phy: qcom: Introduce USB support for Hawi
Message-ID: <20260803091441.3040830-1-mukesh.ojha@oss.qualcomm.com>

This series adds USB PHY support for the Hawi SoC. It documents the
QMP USB3-DP combo PHY and M31 eUSB2 PHY bindings, adds the Hawi
compatible to the DWC3 USB binding, and adds the PHY initialization
sequences to the qmp-combo driver.

---
Changes in v4:
 - Rebased on top of next-20260731
 - Resolved conflict related to recent merged common phy headers
    from pcie.
 - Link to v3: https://lore.kernel.org/lkml/20260508213234.4643-1-ronak.raheja at oss.qualcomm.com/

Changes in v3:
- Fix missing version tag in individual patch subjects
- Link to v2: https://lore.kernel.org/all/20260427214217.2735240-1-ronak.raheja at oss.qualcomm.com

Changes in v2:
- Clarify in the QMP PHY binding commit message why Hawi needs a
  dedicated compatible string vs previous targets
- Remove stray blank line in hawi_usb3_rx_tbl
- Link to v1: https://lore.kernel.org/all/20260425070002.348733-1-ronak.raheja at oss.qualcomm.com/


Ronak Raheja (4):
  dt-bindings: phy: qcom,sc8280xp-qmp-usb43dp-phy: Add Hawi QMP PHY
  dt-bindings: phy: qcom,m31-eusb2-phy: Document M31 eUSB2 PHY for Hawi
  dt-bindings: usb: qcom,snps-dwc3: Add Hawi compatible
  phy: qualcomm: qmp-combo: Add support for Hawi SoC

 .../bindings/phy/qcom,m31-eusb2-phy.yaml      |   1 +
 .../phy/qcom,sc8280xp-qmp-usb43dp-phy.yaml    |   2 +
 .../bindings/usb/qcom,snps-dwc3.yaml          |   3 +
 .../phy/qualcomm/phy-qcom-qmp-com-aon-v10.h   |  15 ++
 drivers/phy/qualcomm/phy-qcom-qmp-combo.c     | 231 +++++++++++++++++-
 .../phy/qualcomm/phy-qcom-qmp-dp-phy-v10.h    |  15 ++
 .../phy/qualcomm/phy-qcom-qmp-pcs-aon-v10.h   |  13 +
 .../phy/qualcomm/phy-qcom-qmp-pcs-usb-v10.h   |  19 ++
 drivers/phy/qualcomm/phy-qcom-qmp-pcs-v10.h   |  16 +-
 .../qualcomm/phy-qcom-qmp-qserdes-com-v10.h   | 116 ++++++---
 .../qualcomm/phy-qcom-qmp-qserdes-txrx-v10.h  |  98 ++++++--
 11 files changed, 459 insertions(+), 70 deletions(-)
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-com-aon-v10.h
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-dp-phy-v10.h
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-pcs-aon-v10.h
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-pcs-usb-v10.h

-- 
2.53.0



From mukesh.ojha at oss.qualcomm.com  Mon Aug  3 02:14:38 2026
From: mukesh.ojha at oss.qualcomm.com (Mukesh Ojha)
Date: Mon,  3 Aug 2026 14:44:38 +0530
Subject: [PATCH v4 1/4] dt-bindings: phy: qcom,sc8280xp-qmp-usb43dp-phy: Add Hawi QMP PHY
In-Reply-To: <20260803091441.3040830-1-mukesh.ojha@oss.qualcomm.com>
References: <20260803091441.3040830-1-mukesh.ojha@oss.qualcomm.com>
Message-ID: <20260803091441.3040830-2-mukesh.ojha@oss.qualcomm.com>

From: Ronak Raheja <ronak.raheja at oss.qualcomm.com>

Document the Hawi compatible string for the QMP combo PHY.

Hawi uses a new QSERDES V10 register layout with a new COM AON module
and hardware-specific PHY init sequences compared to previous targets,
requiring a dedicated compatible string.

Signed-off-by: Ronak Raheja <ronak.raheja at oss.qualcomm.com>
Acked-by: Rob Herring (Arm) <robh at kernel.org>
Signed-off-by: Mukesh Ojha <mukesh.ojha at oss.qualcomm.com>
---
 .../devicetree/bindings/phy/qcom,sc8280xp-qmp-usb43dp-phy.yaml  | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Documentation/devicetree/bindings/phy/qcom,sc8280xp-qmp-usb43dp-phy.yaml b/Documentation/devicetree/bindings/phy/qcom,sc8280xp-qmp-usb43dp-phy.yaml
index 4eff92343ce4..ea3f1699eee8 100644
--- a/Documentation/devicetree/bindings/phy/qcom,sc8280xp-qmp-usb43dp-phy.yaml
+++ b/Documentation/devicetree/bindings/phy/qcom,sc8280xp-qmp-usb43dp-phy.yaml
@@ -26,6 +26,7 @@ properties:
           - const: qcom,sm8750-qmp-usb3-dp-phy
       - enum:
           - qcom,glymur-qmp-usb3-dp-phy
+          - qcom,hawi-qmp-usb3-dp-phy
           - qcom,sar2130p-qmp-usb3-dp-phy
           - qcom,sc7180-qmp-usb3-dp-phy
           - qcom,sc7280-qmp-usb3-dp-phy
@@ -209,6 +210,7 @@ allOf:
           contains:
             enum:
               - qcom,glymur-qmp-usb3-dp-phy
+              - qcom,hawi-qmp-usb3-dp-phy
               - qcom,sar2130p-qmp-usb3-dp-phy
               - qcom,sc8280xp-qmp-usb43dp-phy
               - qcom,sm6350-qmp-usb3-dp-phy
-- 
2.53.0



From mukesh.ojha at oss.qualcomm.com  Mon Aug  3 02:14:39 2026
From: mukesh.ojha at oss.qualcomm.com (Mukesh Ojha)
Date: Mon,  3 Aug 2026 14:44:39 +0530
Subject: [PATCH v4 2/4] dt-bindings: phy: qcom,m31-eusb2-phy: Document M31 eUSB2 PHY for Hawi
In-Reply-To: <20260803091441.3040830-1-mukesh.ojha@oss.qualcomm.com>
References: <20260803091441.3040830-1-mukesh.ojha@oss.qualcomm.com>
Message-ID: <20260803091441.3040830-3-mukesh.ojha@oss.qualcomm.com>

From: Ronak Raheja <ronak.raheja at oss.qualcomm.com>

Document the M31 eUSB2 PHY for Hawi which handles the USB2 path. Use
fallback to indicate the compatibility of the M31 eUSB2 PHY on the Hawi
with that on the SM8750.

Signed-off-by: Ronak Raheja <ronak.raheja at oss.qualcomm.com>
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski at oss.qualcomm.com>
Signed-off-by: Mukesh Ojha <mukesh.ojha at oss.qualcomm.com>
---
 Documentation/devicetree/bindings/phy/qcom,m31-eusb2-phy.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Documentation/devicetree/bindings/phy/qcom,m31-eusb2-phy.yaml b/Documentation/devicetree/bindings/phy/qcom,m31-eusb2-phy.yaml
index cd6b84213a7c..c0e7e2963ce6 100644
--- a/Documentation/devicetree/bindings/phy/qcom,m31-eusb2-phy.yaml
+++ b/Documentation/devicetree/bindings/phy/qcom,m31-eusb2-phy.yaml
@@ -19,6 +19,7 @@ properties:
       - items:
           - enum:
               - qcom,glymur-m31-eusb2-phy
+              - qcom,hawi-m31-eusb2-phy
               - qcom,kaanapali-m31-eusb2-phy
           - const: qcom,sm8750-m31-eusb2-phy
       - const: qcom,sm8750-m31-eusb2-phy
-- 
2.53.0



From mukesh.ojha at oss.qualcomm.com  Mon Aug  3 02:14:40 2026
From: mukesh.ojha at oss.qualcomm.com (Mukesh Ojha)
Date: Mon,  3 Aug 2026 14:44:40 +0530
Subject: [PATCH v4 3/4] dt-bindings: usb: qcom,snps-dwc3: Add Hawi compatible
In-Reply-To: <20260803091441.3040830-1-mukesh.ojha@oss.qualcomm.com>
References: <20260803091441.3040830-1-mukesh.ojha@oss.qualcomm.com>
Message-ID: <20260803091441.3040830-4-mukesh.ojha@oss.qualcomm.com>

From: Ronak Raheja <ronak.raheja at oss.qualcomm.com>

Document the Synopsys DWC3 USB controller found on the Hawi platform.

Signed-off-by: Ronak Raheja <ronak.raheja at oss.qualcomm.com>
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski at oss.qualcomm.com>
Signed-off-by: Mukesh Ojha <mukesh.ojha at oss.qualcomm.com>
---
 Documentation/devicetree/bindings/usb/qcom,snps-dwc3.yaml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Documentation/devicetree/bindings/usb/qcom,snps-dwc3.yaml b/Documentation/devicetree/bindings/usb/qcom,snps-dwc3.yaml
index ea60f7220afe..de50d7a3c999 100644
--- a/Documentation/devicetree/bindings/usb/qcom,snps-dwc3.yaml
+++ b/Documentation/devicetree/bindings/usb/qcom,snps-dwc3.yaml
@@ -27,6 +27,7 @@ properties:
           - qcom,eliza-dwc3
           - qcom,glymur-dwc3
           - qcom,glymur-dwc3-mp
+          - qcom,hawi-dwc3
           - qcom,ipq4019-dwc3
           - qcom,ipq5018-dwc3
           - qcom,ipq5210-dwc3
@@ -206,6 +207,7 @@ allOf:
         compatible:
           contains:
             enum:
+              - qcom,hawi-dwc3
               - qcom,ipq5210-dwc3
               - qcom,ipq5424-dwc3
               - qcom,ipq9574-dwc3
@@ -547,6 +549,7 @@ allOf:
           contains:
             enum:
               - qcom,eliza-dwc3
+              - qcom,hawi-dwc3
               - qcom,ipq4019-dwc3
               - qcom,ipq8064-dwc3
               - qcom,kaanapali-dwc3
-- 
2.53.0



From mukesh.ojha at oss.qualcomm.com  Mon Aug  3 02:14:41 2026
From: mukesh.ojha at oss.qualcomm.com (Mukesh Ojha)
Date: Mon,  3 Aug 2026 14:44:41 +0530
Subject: [PATCH v4 4/4] phy: qualcomm: qmp-combo: Add support for Hawi SoC
In-Reply-To: <20260803091441.3040830-1-mukesh.ojha@oss.qualcomm.com>
References: <20260803091441.3040830-1-mukesh.ojha@oss.qualcomm.com>
Message-ID: <20260803091441.3040830-5-mukesh.ojha@oss.qualcomm.com>

From: Ronak Raheja <ronak.raheja at oss.qualcomm.com>

Add support for the USB3-DP combo PHY found on Hawi platform.

The QMP PHY for Hawi uses QSERDES V10 register layouts. Add the required
PHY sequences from the hardware programming guide and new V10 register
header files. Also add a new v10 offset structure to incorporate the new
COM AON register module.

Signed-off-by: Ronak Raheja <ronak.raheja at oss.qualcomm.com>
Reviewed-by: Konrad Dybcio <konrad.dybcio at oss.qualcomm.com>
Reviewed-by: Abel Vesa <abel.vesa at oss.qualcomm.com>
Signed-off-by: Mukesh Ojha <mukesh.ojha at oss.qualcomm.com>
---
 .../phy/qualcomm/phy-qcom-qmp-com-aon-v10.h   |  15 ++
 drivers/phy/qualcomm/phy-qcom-qmp-combo.c     | 231 +++++++++++++++++-
 .../phy/qualcomm/phy-qcom-qmp-dp-phy-v10.h    |  15 ++
 .../phy/qualcomm/phy-qcom-qmp-pcs-aon-v10.h   |  13 +
 .../phy/qualcomm/phy-qcom-qmp-pcs-usb-v10.h   |  19 ++
 drivers/phy/qualcomm/phy-qcom-qmp-pcs-v10.h   |  16 +-
 .../qualcomm/phy-qcom-qmp-qserdes-com-v10.h   | 116 ++++++---
 .../qualcomm/phy-qcom-qmp-qserdes-txrx-v10.h  |  98 ++++++--
 8 files changed, 453 insertions(+), 70 deletions(-)
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-com-aon-v10.h
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-dp-phy-v10.h
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-pcs-aon-v10.h
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-pcs-usb-v10.h

diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-com-aon-v10.h b/drivers/phy/qualcomm/phy-qcom-qmp-com-aon-v10.h
new file mode 100644
index 000000000000..6542b586be89
--- /dev/null
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-com-aon-v10.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2026, Qualcomm Innovation Center, Inc. All rights reserved.
+ */
+
+#ifndef QCOM_PHY_QMP_COM_AON_V10_H_
+#define QCOM_PHY_QMP_COM_AON_V10_H_
+
+/* Only for QMP V10 PHY - COM AON registers */
+
+#define QPHY_V10_COM_AON_USB3_AON_TOGGLE_ENABLE         0x00
+#define QPHY_V10_COM_AON_DP_AON_TOGGLE_ENABLE           0x04
+#define QPHY_V10_COM_AON_DUMMY_STATUS                   0x08
+
+#endif
diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-combo.c b/drivers/phy/qualcomm/phy-qcom-qmp-combo.c
index cdcfad2e86b1..ae80eee628c5 100644
--- a/drivers/phy/qualcomm/phy-qcom-qmp-combo.c
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-combo.c
@@ -32,6 +32,7 @@
 #include "phy-qcom-qmp.h"
 #include "phy-qcom-qmp-pcs-aon-v6.h"
 #include "phy-qcom-qmp-pcs-aon-v8.h"
+#include "phy-qcom-qmp-pcs-aon-v10.h"
 #include "phy-qcom-qmp-pcs-misc-v3.h"
 #include "phy-qcom-qmp-pcs-misc-v4.h"
 #include "phy-qcom-qmp-pcs-misc-v5.h"
@@ -40,6 +41,7 @@
 #include "phy-qcom-qmp-pcs-usb-v5.h"
 #include "phy-qcom-qmp-pcs-usb-v6.h"
 #include "phy-qcom-qmp-pcs-usb-v8.h"
+#include "phy-qcom-qmp-pcs-usb-v10.h"
 
 #include "phy-qcom-qmp-dp-com-v3.h"
 
@@ -49,9 +51,12 @@
 #include "phy-qcom-qmp-dp-phy-v5.h"
 #include "phy-qcom-qmp-dp-phy-v6.h"
 #include "phy-qcom-qmp-dp-phy-v8.h"
+#include "phy-qcom-qmp-dp-phy-v10.h"
 
 #include "phy-qcom-qmp-usb43-pcs-v8.h"
 
+#include "phy-qcom-qmp-com-aon-v10.h"
+
 /* QPHY_V3_DP_COM_RESET_OVRD_CTRL register bits */
 /* DP PHY soft reset */
 #define SW_DPPHY_RESET				BIT(0)
@@ -268,6 +273,36 @@ static const unsigned int qmp_v8_usb3phy_regs_layout[QPHY_LAYOUT_SIZE] = {
 	[QPHY_TX_TRANSCEIVER_BIAS_EN]	= QSERDES_V8_TX_TRANSCEIVER_BIAS_EN,
 };
 
+static const unsigned int qmp_v10_usb3phy_regs_layout[QPHY_LAYOUT_SIZE] = {
+	[QPHY_SW_RESET]			= QPHY_V10_PCS_SW_RESET,
+	[QPHY_START_CTRL]		= QPHY_V10_PCS_START_CONTROL,
+	[QPHY_PCS_STATUS]		= QPHY_V10_PCS_PCS_STATUS1,
+	[QPHY_PCS_POWER_DOWN_CONTROL]	= QPHY_V10_PCS_POWER_DOWN_CONTROL,
+
+	/* In PCS_USB */
+	[QPHY_PCS_AUTONOMOUS_MODE_CTRL]	= QPHY_V10_PCS_USB3_AUTONOMOUS_MODE_CTRL,
+	[QPHY_PCS_LFPS_RXTERM_IRQ_CLEAR] = QPHY_V10_PCS_USB3_LFPS_RXTERM_IRQ_CLEAR,
+
+	[QPHY_PCS_CLAMP_ENABLE]		= QPHY_V10_PCS_AON_CLAMP_ENABLE,
+
+	[QPHY_AON_TOGGLE_ENABLE]	= QPHY_V10_COM_AON_USB3_AON_TOGGLE_ENABLE,
+	[QPHY_DP_AON_TOGGLE_ENABLE]	= QPHY_V10_COM_AON_DP_AON_TOGGLE_ENABLE,
+
+	[QPHY_COM_RESETSM_CNTRL]	= QSERDES_V10_COM_RESETSM_CNTRL,
+	[QPHY_COM_C_READY_STATUS]	= QSERDES_V10_COM_C_READY_STATUS,
+	[QPHY_COM_CMN_STATUS]		= QSERDES_V10_COM_CMN_STATUS,
+	[QPHY_COM_BIAS_EN_CLKBUFLR_EN]	= QSERDES_V10_COM_BIAS_EN_CLKBUFLR_EN,
+
+	[QPHY_DP_PHY_STATUS]		= QSERDES_V10_DP_PHY_STATUS,
+	[QPHY_DP_PHY_VCO_DIV]		= QSERDES_V10_DP_PHY_VCO_DIV,
+
+	[QPHY_TX_TX_POL_INV]		= QSERDES_V10_TX_TX_POL_INV,
+	[QPHY_TX_TX_DRV_LVL]		= QSERDES_V10_TX_TX_DRV_LVL,
+	[QPHY_TX_TX_EMP_POST1_LVL]	= QSERDES_V10_TX_TX_EMP_POST1_LVL,
+	[QPHY_TX_HIGHZ_DRVR_EN]		= QSERDES_V10_TX_HIGHZ_DRVR_EN,
+	[QPHY_TX_TRANSCEIVER_BIAS_EN]	= QSERDES_V10_TX_TRANSCEIVER_BIAS_EN,
+};
+
 static const unsigned int qmp_v8_n3_usb43dpphy_regs_layout[QPHY_LAYOUT_SIZE] = {
 	[QPHY_SW_RESET]			= QPHY_V8_USB43_PCS_SW_RESET,
 	[QPHY_START_CTRL]			= QPHY_V8_USB43_PCS_START_CONTROL,
@@ -2058,6 +2093,141 @@ static const struct qmp_phy_init_tbl x1e80100_usb43dp_pcs_usb_tbl[] = {
 	QMP_PHY_INIT_CFG(QPHY_V6_PCS_USB3_RXEQTRAINING_DFE_TIME_S2, 0x07),
 };
 
+static const struct qmp_phy_init_tbl hawi_usb3_serdes_tbl[] = {
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_SSC_STEP_SIZE1_MODE1, 0xc0),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_SSC_STEP_SIZE2_MODE1, 0x01),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_CP_CTRL_MODE1, 0x02),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_PLL_RCTRL_MODE1, 0x16),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_PLL_CCTRL_MODE1, 0x36),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_CORECLK_DIV_MODE1, 0x04),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_LOCK_CMP1_MODE1, 0x16),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_LOCK_CMP2_MODE1, 0x41),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_DEC_START_MODE1, 0x41),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_DEC_START_MSB_MODE1, 0x00),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_DIV_FRAC_START1_MODE1, 0x55),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_DIV_FRAC_START2_MODE1, 0x75),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_DIV_FRAC_START3_MODE1, 0x01),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_HSCLK_SEL_1, 0x01),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_VCO_TUNE1_MODE1, 0x25),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_VCO_TUNE2_MODE1, 0x02),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_BIN_VCOCAL_CMP_CODE1_MODE1, 0x5c),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_BIN_VCOCAL_CMP_CODE2_MODE1, 0x0f),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_BIN_VCOCAL_CMP_CODE1_MODE0, 0x5c),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_BIN_VCOCAL_CMP_CODE2_MODE0, 0x0f),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_SSC_STEP_SIZE1_MODE0, 0xc0),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_SSC_STEP_SIZE2_MODE0, 0x01),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_CP_CTRL_MODE0, 0x02),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_PLL_RCTRL_MODE0, 0x16),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_PLL_CCTRL_MODE0, 0x36),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_PLL_CORE_CLK_DIV_MODE0, 0x0a),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_LOCK_CMP1_MODE0, 0x08),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_LOCK_CMP2_MODE0, 0x1a),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_DEC_START_MODE0, 0x41),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_DEC_START_MSB_MODE0, 0x00),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_DIV_FRAC_START1_MODE0, 0x55),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_DIV_FRAC_START2_MODE0, 0x75),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_DIV_FRAC_START3_MODE0, 0x01),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_VCO_TUNE1_MODE0, 0x25),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_VCO_TUNE2_MODE0, 0x02),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_BG_TIMER, 0x0a),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_SSC_EN_CENTER, 0x01),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_SSC_PER1, 0x62),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_SSC_PER2, 0x02),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_SYSCLK_BUF_ENABLE, 0x0c),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_SYSCLK_EN_SEL, 0x1a),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_LOCK_CMP_CFG, 0x14),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_VCO_TUNE_MAP, 0x04),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_CORE_CLK_EN, 0x20),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_CMN_CONFIG_1, 0x16),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_AUTO_GAIN_ADJ_CTRL_1, 0xb6),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_AUTO_GAIN_ADJ_CTRL_2, 0x4a),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_AUTO_GAIN_ADJ_CTRL_3, 0x36),
+	QMP_PHY_INIT_CFG(QSERDES_V10_COM_ADDITIONAL_MISC, 0x0c),
+};
+
+static const struct qmp_phy_init_tbl hawi_usb3_tx_tbl[] = {
+	QMP_PHY_INIT_CFG(QSERDES_V10_TX_RES_CODE_LANE_TX, 0x00),
+	QMP_PHY_INIT_CFG(QSERDES_V10_TX_RES_CODE_LANE_RX, 0x00),
+	QMP_PHY_INIT_CFG(QSERDES_V10_TX_RES_CODE_LANE_OFFSET_TX, 0x1f),
+	QMP_PHY_INIT_CFG(QSERDES_V10_TX_RES_CODE_LANE_OFFSET_RX, 0x09),
+	QMP_PHY_INIT_CFG(QSERDES_V10_TX_LANE_MODE_1, 0xe5),
+	QMP_PHY_INIT_CFG(QSERDES_V10_TX_LANE_MODE_2, 0x02),
+	QMP_PHY_INIT_CFG(QSERDES_V10_TX_LANE_MODE_3, 0x11),
+	QMP_PHY_INIT_CFG(QSERDES_V10_TX_LANE_MODE_4, 0x31),
+	QMP_PHY_INIT_CFG(QSERDES_V10_TX_LANE_MODE_5, 0x5d),
+	QMP_PHY_INIT_CFG(QSERDES_V10_TX_RCV_DETECT_LVL_2, 0x12),
+	QMP_PHY_INIT_CFG_LANE(QSERDES_V10_TX_PI_QEC_CTRL, 0x21, 1),
+	QMP_PHY_INIT_CFG_LANE(QSERDES_V10_TX_PI_QEC_CTRL, 0x05, 2),
+};
+
+static const struct qmp_phy_init_tbl hawi_usb3_rx_tbl[] = {
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_FO_GAIN, 0x0a),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_SO_GAIN, 0x06),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_FASTLOCK_FO_GAIN, 0x2f),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_SO_SATURATION_AND_ENABLE, 0x7f),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_FASTLOCK_COUNT_LOW, 0xff),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_FASTLOCK_COUNT_HIGH, 0x0f),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_PI_CONTROLS, 0x99),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_SB2_THRESH1, 0x08),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_SB2_THRESH2, 0x08),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_SB2_GAIN1, 0x00),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_UCDR_SB2_GAIN2, 0x0a),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_AUX_DATA_TCOARSE_TFINE, 0x20),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_VGA_CAL_CNTRL1, 0x54),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_VGA_CAL_CNTRL2, 0x0f),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_GM_CAL, 0x13),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_EQU_ADAPTOR_CNTRL2, 0x0e),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_EQU_ADAPTOR_CNTRL3, 0x4a),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_EQU_ADAPTOR_CNTRL4, 0x0a),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_IDAC_TSETTLE_LOW, 0x07),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_IDAC_TSETTLE_HIGH, 0x00),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_EQ_OFFSET_ADAPTOR_CNTRL1, 0x27),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_SIGDET_ENABLES, 0x0c),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_SIGDET_CNTRL, 0x04),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_SIGDET_DEGLITCH_CNTRL, 0x0e),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_MODE_00_LOW, 0x3f),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_MODE_00_HIGH, 0xbf),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_MODE_00_HIGH2, 0xff),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_MODE_00_HIGH3, 0xdf),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_MODE_00_HIGH4, 0xed),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_MODE_01_LOW, 0x19),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_MODE_01_HIGH, 0x09),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_MODE_01_HIGH2, 0x91),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_MODE_01_HIGH3, 0xb7),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_RX_MODE_01_HIGH4, 0xaa),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_DFE_EN_TIMER, 0x04),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_DFE_CTLE_POST_CAL_OFFSET, 0x38),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_DCC_CTRL1, 0x0c),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_VTH_CODE, 0x10),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_SIGDET_CAL_CTRL1, 0x14),
+	QMP_PHY_INIT_CFG(QSERDES_V10_RX_SIGDET_CAL_TRIM, 0x08),
+};
+
+static const struct qmp_phy_init_tbl hawi_usb3_pcs_tbl[] = {
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_LOCK_DETECT_CONFIG1, 0xc4),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_LOCK_DETECT_CONFIG2, 0x89),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_LOCK_DETECT_CONFIG3, 0x20),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_LOCK_DETECT_CONFIG6, 0x13),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_REFGEN_REQ_CONFIG1, 0x21),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_RX_SIGDET_LVL, 0x55),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_RCVR_DTCT_DLY_P1U2_L, 0xe7),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_RCVR_DTCT_DLY_P1U2_H, 0x03),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_RX_CONFIG, 0x0a),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_ALIGN_DETECT_CONFIG1, 0x88),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_ALIGN_DETECT_CONFIG2, 0x13),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_PCS_TX_RX_CONFIG, 0x04),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_PCS_TX_RX_CONFIG2, 0x01),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_EQ_CONFIG1, 0x4b),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_EQ_CONFIG5, 0x10),
+};
+
+static const struct qmp_phy_init_tbl hawi_usb3_pcs_usb_tbl[] = {
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_USB3_LFPS_DET_HIGH_COUNT_VAL, 0xf8),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_USB3_RXEQTRAINING_DFE_TIME_S2, 0x07),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_USB3_RCVR_DTCT_DLY_U3_L, 0x40),
+	QMP_PHY_INIT_CFG(QPHY_V10_PCS_USB3_RCVR_DTCT_DLY_U3_H, 0x00),
+};
+
 /* list of regulators */
 static struct regulator_bulk_data qmp_phy_vreg_l[] = {
 	{ .supply = "vdda-phy", .init_load_uA = 21800, },
@@ -2198,6 +2368,7 @@ struct qmp_combo_offsets {
 	u16 dp_txa;
 	u16 dp_txb;
 	u16 dp_dp_phy;
+	u16 aon_toggle;
 };
 
 struct qmp_phy_cfg {
@@ -2270,6 +2441,7 @@ struct qmp_combo {
 	const struct qmp_phy_cfg *cfg;
 
 	void __iomem *com;
+	void __iomem *aon_toggle;
 
 	void __iomem *serdes;
 	void __iomem *tx;
@@ -2416,6 +2588,24 @@ static const struct qmp_combo_offsets qmp_combo_offsets_v8 = {
 	.dp_dp_phy	= 0x3c00,
 };
 
+static const struct qmp_combo_offsets qmp_combo_offsets_v10 = {
+	.com		= 0x0000,
+	.aon_toggle	= 0x0400,
+	.txa		= 0x1400,
+	.rxa		= 0x1600,
+	.txb		= 0x1800,
+	.rxb		= 0x1a00,
+	.usb3_serdes	= 0x1000,
+	.usb3_pcs_misc	= 0x1c00,
+	.usb3_pcs	= 0x1e00,
+	.usb3_pcs_aon	= 0x2000,
+	.usb3_pcs_usb	= 0x2100,
+	.dp_serdes	= 0x3000,
+	.dp_txa		= 0x3400,
+	.dp_txb		= 0x3800,
+	.dp_dp_phy	= 0x3c00,
+};
+
 static const struct qmp_combo_offsets qmp_combo_usb43dp_offsets_v8 = {
 	.com		= 0x0000,
 	.usb3_pcs_aon	= 0x0100,
@@ -2705,6 +2895,27 @@ static const struct qmp_phy_cfg x1e80100_usb3dpphy_cfg = {
 	.regs			= qmp_v6_n4_usb3phy_regs_layout,
 };
 
+static const struct qmp_phy_cfg hawi_usb3dpphy_cfg = {
+	.offsets		= &qmp_combo_offsets_v10,
+
+	.serdes_tbl		= hawi_usb3_serdes_tbl,
+	.serdes_tbl_num		= ARRAY_SIZE(hawi_usb3_serdes_tbl),
+	.tx_tbl			= hawi_usb3_tx_tbl,
+	.tx_tbl_num		= ARRAY_SIZE(hawi_usb3_tx_tbl),
+	.rx_tbl			= hawi_usb3_rx_tbl,
+	.rx_tbl_num		= ARRAY_SIZE(hawi_usb3_rx_tbl),
+	.pcs_tbl		= hawi_usb3_pcs_tbl,
+	.pcs_tbl_num		= ARRAY_SIZE(hawi_usb3_pcs_tbl),
+	.pcs_usb_tbl		= hawi_usb3_pcs_usb_tbl,
+	.pcs_usb_tbl_num	= ARRAY_SIZE(hawi_usb3_pcs_usb_tbl),
+
+	.regs			= qmp_v10_usb3phy_regs_layout,
+	.reset_list		= msm8996_usb3phy_reset_l,
+	.num_resets		= ARRAY_SIZE(msm8996_usb3phy_reset_l),
+	.vreg_list		= qmp_phy_vreg_l,
+	.num_vregs		= ARRAY_SIZE(qmp_phy_vreg_l),
+};
+
 static const struct qmp_phy_cfg sm6350_usb3dpphy_cfg = {
 	.offsets		= &qmp_combo_offsets_v3,
 
@@ -3662,13 +3873,18 @@ static int qmp_combo_com_init(struct qmp_combo *qmp, bool force)
 {
 	const struct qmp_phy_cfg *cfg = qmp->cfg;
 	void __iomem *com = qmp->com;
-	void __iomem *pcs_aon = qmp->pcs_aon;
+	void __iomem *aon_toggle;
 	int ret;
 	u32 val;
 
 	if (!force && qmp->init_count++)
 		return 0;
 
+	if (qmp->aon_toggle)
+		aon_toggle = qmp->aon_toggle;
+	else
+		aon_toggle = qmp->pcs_aon;
+
 	ret = regulator_bulk_enable(cfg->num_vregs, qmp->vregs);
 	if (ret) {
 		dev_err(qmp->dev, "failed to enable regulators, err=%d\n", ret);
@@ -3706,9 +3922,9 @@ static int qmp_combo_com_init(struct qmp_combo *qmp, bool force)
 			SW_USB3PHY_RESET_MUX | SW_USB3PHY_RESET);
 
 	/* override hardware control for reset of qmp phy */
-	if (pcs_aon && cfg->regs[QPHY_AON_TOGGLE_ENABLE]) {
-		qphy_clrbits(pcs_aon, cfg->regs[QPHY_AON_TOGGLE_ENABLE], 0x1);
-		qphy_clrbits(pcs_aon, cfg->regs[QPHY_DP_AON_TOGGLE_ENABLE], 0x1);
+	if (aon_toggle && cfg->regs[QPHY_AON_TOGGLE_ENABLE]) {
+		qphy_clrbits(aon_toggle, cfg->regs[QPHY_AON_TOGGLE_ENABLE], 0x1);
+		qphy_clrbits(aon_toggle, cfg->regs[QPHY_DP_AON_TOGGLE_ENABLE], 0x1);
 	}
 
 	/* Use software based port select and switch on typec orientation */
@@ -4735,6 +4951,9 @@ static int qmp_combo_parse_dt(struct qmp_combo *qmp)
 	}
 	qmp->dp_dp_phy = base + offs->dp_dp_phy;
 
+	if (offs->aon_toggle)
+		qmp->aon_toggle = base + offs->aon_toggle;
+
 	ret = qmp_combo_clk_init(qmp);
 	if (ret)
 		return ret;
@@ -4988,6 +5207,10 @@ static const struct of_device_id qmp_combo_of_match_table[] = {
 		.compatible = "qcom,glymur-qmp-usb3-dp-phy",
 		.data = &glymur_usb3dpphy_cfg,
 	},
+	{
+		.compatible = "qcom,hawi-qmp-usb3-dp-phy",
+		.data = &hawi_usb3dpphy_cfg,
+	},
 	{
 		.compatible = "qcom,sar2130p-qmp-usb3-dp-phy",
 		.data = &sar2130p_usb3dpphy_cfg,
diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-dp-phy-v10.h b/drivers/phy/qualcomm/phy-qcom-qmp-dp-phy-v10.h
new file mode 100644
index 000000000000..6f3ea7d13556
--- /dev/null
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-dp-phy-v10.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2026, Qualcomm Innovation Center, Inc. All rights reserved.
+ */
+
+#ifndef QCOM_PHY_QMP_DP_PHY_V10_H_
+#define QCOM_PHY_QMP_DP_PHY_V10_H_
+
+/* Only for QMP V10 PHY - DP PHY registers */
+
+#define QSERDES_V10_DP_PHY_VCO_DIV			0x070
+#define QSERDES_V10_DP_PHY_AUX_INTERRUPT_STATUS		0x0e0
+#define QSERDES_V10_DP_PHY_STATUS			0x0e4
+
+#endif
diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-pcs-aon-v10.h b/drivers/phy/qualcomm/phy-qcom-qmp-pcs-aon-v10.h
new file mode 100644
index 000000000000..b858381bc238
--- /dev/null
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-pcs-aon-v10.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2026, Qualcomm Innovation Center, Inc. All rights reserved.
+ */
+
+#ifndef QCOM_PHY_QMP_PCS_AON_V10_H_
+#define QCOM_PHY_QMP_PCS_AON_V10_H_
+
+/* Only for QMP V10 PHY - PCS AON registers */
+
+#define QPHY_V10_PCS_AON_CLAMP_ENABLE			0x00
+
+#endif
diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-pcs-usb-v10.h b/drivers/phy/qualcomm/phy-qcom-qmp-pcs-usb-v10.h
new file mode 100644
index 000000000000..0cc25e6acf58
--- /dev/null
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-pcs-usb-v10.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2026, Qualcomm Innovation Center, Inc. All rights reserved.
+ */
+
+#ifndef QCOM_PHY_QMP_PCS_USB_V10_H_
+#define QCOM_PHY_QMP_PCS_USB_V10_H_
+
+/* Only for QMP V10 PHY - USB PCS registers */
+
+#define QPHY_V10_PCS_USB3_POWER_STATE_CONFIG1		0x00
+#define QPHY_V10_PCS_USB3_AUTONOMOUS_MODE_CTRL		0x08
+#define QPHY_V10_PCS_USB3_LFPS_RXTERM_IRQ_CLEAR		0x14
+#define QPHY_V10_PCS_USB3_LFPS_DET_HIGH_COUNT_VAL	0x18
+#define QPHY_V10_PCS_USB3_RXEQTRAINING_DFE_TIME_S2	0x3c
+#define QPHY_V10_PCS_USB3_RCVR_DTCT_DLY_U3_L		0x40
+#define QPHY_V10_PCS_USB3_RCVR_DTCT_DLY_U3_H		0x44
+
+#endif
diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-pcs-v10.h b/drivers/phy/qualcomm/phy-qcom-qmp-pcs-v10.h
index 165ce8a28f61..e881fb14650c 100644
--- a/drivers/phy/qualcomm/phy-qcom-qmp-pcs-v10.h
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-pcs-v10.h
@@ -6,17 +6,31 @@
 #ifndef QCOM_PHY_QMP_PCS_V10_H_
 #define QCOM_PHY_QMP_PCS_V10_H_
 
-/* Only for QMP V10 PHY - PCIe PCS registers */
+/* Only for QMP V10 PHY - PCS registers */
+
 #define QPHY_V10_PCS_SW_RESET				0x000
 #define QPHY_V10_PCS_PCS_STATUS1			0x014
 #define QPHY_V10_PCS_POWER_DOWN_CONTROL			0x040
 #define QPHY_V10_PCS_START_CONTROL			0x044
+#define QPHY_V10_PCS_POWER_STATE_CONFIG1		0x090
+#define QPHY_V10_PCS_LOCK_DETECT_CONFIG1		0x0c4
+#define QPHY_V10_PCS_LOCK_DETECT_CONFIG2		0x0c8
+#define QPHY_V10_PCS_LOCK_DETECT_CONFIG3		0x0cc
+#define QPHY_V10_PCS_LOCK_DETECT_CONFIG6		0x0d8
 #define QPHY_V10_PCS_REFGEN_REQ_CONFIG1			0x0dc
 #define QPHY_V10_PCS_G12S1_TXDEEMPH_M6DB		0x168
 #define QPHY_V10_PCS_G3S2_PRE_GAIN			0x170
 #define QPHY_V10_PCS_RX_SIGDET_LVL			0x188
+#define QPHY_V10_PCS_RCVR_DTCT_DLY_P1U2_L		0x190
+#define QPHY_V10_PCS_RCVR_DTCT_DLY_P1U2_H		0x194
 #define QPHY_V10_PCS_RATE_SLEW_CNTRL1			0x198
+#define QPHY_V10_PCS_RX_CONFIG				0x1b0
+#define QPHY_V10_PCS_ALIGN_DETECT_CONFIG1		0x1c0
+#define QPHY_V10_PCS_ALIGN_DETECT_CONFIG2		0x1c4
 #define QPHY_V10_PCS_PCS_TX_RX_CONFIG			0x1d0
+#define QPHY_V10_PCS_PCS_TX_RX_CONFIG2			0x1d4
+#define QPHY_V10_PCS_EQ_CONFIG1				0x1e0
 #define QPHY_V10_PCS_EQ_CONFIG2				0x1e4
+#define QPHY_V10_PCS_EQ_CONFIG5				0x1f0
 
 #endif
diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-com-v10.h b/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-com-v10.h
index 09199e7b4aac..bbd1f67ea69a 100644
--- a/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-com-v10.h
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-com-v10.h
@@ -7,43 +7,83 @@
 #define QCOM_PHY_QMP_QSERDES_COM_V10_H_
 
 /* Only for QMP V10 PHY - QSERDES COM registers */
-#define QSERDES_V10_COM_SSC_STEP_SIZE1_MODE1		0x00
-#define QSERDES_V10_COM_SSC_STEP_SIZE2_MODE1		0x04
-#define QSERDES_V10_COM_CP_CTRL_MODE1			0x10
-#define QSERDES_V10_COM_PLL_RCTRL_MODE1			0x14
-#define QSERDES_V10_COM_PLL_CCTRL_MODE1			0x18
-#define QSERDES_V10_COM_CORECLK_DIV_MODE1		0x1c
-#define QSERDES_V10_COM_LOCK_CMP1_MODE1			0x20
-#define QSERDES_V10_COM_LOCK_CMP2_MODE1			0x24
-#define QSERDES_V10_COM_DEC_START_MODE1			0x28
-#define QSERDES_V10_COM_DIV_FRAC_START1_MODE1		0x30
-#define QSERDES_V10_COM_DIV_FRAC_START2_MODE1		0x34
-#define QSERDES_V10_COM_DIV_FRAC_START3_MODE1		0x38
-#define QSERDES_V10_COM_HSCLK_SEL_1			0x3c
-#define QSERDES_V10_COM_SSC_STEP_SIZE1_MODE0		0x60
-#define QSERDES_V10_COM_SSC_STEP_SIZE2_MODE0		0x64
-#define QSERDES_V10_COM_CP_CTRL_MODE0			0x70
-#define QSERDES_V10_COM_PLL_RCTRL_MODE0			0x74
-#define QSERDES_V10_COM_PLL_CCTRL_MODE0			0x78
-#define QSERDES_V10_COM_LOCK_CMP1_MODE0			0x80
-#define QSERDES_V10_COM_LOCK_CMP2_MODE0			0x84
-#define QSERDES_V10_COM_DEC_START_MODE0			0x88
-#define QSERDES_V10_COM_DIV_FRAC_START1_MODE0		0x90
-#define QSERDES_V10_COM_DIV_FRAC_START2_MODE0		0x94
-#define QSERDES_V10_COM_DIV_FRAC_START3_MODE0		0x98
-#define QSERDES_V10_COM_BG_TIMER			0xbc
-#define QSERDES_V10_COM_SSC_EN_CENTER			0xc0
-#define QSERDES_V10_COM_SSC_PER1			0xcc
-#define QSERDES_V10_COM_SSC_PER2			0xd0
-#define QSERDES_V10_COM_CLK_ENABLE1			0xe0
-#define QSERDES_V10_COM_SYS_CLK_CTRL			0xe4
-#define QSERDES_V10_COM_PLL_IVCO			0xf4
-#define QSERDES_V10_COM_SYSCLK_EN_SEL			0x110
-#define QSERDES_V10_COM_LOCK_CMP_EN			0x120
-#define QSERDES_V10_COM_VCO_TUNE_MAP			0x140
-#define QSERDES_V10_COM_CLK_SELECT			0x164
-#define QSERDES_V10_COM_CORE_CLK_EN			0x170
-#define QSERDES_V10_COM_CMN_CONFIG_1			0x174
-#define QSERDES_V10_COM_ADDITIONAL_MISC_3		0x1bc
+
+#define QSERDES_V10_COM_SSC_STEP_SIZE1_MODE1			0x00
+#define QSERDES_V10_COM_SSC_STEP_SIZE2_MODE1			0x04
+#define QSERDES_V10_COM_CP_CTRL_MODE1				0x10
+#define QSERDES_V10_COM_PLL_RCTRL_MODE1				0x14
+#define QSERDES_V10_COM_PLL_CCTRL_MODE1				0x18
+#define QSERDES_V10_COM_CORECLK_DIV_MODE1			0x1c
+#define QSERDES_V10_COM_LOCK_CMP1_MODE1				0x20
+#define QSERDES_V10_COM_LOCK_CMP2_MODE1				0x24
+#define QSERDES_V10_COM_DEC_START_MODE1				0x28
+#define QSERDES_V10_COM_DEC_START_MSB_MODE1			0x2c
+#define QSERDES_V10_COM_DIV_FRAC_START1_MODE1			0x30
+#define QSERDES_V10_COM_DIV_FRAC_START2_MODE1			0x34
+#define QSERDES_V10_COM_DIV_FRAC_START3_MODE1			0x38
+#define QSERDES_V10_COM_HSCLK_SEL_1				0x3c
+#define QSERDES_V10_COM_INTEGLOOP_GAIN0_MODE1			0x40
+#define QSERDES_V10_COM_INTEGLOOP_GAIN1_MODE1			0x44
+#define QSERDES_V10_COM_VCO_TUNE1_MODE1				0x48
+#define QSERDES_V10_COM_VCO_TUNE2_MODE1				0x4c
+#define QSERDES_V10_COM_BIN_VCOCAL_CMP_CODE1_MODE1		0x50
+#define QSERDES_V10_COM_BIN_VCOCAL_CMP_CODE2_MODE1		0x54
+#define QSERDES_V10_COM_BIN_VCOCAL_CMP_CODE1_MODE0		0x58
+#define QSERDES_V10_COM_BIN_VCOCAL_CMP_CODE2_MODE0		0x5c
+#define QSERDES_V10_COM_SSC_STEP_SIZE1_MODE0			0x60
+#define QSERDES_V10_COM_SSC_STEP_SIZE2_MODE0			0x64
+#define QSERDES_V10_COM_CP_CTRL_MODE0				0x70
+#define QSERDES_V10_COM_PLL_RCTRL_MODE0				0x74
+#define QSERDES_V10_COM_PLL_CCTRL_MODE0				0x78
+#define QSERDES_V10_COM_PLL_CORE_CLK_DIV_MODE0			0x7c
+#define QSERDES_V10_COM_LOCK_CMP1_MODE0				0x80
+#define QSERDES_V10_COM_LOCK_CMP2_MODE0				0x84
+#define QSERDES_V10_COM_DEC_START_MODE0				0x88
+#define QSERDES_V10_COM_DEC_START_MSB_MODE0			0x8c
+#define QSERDES_V10_COM_DIV_FRAC_START1_MODE0			0x90
+#define QSERDES_V10_COM_DIV_FRAC_START2_MODE0			0x94
+#define QSERDES_V10_COM_DIV_FRAC_START3_MODE0			0x98
+#define QSERDES_V10_COM_HSCLK_HS_SWITCH_SEL_1			0x9c
+#define QSERDES_V10_COM_INTEGLOOP_GAIN0_MODE0			0xa0
+#define QSERDES_V10_COM_INTEGLOOP_GAIN1_MODE0			0xa4
+#define QSERDES_V10_COM_VCO_TUNE1_MODE0				0xa8
+#define QSERDES_V10_COM_VCO_TUNE2_MODE0				0xac
+#define QSERDES_V10_COM_BG_TIMER				0xbc
+#define QSERDES_V10_COM_SSC_EN_CENTER				0xc0
+#define QSERDES_V10_COM_SSC_ADJ_PER1				0xc4
+#define QSERDES_V10_COM_SSC_PER1				0xcc
+#define QSERDES_V10_COM_SSC_PER2				0xd0
+#define QSERDES_V10_COM_POST_DIV_MUX				0xd8
+#define QSERDES_V10_COM_BIAS_EN_CLKBUFLR_EN			0xdc
+#define QSERDES_V10_COM_CLK_ENABLE1				0xe0
+#define QSERDES_V10_COM_SYS_CLK_CTRL				0xe4
+#define QSERDES_V10_COM_SYSCLK_BUF_ENABLE			0xe8
+#define QSERDES_V10_COM_PLL_IVCO				0xf4
+#define QSERDES_V10_COM_PLL_IVCO_MODE1				0xf8
+#define QSERDES_V10_COM_CMN_IETRIM				0xfc
+#define QSERDES_V10_COM_CMN_IPTRIM				0x100
+#define QSERDES_V10_COM_SYSCLK_EN_SEL				0x110
+#define QSERDES_V10_COM_RESETSM_CNTRL				0x118
+#define QSERDES_V10_COM_LOCK_CMP_EN				0x120
+#define QSERDES_V10_COM_LOCK_CMP_CFG				0x124
+#define QSERDES_V10_COM_VCO_TUNE_CTRL				0x13c
+#define QSERDES_V10_COM_VCO_TUNE_MAP				0x140
+#define QSERDES_V10_COM_VCO_TUNE_INITVAL2			0x148
+#define QSERDES_V10_COM_VCO_TUNE_MAXVAL2			0x158
+#define QSERDES_V10_COM_CLK_SELECT				0x164
+#define QSERDES_V10_COM_CORE_CLK_EN				0x170
+#define QSERDES_V10_COM_CMN_CONFIG_1				0x174
+#define QSERDES_V10_COM_SVS_MODE_CLK_SEL			0x17c
+#define QSERDES_V10_COM_CMN_MISC_1				0x184
+#define QSERDES_V10_COM_CMN_MODE				0x188
+#define QSERDES_V10_COM_PLL_VCO_DC_LEVEL_CTRL			0x198
+#define QSERDES_V10_COM_AUTO_GAIN_ADJ_CTRL_1			0x1a4
+#define QSERDES_V10_COM_AUTO_GAIN_ADJ_CTRL_2			0x1a8
+#define QSERDES_V10_COM_AUTO_GAIN_ADJ_CTRL_3			0x1ac
+#define QSERDES_V10_COM_ADDITIONAL_MISC				0x1b4
+#define QSERDES_V10_COM_ADDITIONAL_MISC_2			0x1b8
+#define QSERDES_V10_COM_ADDITIONAL_MISC_3			0x1bc
+#define QSERDES_V10_COM_CMN_STATUS				0x2c8
+#define QSERDES_V10_COM_C_READY_STATUS				0x2f0
 
 #endif
diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-txrx-v10.h b/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-txrx-v10.h
index d81ebdde0063..edbc26b6d74f 100644
--- a/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-txrx-v10.h
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-txrx-v10.h
@@ -6,42 +6,86 @@
 #ifndef QCOM_PHY_QMP_QSERDES_TXRX_V10_H_
 #define QCOM_PHY_QMP_QSERDES_TXRX_V10_H_
 
-#define QSERDES_V10_TX_RES_CODE_LANE_OFFSET_TX			0x03c
-#define QSERDES_V10_TX_RES_CODE_LANE_OFFSET_RX			0x040
-#define QSERDES_V10_TX_LANE_MODE_1				0x084
-#define QSERDES_V10_TX_LANE_MODE_3				0x08c
-#define QSERDES_V10_TX_LANE_MODE_4				0x090
-#define QSERDES_V10_TX_LANE_MODE_5				0x094
-#define QSERDES_V10_TX_PI_QEC_CTRL				0x0e4
+/* Only for QMP V10 PHY - QSERDES TX/RX registers */
 
-#define QSERDES_V10_RX_UCDR_FO_GAIN				0x008
-#define QSERDES_V10_RX_UCDR_SO_GAIN				0x014
-#define QSERDES_V10_RX_UCDR_SB2_THRESH1				0x04c
-#define QSERDES_V10_RX_UCDR_SB2_THRESH2				0x050
-#define QSERDES_V10_RX_TX_ADAPT_PRE_THRESH1			0x0c4
-#define QSERDES_V10_RX_TX_ADAPT_PRE_THRESH2			0x0c8
-#define QSERDES_V10_RX_TX_ADAPT_POST_THRESH			0x0cc
-#define QSERDES_V10_RX_VGA_CAL_CNTRL2				0x0d8
-#define QSERDES_V10_RX_GM_CAL					0x0dc
-#define QSERDES_V10_RX_RX_IDAC_TSETTLE_LOW			0x0f8
+/* TX registers */
+#define QSERDES_V10_TX_CLKBUF_ENABLE				0x08
+#define QSERDES_V10_TX_TX_EMP_POST1_LVL			0x0c
+#define QSERDES_V10_TX_TX_DRV_LVL				0x14
+#define QSERDES_V10_TX_RESET_TSYNC_EN				0x1c
+#define QSERDES_V10_TX_PRE_STALL_LDO_BOOST_EN			0x20
+#define QSERDES_V10_TX_TX_BAND				0x24
+#define QSERDES_V10_TX_INTERFACE_SELECT			0x2c
+#define QSERDES_V10_TX_RES_CODE_LANE_TX			0x34
+#define QSERDES_V10_TX_RES_CODE_LANE_RX			0x38
+#define QSERDES_V10_TX_RES_CODE_LANE_OFFSET_TX			0x3c
+#define QSERDES_V10_TX_RES_CODE_LANE_OFFSET_RX			0x40
+#define QSERDES_V10_TX_TRANSCEIVER_BIAS_EN			0x54
+#define QSERDES_V10_TX_HIGHZ_DRVR_EN				0x58
+#define QSERDES_V10_TX_TX_POL_INV				0x5c
+#define QSERDES_V10_TX_PARRATE_REC_DETECT_IDLE_EN		0x60
+#define QSERDES_V10_TX_BIST_PATTERN7				0x7c
+#define QSERDES_V10_TX_LANE_MODE_1				0x84
+#define QSERDES_V10_TX_LANE_MODE_2				0x88
+#define QSERDES_V10_TX_LANE_MODE_3				0x8c
+#define QSERDES_V10_TX_LANE_MODE_4				0x90
+#define QSERDES_V10_TX_LANE_MODE_5				0x94
+#define QSERDES_V10_TX_RCV_DETECT_LVL_2			0xa4
+#define QSERDES_V10_TX_TRAN_DRVR_EMP_EN			0xc0
+#define QSERDES_V10_TX_TX_INTERFACE_MODE			0xc4
+#define QSERDES_V10_TX_VMODE_CTRL1				0xc8
+#define QSERDES_V10_TX_PI_QEC_CTRL				0xe4
+
+/* RX registers */
+#define QSERDES_V10_RX_UCDR_FO_GAIN				0x08
+#define QSERDES_V10_RX_UCDR_SO_GAIN				0x14
+#define QSERDES_V10_RX_UCDR_FASTLOCK_FO_GAIN			0x30
+#define QSERDES_V10_RX_UCDR_SO_SATURATION_AND_ENABLE		0x34
+#define QSERDES_V10_RX_UCDR_FASTLOCK_COUNT_LOW			0x3c
+#define QSERDES_V10_RX_UCDR_FASTLOCK_COUNT_HIGH		0x40
+#define QSERDES_V10_RX_UCDR_PI_CONTROLS			0x44
+#define QSERDES_V10_RX_UCDR_SB2_THRESH1			0x4c
+#define QSERDES_V10_RX_UCDR_SB2_THRESH2			0x50
+#define QSERDES_V10_RX_UCDR_SB2_GAIN1				0x54
+#define QSERDES_V10_RX_UCDR_SB2_GAIN2				0x58
+#define QSERDES_V10_RX_AUX_DATA_TCOARSE_TFINE			0x60
+#define QSERDES_V10_RX_TX_ADAPT_PRE_THRESH1			0xc4
+#define QSERDES_V10_RX_TX_ADAPT_PRE_THRESH2			0xc8
+#define QSERDES_V10_RX_TX_ADAPT_POST_THRESH			0xcc
+#define QSERDES_V10_RX_VGA_CAL_CNTRL1				0xd4
+#define QSERDES_V10_RX_VGA_CAL_CNTRL2				0xd8
+#define QSERDES_V10_RX_GM_CAL				0xdc
+#define QSERDES_V10_RX_RX_EQU_ADAPTOR_CNTRL2			0xec
+#define QSERDES_V10_RX_RX_EQU_ADAPTOR_CNTRL3			0xf0
+#define QSERDES_V10_RX_RX_EQU_ADAPTOR_CNTRL4			0xf4
+#define QSERDES_V10_RX_RX_IDAC_TSETTLE_LOW			0xf8
+#define QSERDES_V10_RX_RX_IDAC_TSETTLE_HIGH			0xfc
+#define QSERDES_V10_RX_RX_IDAC_ENABLES			0x100
+#define QSERDES_V10_RX_RX_TERM_AC_BYPASS_DC_COUPLE_OFFSET	0x104
+#define QSERDES_V10_RX_RX_EQ_OFFSET_ADAPTOR_CNTRL1		0x110
 #define QSERDES_V10_RX_SIGDET_ENABLES				0x118
 #define QSERDES_V10_RX_SIGDET_CNTRL				0x11c
+#define QSERDES_V10_RX_SIGDET_DEGLITCH_CNTRL			0x124
 #define QSERDES_V10_RX_RX_MODE_00_LOW				0x15c
 #define QSERDES_V10_RX_RX_MODE_00_HIGH				0x160
-#define QSERDES_V10_RX_RX_MODE_00_HIGH2				0x164
-#define QSERDES_V10_RX_RX_MODE_00_HIGH3				0x168
-#define QSERDES_V10_RX_RX_MODE_00_HIGH4				0x16c
+#define QSERDES_V10_RX_RX_MODE_00_HIGH2			0x164
+#define QSERDES_V10_RX_RX_MODE_00_HIGH3			0x168
+#define QSERDES_V10_RX_RX_MODE_00_HIGH4			0x16c
 #define QSERDES_V10_RX_RX_MODE_01_LOW				0x170
 #define QSERDES_V10_RX_RX_MODE_01_HIGH				0x174
-#define QSERDES_V10_RX_RX_MODE_01_HIGH2				0x178
-#define QSERDES_V10_RX_RX_MODE_01_HIGH3				0x17c
-#define QSERDES_V10_RX_RX_MODE_01_HIGH4				0x180
+#define QSERDES_V10_RX_RX_MODE_01_HIGH2			0x178
+#define QSERDES_V10_RX_RX_MODE_01_HIGH3			0x17c
+#define QSERDES_V10_RX_RX_MODE_01_HIGH4			0x180
 #define QSERDES_V10_RX_RX_MODE_10_LOW				0x184
 #define QSERDES_V10_RX_RX_MODE_10_HIGH				0x188
-#define QSERDES_V10_RX_RX_MODE_10_HIGH2				0x18c
-#define QSERDES_V10_RX_RX_MODE_10_HIGH3				0x190
-#define QSERDES_V10_RX_RX_MODE_10_HIGH4				0x194
-#define QSERDES_V10_RX_DFE_CTLE_POST_CAL_OFFSET			0x1a4
+#define QSERDES_V10_RX_RX_MODE_10_HIGH2			0x18c
+#define QSERDES_V10_RX_RX_MODE_10_HIGH3			0x190
+#define QSERDES_V10_RX_RX_MODE_10_HIGH4			0x194
+#define QSERDES_V10_RX_DFE_EN_TIMER				0x1a0
+#define QSERDES_V10_RX_DFE_CTLE_POST_CAL_OFFSET		0x1a4
+#define QSERDES_V10_RX_DCC_CTRL1				0x1a8
+#define QSERDES_V10_RX_VTH_CODE				0x1b0
+#define QSERDES_V10_RX_SIGDET_CAL_CTRL1			0x1e4
 #define QSERDES_V10_RX_SIGDET_CAL_TRIM				0x1f8
 
 #endif
-- 
2.53.0



From mukesh.ojha at oss.qualcomm.com  Mon Aug  3 02:22:50 2026
From: mukesh.ojha at oss.qualcomm.com (Mukesh Ojha)
Date: Mon, 3 Aug 2026 14:52:50 +0530
Subject: [PATCH v3 0/4] phy: qcom: Introduce USB support for Hawi
In-Reply-To: <al4EG1FMrI0z1D7z@vaman>
References: <20260508213234.4643-1-ronak.raheja@oss.qualcomm.com>
 <chf57j2dkhhjzigpguldadfld5utepa4sxwbo7fjxk5nq37bhp@yci5agzh46ly>
 <al4EG1FMrI0z1D7z@vaman>
Message-ID: <20260803092250.ucoh5qnnnyurgyzu@hu-mojha-hyd.qualcomm.com>

On Mon, Jul 20, 2026 at 04:48:51PM +0530, Vinod Koul wrote:
> On 13-07-26, 13:12, Manivannan Sadhasivam wrote:
> > On Fri, May 08, 2026 at 02:32:30PM -0700, Ronak Raheja wrote:
> > > This series adds USB PHY support for the Hawi SoC. It documents the
> > > QMP USB3-DP combo PHY and M31 eUSB2 PHY bindings, adds the Hawi
> > > compatible to the DWC3 USB binding, and adds the PHY initialization
> > > sequences to the qmp-combo driver.
> > > 
> > 
> > Vinod, does this series look good to you?
> 
> It doesnt apply for me, can you folks rebase and resend

Sorry for the late response, sent the v4 here [1] rebased on next-20260731

[1]
https://lore.kernel.org/lkml/20260803091441.3040830-1-mukesh.ojha at oss.qualcomm.com/

-- 
-Mukesh Ojha


From sashiko-bot at kernel.org  Mon Aug  3 02:30:44 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 09:30:44 +0000
Subject: [PATCH v4 4/4] phy: qualcomm: qmp-combo: Add support for Hawi
 SoC
In-Reply-To: <20260803091441.3040830-5-mukesh.ojha@oss.qualcomm.com>
References: <20260803091441.3040830-1-mukesh.ojha@oss.qualcomm.com>
 <20260803091441.3040830-5-mukesh.ojha@oss.qualcomm.com>
Message-ID: <20260803093045.548671F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Missing DisplayPort initialization callbacks in `hawi_usb3dpphy_cfg` will cause a NULL pointer dereference when the DP PHY is initialized.
- [High] The AON toggle registers will not be cleared because `cfg->regs[QPHY_AON_TOGGLE_ENABLE]` evaluates to false for the Hawi V10 PHY.
--

--- Patch [4]: [PATCH v4 4/4] phy: qualcomm: qmp-combo: Add support for Hawi SoC ---
commit 74a7f87645b4d6da0a341014f5299e1ad20ae0d5
Author: Ronak Raheja <ronak.raheja at oss.qualcomm.com>

phy: qualcomm: qmp-combo: Add support for Hawi SoC

Add support for the USB3-DP combo PHY found on Hawi platform.
The QMP PHY for Hawi uses QSERDES V10 register layouts.

> diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-combo.c b/drivers/phy/qualcomm/phy-qcom-qmp-combo.c
> index cdcfad2e86b1d..ae80eee628c5e 100644
> --- a/drivers/phy/qualcomm/phy-qcom-qmp-combo.c
> +++ b/drivers/phy/qualcomm/phy-qcom-qmp-combo.c

[ ... ]

> @@ -2705,6 +2895,27 @@ static const struct qmp_phy_cfg x1e80100_usb3dpphy_cfg = {
>  	.regs			= qmp_v6_n4_usb3phy_regs_layout,
>  };
>  
> +static const struct qmp_phy_cfg hawi_usb3dpphy_cfg = {
> +	.offsets		= &qmp_combo_offsets_v10,
> +
> +	.serdes_tbl		= hawi_usb3_serdes_tbl,
> +	.serdes_tbl_num		= ARRAY_SIZE(hawi_usb3_serdes_tbl),
> +	.tx_tbl			= hawi_usb3_tx_tbl,
> +	.tx_tbl_num		= ARRAY_SIZE(hawi_usb3_tx_tbl),
> +	.rx_tbl			= hawi_usb3_rx_tbl,
> +	.rx_tbl_num		= ARRAY_SIZE(hawi_usb3_rx_tbl),
> +	.pcs_tbl		= hawi_usb3_pcs_tbl,
> +	.pcs_tbl_num		= ARRAY_SIZE(hawi_usb3_pcs_tbl),
> +	.pcs_usb_tbl		= hawi_usb3_pcs_usb_tbl,
> +	.pcs_usb_tbl_num	= ARRAY_SIZE(hawi_usb3_pcs_usb_tbl),
> +
> +	.regs			= qmp_v10_usb3phy_regs_layout,

[Severity: High]
Does this configuration miss the mandatory DisplayPort function pointers
like dp_aux_init, configure_dp_tx, and configure_dp_phy?

If a DisplayPort client calls phy_init() or phy_power_on(), the driver
executes qmp_combo_dp_init() and qmp_combo_dp_power_on() which
unconditionally call these function pointers. Because they are omitted here,
they will be NULL and cause a NULL pointer dereference regression.

> +	.reset_list		= msm8996_usb3phy_reset_l,
> +	.num_resets		= ARRAY_SIZE(msm8996_usb3phy_reset_l),
> +	.vreg_list		= qmp_phy_vreg_l,
> +	.num_vregs		= ARRAY_SIZE(qmp_phy_vreg_l),
> +};
> +
>  static const struct qmp_phy_cfg sm6350_usb3dpphy_cfg = {
>  	.offsets		= &qmp_combo_offsets_v3,

[ ... ]

> @@ -3706,9 +3922,9 @@ static int qmp_combo_com_init(struct qmp_combo *qmp, bool force)
>  			SW_USB3PHY_RESET_MUX | SW_USB3PHY_RESET);
>  
>  	/* override hardware control for reset of qmp phy */
> -	if (pcs_aon && cfg->regs[QPHY_AON_TOGGLE_ENABLE]) {
> -		qphy_clrbits(pcs_aon, cfg->regs[QPHY_AON_TOGGLE_ENABLE], 0x1);
> -		qphy_clrbits(pcs_aon, cfg->regs[QPHY_DP_AON_TOGGLE_ENABLE], 0x1);
> +	if (aon_toggle && cfg->regs[QPHY_AON_TOGGLE_ENABLE]) {

[Severity: High]
Will this condition fail to clear the AON toggle registers for the Hawi V10
PHY?

Since QPHY_V10_COM_AON_USB3_AON_TOGGLE_ENABLE is defined as 0x00,
cfg->regs[QPHY_AON_TOGGLE_ENABLE] evaluates to 0. This causes the condition
to fail and skips the qphy_clrbits() execution, leaving the hardware reset
control improperly configured and potentially causing a regression.

> +		qphy_clrbits(aon_toggle, cfg->regs[QPHY_AON_TOGGLE_ENABLE], 0x1);
> +		qphy_clrbits(aon_toggle, cfg->regs[QPHY_DP_AON_TOGGLE_ENABLE], 0x1);
>  	}
>  
>  	/* Use software based port select and switch on typec orientation */

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260803091441.3040830-1-mukesh.ojha at oss.qualcomm.com?part=4


From michael.riesch at collabora.com  Mon Aug  3 04:04:07 2026
From: michael.riesch at collabora.com (Michael Riesch)
Date: Mon, 3 Aug 2026 13:04:07 +0200
Subject: [PATCH v4 3/3] phy: rockchip: phy-rockchip-inno-csidphy: add
 clock lane phase tuning
In-Reply-To: <20260725-feature-mipi-csi-dphy-4k60-v4-3-5b2c4626d31e@wolfvision.net>
References: <20260725-feature-mipi-csi-dphy-4k60-v4-0-5b2c4626d31e@wolfvision.net>
 <20260725-feature-mipi-csi-dphy-4k60-v4-3-5b2c4626d31e@wolfvision.net>
Message-ID: <e4a4eef8-3c42-407b-a6b7-e2f9a32da29e@collabora.com>

Hi Gerald,

On 7/25/26 18:56, Gerald Loacker wrote:
> At high data rates like 4K60 (2500 Mbps), such as when using an
> LT6911GXD bridge chip on an RK3588 board, fixed default timing parameters
> can cause signal integrity issues and clock-data recovery failures.
> The driver currently lacks a mechanism to adjust the clock lane sampling
> phase to compensate for board-specific trace variations.
> 
> Resolve this by parsing and applying the optional 'rockchip,clk-lane-phase'
> device tree property. This enables board-specific tuning of the clock
> lane sampling phase in ~40 ps steps (range 0-7) to optimize link
> stability. If the property is absent, the driver falls back to the
> hardware default.
> 
> Signed-off-by: Gerald Loacker <gerald.loacker at wolfvision.net>

Reviewed-by: Michael Riesch <michael.riesch at collabora.com>

Thanks and best regards,
Michael

> ---
>  drivers/phy/rockchip/phy-rockchip-inno-csidphy.c | 25 ++++++++++++++++++++++++
>  1 file changed, 25 insertions(+)
> 
> diff --git a/drivers/phy/rockchip/phy-rockchip-inno-csidphy.c b/drivers/phy/rockchip/phy-rockchip-inno-csidphy.c
> index 5281f8dea0ad3..d989f1649b0e4 100644
> --- a/drivers/phy/rockchip/phy-rockchip-inno-csidphy.c
> +++ b/drivers/phy/rockchip/phy-rockchip-inno-csidphy.c
> @@ -69,6 +69,10 @@
>  #define RK1808_CSIDPHY_CLK_CALIB_EN		0x168
>  #define RK3568_CSIDPHY_CLK_CALIB_EN		0x168
>  
> +#define CSIDPHY_LANE_CLK_3_PHASE		0x38
> +#define CSIDPHY_CLK_PHASE_MASK			GENMASK(6, 4)
> +#define CSIDPHY_CLK_PHASE_DEFAULT		3
> +
>  #define RESETS_MAX				2
>  
>  /*
> @@ -151,6 +155,7 @@ struct rockchip_inno_csidphy {
>  	const struct dphy_drv_data *drv_data;
>  	struct phy_configure_opts_mipi_dphy config;
>  	u8 hsfreq;
> +	int clk_phase;
>  };
>  
>  static inline void write_grf_reg(struct rockchip_inno_csidphy *priv,
> @@ -304,6 +309,13 @@ static int rockchip_inno_csidphy_power_on(struct phy *phy)
>  		rockchip_inno_csidphy_ths_settle(priv, priv->hsfreq,
>  						 CSIDPHY_LANE_THS_SETTLE(i));
>  
> +	if (priv->clk_phase >= 0) {
> +		val = readl(priv->phy_base + CSIDPHY_LANE_CLK_3_PHASE);
> +		val &= ~CSIDPHY_CLK_PHASE_MASK;
> +		val |= FIELD_PREP(CSIDPHY_CLK_PHASE_MASK, priv->clk_phase);
> +		writel(val, priv->phy_base + CSIDPHY_LANE_CLK_3_PHASE);
> +	}
> +
>  	write_grf_reg(priv, GRF_DPHY_CSIPHY_CLKLANE_EN, 0x1);
>  	write_grf_reg(priv, GRF_DPHY_CSIPHY_DATALANE_EN,
>  		      GENMASK(priv->config.lanes - 1, 0));
> @@ -449,6 +461,7 @@ static int rockchip_inno_csidphy_probe(struct platform_device *pdev)
>  	struct device *dev = &pdev->dev;
>  	struct phy_provider *phy_provider;
>  	struct phy *phy;
> +	u32 phase;
>  	int ret;
>  
>  	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> @@ -464,6 +477,18 @@ static int rockchip_inno_csidphy_probe(struct platform_device *pdev)
>  		return -ENODEV;
>  	}
>  
> +	priv->clk_phase = -1;
> +	if (device_property_read_u32(dev, "rockchip,clk-lane-phase",
> +				     &phase) == 0) {
> +		if (phase > 7) {
> +			dev_warn(dev,
> +				 "invalid rockchip,clk-lane-phase %u, using default %u\n",
> +				 phase, CSIDPHY_CLK_PHASE_DEFAULT);
> +			phase = CSIDPHY_CLK_PHASE_DEFAULT;
> +		}
> +		priv->clk_phase = phase;
> +	}
> +
>  	priv->grf = syscon_regmap_lookup_by_phandle(dev->of_node,
>  						    "rockchip,grf");
>  	if (IS_ERR(priv->grf)) {
> 



From michael.riesch at collabora.com  Mon Aug  3 04:17:05 2026
From: michael.riesch at collabora.com (Michael Riesch)
Date: Mon, 3 Aug 2026 13:17:05 +0200
Subject: [PATCH v2 1/4] dt-bindings: phy: rockchip,rk3588-mipi-dcphy:
 support per-direction phys
In-Reply-To: <20260726-dcphy-rx-v1-v2-1-cf9cc34a412a@gmail.com>
References: <20260726-dcphy-rx-v1-v2-0-cf9cc34a412a@gmail.com>
 <20260726-dcphy-rx-v1-v2-1-cf9cc34a412a@gmail.com>
Message-ID: <82da3622-9c3a-454c-87bc-fb4ec7adb68d@collabora.com>

Hi Jason,

Thanks for your patches!

On 7/26/26 16:55, Jason Yang via B4 Relay wrote:
> From: Jason Yang <jason98166 at gmail.com>
> 
> The Samsung MIPI D-/C-PHY block on RK3588 drives a MIPI DSI transmitter
> and, on the same PHY, can receive a MIPI CSI camera. Describing both as
> independent consumers of one PHY requires the direction to be expressed
> in the devicetree.
> 
> Allow "#phy-cells" to be 1 or 2. The existing single-cell form still
> selects the PHY type and refers to the transmitter, so shipped DSI
> device trees stay valid; bumping the cell count unconditionally would
> invalidate them. A second cell adds the direction, letting a DSI (TX)
> and a CSI (RX) reference the same PHY node as separate consumers.
> 
> Add include/dt-bindings/phy/rockchip,rk3588-mipi-dcphy.h for the
> RK_DCPHY_DIR_TX and RK_DCPHY_DIR_RX direction constants.
> 
> Signed-off-by: Jason Yang <jason98166 at gmail.com>
> Assisted-by: Claude:claude-opus-4-8
> ---
>  .../bindings/phy/rockchip,rk3588-mipi-dcphy.yaml   | 22 ++++++++++++++++------
>  .../dt-bindings/phy/rockchip,rk3588-mipi-dcphy.h   | 13 +++++++++++++
>  2 files changed, 29 insertions(+), 6 deletions(-)
> 
> diff --git a/Documentation/devicetree/bindings/phy/rockchip,rk3588-mipi-dcphy.yaml b/Documentation/devicetree/bindings/phy/rockchip,rk3588-mipi-dcphy.yaml
> index c8ff5ba22a86..68a43db9c669 100644
> --- a/Documentation/devicetree/bindings/phy/rockchip,rk3588-mipi-dcphy.yaml
> +++ b/Documentation/devicetree/bindings/phy/rockchip,rk3588-mipi-dcphy.yaml
> @@ -20,12 +20,22 @@ properties:
>      maxItems: 1
>  
>    "#phy-cells":
> -    const: 1
> +    enum: [1, 2]
>      description: |
> -      Argument is mode to operate in. Supported modes are:
> -        - PHY_TYPE_DPHY
> -        - PHY_TYPE_CPHY
> -      See include/dt-bindings/phy/phy.h for constants.
> +      A single cell selects the PHY type and always refers to the
> +      transmitter (DSI) direction. It is kept for backwards compatibility
> +      with existing device trees:
> +        - <PHY_TYPE_DPHY>
> +        - <PHY_TYPE_CPHY>
> +
> +      Two cells select the PHY type and the direction, so that the
> +      transmitter (DSI) and the receiver (CSI) can be described as
> +      independent consumers of the same PHY:
> +        - <PHY_TYPE_DPHY RK_DCPHY_DIR_TX>
> +        - <PHY_TYPE_DPHY RK_DCPHY_DIR_RX>

How is that Rockchip specific? As you state above this is either MIPI
CSI or MIPI DSI, two perfectly standard protocols.

@all Would it be reasonable to introduce PHY_TYPE_CSI and PHY_TYPE_DSI?

The rest of the patch LGTM.

Best regards,
Michael

> +
> +      See include/dt-bindings/phy/phy.h and
> +      include/dt-bindings/phy/rockchip,rk3588-mipi-dcphy.h for constants.
>  
>    clocks:
>      maxItems: 2
> @@ -82,6 +92,6 @@ examples:
>                   <&cru SRST_S_MIPI_DCPHY0>;
>          reset-names = "m_phy", "apb", "grf", "s_phy";
>          rockchip,grf = <&mipidcphy0_grf>;
> -        #phy-cells = <1>;
> +        #phy-cells = <2>;
>        };
>      };
> diff --git a/include/dt-bindings/phy/rockchip,rk3588-mipi-dcphy.h b/include/dt-bindings/phy/rockchip,rk3588-mipi-dcphy.h
> new file mode 100644
> index 000000000000..5029838fa275
> --- /dev/null
> +++ b/include/dt-bindings/phy/rockchip,rk3588-mipi-dcphy.h
> @@ -0,0 +1,13 @@
> +/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
> +/*
> + * Copyright (c) 2026 Jason Yang <jason98166 at gmail.com>
> + */
> +
> +#ifndef _DT_BINDINGS_PHY_ROCKCHIP_RK3588_MIPI_DCPHY_H
> +#define _DT_BINDINGS_PHY_ROCKCHIP_RK3588_MIPI_DCPHY_H
> +
> +/* Direction of a Rockchip Samsung MIPI D-/C-PHY combo block */
> +#define RK_DCPHY_DIR_TX		0
> +#define RK_DCPHY_DIR_RX		1
> +
> +#endif /* _DT_BINDINGS_PHY_ROCKCHIP_RK3588_MIPI_DCPHY_H */
> 



From michael.riesch at collabora.com  Mon Aug  3 05:05:29 2026
From: michael.riesch at collabora.com (Michael Riesch)
Date: Mon, 3 Aug 2026 14:05:29 +0200
Subject: [PATCH v2 3/4] phy: rockchip-samsung-dcphy: model TX and RX as
 separate PHYs
In-Reply-To: <20260726-dcphy-rx-v1-v2-3-cf9cc34a412a@gmail.com>
References: <20260726-dcphy-rx-v1-v2-0-cf9cc34a412a@gmail.com>
 <20260726-dcphy-rx-v1-v2-3-cf9cc34a412a@gmail.com>
Message-ID: <7e55b8bc-d11d-4b25-99ee-d5d181be158f@collabora.com>

Hi Jason,

Thanks for the patch!

On 7/26/26 16:55, Jason Yang via B4 Relay wrote:
> From: Jason Yang <jason98166 at gmail.com>
> 
> The DC-PHY drives a MIPI DSI transmitter and a MIPI CSI receiver, and on
> RK3588 both can be wired to the same PHY as independent consumers. The
> PHY core reference-counts power_on() per struct phy, so a single struct
> phy cannot bring the two directions up independently.
> 
> Register one struct phy per direction and move the per-direction state
> (direction, PHY type, lane count and powered flag) into its own driver
> data. of_xlate() accepts the legacy single cell as the transmitter and
> an optional second cell that selects the direction, so existing
> single-cell DSI device trees keep resolving to the transmitter.
> configure() rejects the not yet supported receiver before touching any
> state, and each direction keeps its own lane count.
> 
> The PHY's common block contains a single BIAS block shared by both
> directions (RK3588 TRM section 22.2), so it must be programmed only
> once. Whichever direction powers on first enables it; each direction
> records its powered state under the per-provider mutex so that powering
> one direction on does not disturb an already running one. The APB reset
> is issued on the transmitter bring-up path.
> The receiver bring-up itself is added in a later change.
> 
> Signed-off-by: Jason Yang <jason98166 at gmail.com>
> Assisted-by: Claude:claude-opus-4-8
> ---
>  drivers/phy/rockchip/phy-rockchip-samsung-dcphy.c | 143 +++++++++++++++-------
>  1 file changed, 102 insertions(+), 41 deletions(-)
> 
> diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-dcphy.c b/drivers/phy/rockchip/phy-rockchip-samsung-dcphy.c
> index d0d77421bd4b..95eb1200cab4 100644
> --- a/drivers/phy/rockchip/phy-rockchip-samsung-dcphy.c
> +++ b/drivers/phy/rockchip/phy-rockchip-samsung-dcphy.c
> @@ -5,6 +5,7 @@
>   *      Guochun Huang <hero.huang at rock-chips.com>
>   */
>  
> +#include <dt-bindings/phy/rockchip,rk3588-mipi-dcphy.h>
>  #include <dt-bindings/phy/phy.h>
>  #include <linux/bitfield.h>
>  #include <linux/clk.h>
> @@ -13,6 +14,7 @@
>  #include <linux/kernel.h>
>  #include <linux/mfd/syscon.h>
>  #include <linux/module.h>
> +#include <linux/mutex.h>
>  #include <linux/of.h>
>  #include <linux/phy/phy.h>
>  #include <linux/platform_device.h>
> @@ -280,6 +282,19 @@ struct samsung_mipi_dcphy_plat_data {
>  	u32 dphy_tx_max_lane_kbps;
>  };
>  
> +struct samsung_mipi_dcphy;
> +
> +/* One PHY per direction: transmitter (DSI) and receiver (CSI). */
> +struct samsung_mipi_dcphy_dir {

This may be bike-shedding, but I am not a big fan of the notion of
"direction". This is a combo PHY that in essence contains different
PHYs, so why not call this "samsung_mipi_phy" or something?

> +	struct phy *phy;
> +	struct samsung_mipi_dcphy *parent;
> +	u8 dir;
> +	u8 type;
> +	unsigned int lanes;
> +	/* Written under the parent's lock. */
> +	bool powered;
> +};
> +
>  struct samsung_mipi_dcphy {
>  	struct device *dev;
>  	struct clk *ref_clk;
> @@ -290,9 +305,9 @@ struct samsung_mipi_dcphy {
>  	struct reset_control *s_phy_rst;
>  	struct reset_control *apb_rst;
>  	struct reset_control *grf_apb_rst;
> -	unsigned int lanes;
> -	struct phy *phy;
> -	u8 type;
> +	struct samsung_mipi_dcphy_dir phys[2];

This would look like this then:

	struct samsung_mipi_phy phys[2];

which seems clean and simple to me.

> +	/* Serialises the two directions' access to the shared PHY state. */
> +	struct mutex lock;
>  
>  	const struct samsung_mipi_dcphy_plat_data *pdata;
>  	struct {
> @@ -995,7 +1010,7 @@ static void samsung_mipi_dphy_lane_enable(struct samsung_mipi_dcphy *samsung)
>  	regmap_update_bits(samsung->regmap, DPHY_MC_GNR_CON0,
>  			   PHY_ENABLE, PHY_ENABLE);
>  
> -	switch (samsung->lanes) {
> +	switch (samsung->phys[RK_DCPHY_DIR_TX].lanes) {
>  	case 4:
>  		regmap_write(samsung->regmap, DPHY_MD3_GNR_CON1,
>  			     T_PHY_READY(0x2000));
> @@ -1026,7 +1041,7 @@ static void samsung_mipi_dphy_lane_enable(struct samsung_mipi_dcphy *samsung)
>  
>  static void samsung_mipi_dphy_lane_disable(struct samsung_mipi_dcphy *samsung)
>  {
> -	switch (samsung->lanes) {
> +	switch (samsung->phys[RK_DCPHY_DIR_TX].lanes) {
>  	case 4:
>  		regmap_update_bits(samsung->regmap, DPHY_MD3_GNR_CON0,
>  				   PHY_ENABLE, 0);
> @@ -1334,11 +1349,23 @@ samsung_mipi_dphy_data_lane_timing_init(struct samsung_mipi_dcphy *samsung)
>  
>  static int samsung_mipi_dphy_tx_power_on(struct samsung_mipi_dcphy *samsung)
>  {
> +	bool first = !samsung->phys[RK_DCPHY_DIR_RX].powered;

Note to myself: the lock is acquired in the calling method, all is well.

>  	int ret;
>  
> +	/*
> +	 * The shared BIAS block is brought up by whichever direction powers
> +	 * on first, leaving an already active peer undisturbed.
> +	 */
> +	if (first) {
> +		reset_control_assert(samsung->apb_rst);
> +		udelay(1);
> +		reset_control_deassert(samsung->apb_rst);
> +	}
> +
>  	reset_control_assert(samsung->m_phy_rst);
>  
> -	samsung_mipi_dcphy_bias_block_enable(samsung);
> +	if (first)
> +		samsung_mipi_dcphy_bias_block_enable(samsung);

This approach with the "first" variable seems unintuitive. If you need
reference counting on the BIAS block, then use a state variable
"bias_powered", use a mutex that protects it, and call
samsung_mipi_dcphy_bias_block_{en,dis}able (from
samsung_mipi_dcphy_power{on, off}) unconditionally. Those methods shall
acquire the mutex, check the state variable, {en,dis}able the BIAS
block, set the state variable accordingly, release the mutex.


>  	samsung_mipi_dcphy_pll_configure(samsung);
>  	samsung_mipi_dphy_clk_lane_timing_init(samsung);
>  	samsung_mipi_dphy_data_lane_timing_init(samsung);
> @@ -1368,34 +1395,44 @@ static int samsung_mipi_dphy_tx_power_off(struct samsung_mipi_dcphy *samsung)
>  
>  static int samsung_mipi_dcphy_power_on(struct phy *phy)
>  {
> -	struct samsung_mipi_dcphy *samsung = phy_get_drvdata(phy);
> -
> -	reset_control_assert(samsung->apb_rst);
> -	udelay(1);
> -	reset_control_deassert(samsung->apb_rst);
> +	struct samsung_mipi_dcphy_dir *pd = phy_get_drvdata(phy);

Again, this may be bike-shedding, but "pd" seems misleading to me.
Please reconsider the naming. Maybe "samsung_phy" for the individual PHY
and "samsung" for the complete combo PHY? Or "phy_data" and
"dcphy_data"? ...?

> +	struct samsung_mipi_dcphy *samsung = pd->parent;
> +	int ret;
>  
> -	switch (samsung->type) {
> -	case PHY_TYPE_DPHY:
> -		return samsung_mipi_dphy_tx_power_on(samsung);
> -	default:
> -		/* CPHY part to be implemented later */
> +	if (pd->type != PHY_TYPE_DPHY)
>  		return -EOPNOTSUPP;
> -	}
>  
> -	return 0;
> +	mutex_lock(&samsung->lock);
> +	if (pd->dir == RK_DCPHY_DIR_RX)
> +		ret = -EOPNOTSUPP;
> +	else
> +		ret = samsung_mipi_dphy_tx_power_on(samsung);
> +	if (!ret)
> +		pd->powered = true;

Not sure whether you actually need to track the power status of the
individual PHYs (maybe you actually need to track the BIAS block status
alone) but anyway I would appreciate if
"samsung_mipi_dphy_tx_power_{on,off}" would set the "pd->powered" variable.

Best regards,
Michael


> +	mutex_unlock(&samsung->lock);
> +
> +	return ret;
>  }
>  
>  static int samsung_mipi_dcphy_power_off(struct phy *phy)
>  {
> -	struct samsung_mipi_dcphy *samsung = phy_get_drvdata(phy);
> +	struct samsung_mipi_dcphy_dir *pd = phy_get_drvdata(phy);
> +	struct samsung_mipi_dcphy *samsung = pd->parent;
> +	int ret;
>  
> -	switch (samsung->type) {
> -	case PHY_TYPE_DPHY:
> -		return samsung_mipi_dphy_tx_power_off(samsung);
> -	default:
> -		/* CPHY part to be implemented later */
> +	if (pd->type != PHY_TYPE_DPHY)
>  		return -EOPNOTSUPP;
> -	}
> +
> +	if (pd->dir == RK_DCPHY_DIR_RX)
> +		return -EOPNOTSUPP;
> +
> +	mutex_lock(&samsung->lock);
> +	ret = samsung_mipi_dphy_tx_power_off(samsung);
> +	if (!ret)
> +		pd->powered = false;
> +	mutex_unlock(&samsung->lock);
> +
> +	return ret;
>  }
>  
>  static int
> @@ -1488,10 +1525,14 @@ samsung_mipi_dcphy_pll_calc_rate(struct samsung_mipi_dcphy *samsung,
>  static int samsung_mipi_dcphy_configure(struct phy *phy,
>  					union phy_configure_opts *opts)
>  {
> -	struct samsung_mipi_dcphy *samsung = phy_get_drvdata(phy);
> +	struct samsung_mipi_dcphy_dir *pd = phy_get_drvdata(phy);
> +	struct samsung_mipi_dcphy *samsung = pd->parent;
>  	unsigned long long target_rate = opts->mipi_dphy.hs_clk_rate;
>  
> -	samsung->lanes = opts->mipi_dphy.lanes > 4 ? 4 : opts->mipi_dphy.lanes;
> +	if (pd->dir == RK_DCPHY_DIR_RX)
> +		return -EOPNOTSUPP;
> +
> +	pd->lanes = opts->mipi_dphy.lanes > 4 ? 4 : opts->mipi_dphy.lanes;
>  
>  	samsung_mipi_dcphy_pll_calc_rate(samsung, target_rate);
>  	opts->mipi_dphy.hs_clk_rate = samsung->pll.rate;
> @@ -1501,16 +1542,16 @@ static int samsung_mipi_dcphy_configure(struct phy *phy,
>  
>  static int samsung_mipi_dcphy_init(struct phy *phy)
>  {
> -	struct samsung_mipi_dcphy *samsung = phy_get_drvdata(phy);
> +	struct samsung_mipi_dcphy_dir *pd = phy_get_drvdata(phy);
>  
> -	return pm_runtime_resume_and_get(samsung->dev);
> +	return pm_runtime_resume_and_get(pd->parent->dev);
>  }
>  
>  static int samsung_mipi_dcphy_exit(struct phy *phy)
>  {
> -	struct samsung_mipi_dcphy *samsung = phy_get_drvdata(phy);
> +	struct samsung_mipi_dcphy_dir *pd = phy_get_drvdata(phy);
>  
> -	pm_runtime_put(samsung->dev);
> +	pm_runtime_put(pd->parent->dev);
>  
>  	return 0;
>  }
> @@ -1536,19 +1577,29 @@ static struct phy *samsung_mipi_dcphy_xlate(struct device *dev,
>  					    const struct of_phandle_args *args)
>  {
>  	struct samsung_mipi_dcphy *samsung = dev_get_drvdata(dev);
> +	struct samsung_mipi_dcphy_dir *pd;
> +	u8 dir = RK_DCPHY_DIR_TX;
>  
> -	if (args->args_count != 1) {
> +	if (args->args_count < 1 || args->args_count > 2) {
>  		dev_err(dev, "invalid number of arguments\n");
>  		return ERR_PTR(-EINVAL);
>  	}
>  
> -	if (samsung->type != PHY_NONE && samsung->type != args->args[0])
> -		dev_warn(dev, "phy type select %d overwriting type %d\n",
> -			 args->args[0], samsung->type);
> +	if (args->args_count == 2) {
> +		if (args->args[1] > RK_DCPHY_DIR_RX) {
> +			dev_err(dev, "invalid direction %u\n", args->args[1]);
> +			return ERR_PTR(-EINVAL);
> +		}
> +		dir = args->args[1];
> +	}
>  
> -	samsung->type = args->args[0];
> +	pd = &samsung->phys[dir];
> +	if (pd->type != PHY_NONE && pd->type != args->args[0])
> +		dev_warn(dev, "phy type select %u overwriting type %u\n",
> +			 args->args[0], pd->type);
> +	pd->type = args->args[0];
>  
> -	return samsung->phy;
> +	return pd->phy;
>  }
>  
>  static int samsung_mipi_dcphy_probe(struct platform_device *pdev)
> @@ -1559,6 +1610,7 @@ static int samsung_mipi_dcphy_probe(struct platform_device *pdev)
>  	struct phy_provider *phy_provider;
>  	struct resource *res;
>  	void __iomem *regs;
> +	unsigned int i;
>  	int ret;
>  
>  	samsung = devm_kzalloc(dev, sizeof(*samsung), GFP_KERNEL);
> @@ -1568,6 +1620,7 @@ static int samsung_mipi_dcphy_probe(struct platform_device *pdev)
>  	samsung->dev = dev;
>  	samsung->pdata = device_get_match_data(dev);
>  	platform_set_drvdata(pdev, samsung);
> +	mutex_init(&samsung->lock);
>  
>  	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
>  	regs = devm_ioremap_resource(dev, res);
> @@ -1613,11 +1666,19 @@ static int samsung_mipi_dcphy_probe(struct platform_device *pdev)
>  		return dev_err_probe(dev, PTR_ERR(samsung->grf_apb_rst),
>  				     "Failed to get system grf_apb_rst control\n");
>  
> -	samsung->phy = devm_phy_create(dev, NULL, &samsung_mipi_dcphy_ops);
> -	if (IS_ERR(samsung->phy))
> -		return dev_err_probe(dev, PTR_ERR(samsung->phy), "Failed to create MIPI DC-PHY\n");
> +	for (i = 0; i < ARRAY_SIZE(samsung->phys); i++) {
> +		struct phy *phy = devm_phy_create(dev, NULL,
> +						  &samsung_mipi_dcphy_ops);
>  
> -	phy_set_drvdata(samsung->phy, samsung);
> +		if (IS_ERR(phy))
> +			return dev_err_probe(dev, PTR_ERR(phy),
> +					     "Failed to create MIPI DC-PHY\n");
> +
> +		samsung->phys[i].phy = phy;
> +		samsung->phys[i].parent = samsung;
> +		samsung->phys[i].dir = i;
> +		phy_set_drvdata(phy, &samsung->phys[i]);
> +	}
>  
>  	ret = devm_pm_runtime_enable(dev);
>  	if (ret)
> 



From ustc.gu at gmail.com  Mon Aug  3 05:43:24 2026
From: ustc.gu at gmail.com (Felix Gu)
Date: Mon, 03 Aug 2026 20:43:24 +0800
Subject: [PATCH] phy: renesas: rcar-gen2: Fix double of_node_put on phy
 creation failure
Message-ID: <20260803-rcar-gen2-v1-1-9aa35c36d7d7@gmail.com>

for_each_child_of_node_scoped() releases the node reference on scope
exit, so the explicit of_node_put(np) in the devm_phy_create() error
path drops it twice.

Drop the redundant of_node_put() and let the scoped cleanup handle it.

Fixes: b64b32791fb5 ("phy: renesas: rcar-gen2: Simplify with scoped for each OF child loop")
Signed-off-by: Felix Gu <ustc.gu at gmail.com>
---
 drivers/phy/renesas/phy-rcar-gen2.c | 1 -
 1 file changed, 1 deletion(-)

diff --git a/drivers/phy/renesas/phy-rcar-gen2.c b/drivers/phy/renesas/phy-rcar-gen2.c
index 6c671254c625..5a272e25e051 100644
--- a/drivers/phy/renesas/phy-rcar-gen2.c
+++ b/drivers/phy/renesas/phy-rcar-gen2.c
@@ -405,7 +405,6 @@ static int rcar_gen2_phy_probe(struct platform_device *pdev)
 						   data->gen2_phy_ops);
 			if (IS_ERR(phy->phy)) {
 				dev_err(dev, "Failed to create PHY\n");
-				of_node_put(np);
 				return PTR_ERR(phy->phy);
 			}
 			phy_set_drvdata(phy->phy, phy);

---
base-commit: 415606a7be939835db9b0d6b711887586646346d
change-id: 20260803-rcar-gen2-140f014cd384

Best regards,
--  
Felix Gu <ustc.gu at gmail.com>



From sashiko-bot at kernel.org  Mon Aug  3 05:54:48 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 12:54:48 +0000
Subject: [PATCH] phy: renesas: rcar-gen2: Fix double of_node_put on phy
 creation failure
In-Reply-To: <20260803-rcar-gen2-v1-1-9aa35c36d7d7@gmail.com>
References: <20260803-rcar-gen2-v1-1-9aa35c36d7d7@gmail.com>
Message-ID: <20260803125449.C685C1F000E9@smtp.kernel.org>

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

Pre-existing issues:
- [High] The driver saves a device node pointer (`np`) in `channel->of_node` without acquiring a reference, leading to a potential dangling pointer.
- [High] Potential out-of-bounds array access when populating the `drv->channels` array due to a time-of-check to time-of-use (TOCTOU) race condition.
- [High] The probe function incorrectly returns success (0) when a child node has an out-of-bounds 'reg' property.
--

--- Patch [1]: [PATCH] phy: renesas: rcar-gen2: Fix double of_node_put on phy creation failure ---
commit 007ce311e8760f22502bbc2b270c02df1d8b9ee0
Author: Felix Gu <ustc.gu at gmail.com>

phy: renesas: rcar-gen2: Fix double of_node_put on phy creation failure

This commit removes a redundant of_node_put() in the error path of
devm_phy_create(). The loop uses for_each_child_of_node_scoped(), which
automatically handles releasing the device node reference on scope exit.

> diff --git a/drivers/phy/renesas/phy-rcar-gen2.c b/drivers/phy/renesas/phy-rcar-gen2.c
> index 6c671254c6258..5a272e25e051c 100644
> --- a/drivers/phy/renesas/phy-rcar-gen2.c
> +++ b/drivers/phy/renesas/phy-rcar-gen2.c
> @@ -405,7 +405,6 @@ static int rcar_gen2_phy_probe(struct platform_device *pdev)

[Severity: High]
This is a pre-existing issue, but does this loop in rcar_gen2_phy_probe()
have a potential out-of-bounds array access?

The driver allocates the channels array based on of_get_child_count():

    drv->num_channels = of_get_child_count(dev->of_node);
    drv->channels = devm_kcalloc(dev, drv->num_channels, ...);

Then it iterates over the child nodes:

    for_each_child_of_node_scoped(dev->of_node, np) {
        struct rcar_gen2_channel *channel = drv->channels + i;
        ...
        i++;
    }

If a child node is added concurrently to the device tree (e.g., via an
overlay) between the count and the loop, could i exceed the allocated size
of the array?

[Severity: High]
This is a pre-existing issue, but does this code save a device node pointer
without taking a reference?

Inside the same for_each_child_of_node_scoped() loop in
rcar_gen2_phy_probe():

    channel->of_node = np;

The iterator automatically drops the reference to np when advancing to the
next iteration or when the loop terminates. If the device tree overlay is
removed and the node is freed, could this result in a dangling pointer when
accessed later?

Should it use of_node_get() to increment the reference count?

[Severity: High]
This is a pre-existing issue, but can this error handling block return 0
instead of a proper error code?

When validating the "reg" property in rcar_gen2_phy_probe():

    error = of_property_read_u32(np, "reg", &channel_num);
    if (error || channel_num >= data->num_channels) {
        dev_err(dev, "Invalid \"reg\" property\n");
        return error;
    }

If of_property_read_u32() succeeds, error is set to 0. If the channel_num is
subsequently found to be out of bounds, the condition evaluates to true.

Would returning error here falsely signal to the driver core that the probe
succeeded?

>  						   data->gen2_phy_ops);
>  			if (IS_ERR(phy->phy)) {
>  				dev_err(dev, "Failed to create PHY\n");
> -				of_node_put(np);
>  				return PTR_ERR(phy->phy);
>  			}
>  			phy_set_drvdata(phy->phy, phy);

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260803-rcar-gen2-v1-1-9aa35c36d7d7 at gmail.com?part=1


From ustc.gu at gmail.com  Mon Aug  3 06:07:27 2026
From: ustc.gu at gmail.com (Felix Gu)
Date: Mon, 03 Aug 2026 21:07:27 +0800
Subject: [PATCH] phy: renesas: rcar-gen2: Return -EINVAL for out-of-range
 channel reg
Message-ID: <20260803-rcar-gen2-1-v1-1-4796ea6e76d8@gmail.com>

When of_property_read_u32() succeeds but channel_num exceeds
data->num_channels, rcar_gen2_phy_probe() returns error which is 0,
so probe reports success even though no PHY provider is registered.

Return -EINVAL in that case.

Fixes: 1233f59f745b ("phy: Renesas R-Car Gen2 PHY driver")
Signed-off-by: Felix Gu <ustc.gu at gmail.com>
---
 drivers/phy/renesas/phy-rcar-gen2.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/phy/renesas/phy-rcar-gen2.c b/drivers/phy/renesas/phy-rcar-gen2.c
index 6c671254c625..f0858d8cc705 100644
--- a/drivers/phy/renesas/phy-rcar-gen2.c
+++ b/drivers/phy/renesas/phy-rcar-gen2.c
@@ -390,7 +390,7 @@ static int rcar_gen2_phy_probe(struct platform_device *pdev)
 		error = of_property_read_u32(np, "reg", &channel_num);
 		if (error || channel_num >= data->num_channels) {
 			dev_err(dev, "Invalid \"reg\" property\n");
-			return error;
+			return error ?: -EINVAL;
 		}
 		channel->select_mask = select_mask[channel_num];
 

---
base-commit: 415606a7be939835db9b0d6b711887586646346d
change-id: 20260803-rcar-gen2-1-98f440b28b35

Best regards,
--  
Felix Gu <ustc.gu at gmail.com>



From palash.kambar at oss.qualcomm.com  Mon Aug  3 06:08:50 2026
From: palash.kambar at oss.qualcomm.com (palash.kambar at oss.qualcomm.com)
Date: Mon,  3 Aug 2026 18:38:50 +0530
Subject: [PATCH v5 0/2] Add Hawi UFS PHY and Controller support
Message-ID: <20260803130852.1432714-1-palash.kambar@oss.qualcomm.com>

From: Palash Kambar <palash.kambar at oss.qualcomm.com>

This series introduces devicetree binding documentation and PHY
initialization support required to enable UFS on this platform.

  1. Devicetree binding documentation for the QMP UFS PHY
     used on Qualcomm Hawi.
  2. Initialization sequence tables and configuration required
     for the QMP UFS PHY on Hawi SoC.

---
changes from V1
1) Addressed Dmitry's comments to fix versioning for PCS and qserdes.
2) Addressed Mani's comments and fixed missed compatible string and
   binding name correction.

changes from V2
1) Addressed Dmitry's comments to remove whitespace and stray line.

changes from V3:
1) Addressed Vinod's comment on missing macros.
2) Binding patch for UFS controller has been merged, so removed here.

changes from V4:
1) Rebased code to tip.

Palash Kambar (2):
  phy: qcom-qmp-ufs: Add UFS PHY support on Hawi
  dt-bindings: phy: qcom,sc8280xp-qmp-ufs-phy: Add Hawi UFS PHY
    compatible

 .../phy/qcom,sc8280xp-qmp-ufs-phy.yaml        |   2 +
 .../phy/qualcomm/phy-qcom-qmp-pcs-ufs-v7.h    |  24 +++
 .../qualcomm/phy-qcom-qmp-qserdes-com-v8.h    |  13 +-
 .../phy-qcom-qmp-qserdes-txrx-ufs-v8.h        |  37 +++++
 drivers/phy/qualcomm/phy-qcom-qmp-ufs.c       | 139 ++++++++++++++++++
 5 files changed, 214 insertions(+), 1 deletion(-)
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-pcs-ufs-v7.h
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-qserdes-txrx-ufs-v8.h

-- 
2.34.1



From krzysztof.kozlowski at oss.qualcomm.com  Mon Aug  3 06:09:05 2026
From: krzysztof.kozlowski at oss.qualcomm.com (Krzysztof Kozlowski)
Date: Mon, 3 Aug 2026 15:09:05 +0200
Subject: [PATCH] phy: renesas: rcar-gen2: Fix double of_node_put on phy
 creation failure
In-Reply-To: <20260803-rcar-gen2-v1-1-9aa35c36d7d7@gmail.com>
References: <20260803-rcar-gen2-v1-1-9aa35c36d7d7@gmail.com>
Message-ID: <862f47fa-628b-49dd-b8a9-873955ce5a13@oss.qualcomm.com>

On 03/08/2026 14:43, Felix Gu wrote:
> for_each_child_of_node_scoped() releases the node reference on scope
> exit, so the explicit of_node_put(np) in the devm_phy_create() error
> path drops it twice.
> 
> Drop the redundant of_node_put() and let the scoped cleanup handle it.
> 
> Fixes: b64b32791fb5 ("phy: renesas: rcar-gen2: Simplify with scoped for each OF child loop")
> Signed-off-by: Felix Gu <ustc.gu at gmail.com>
> ---
>  drivers/phy/renesas/phy-rcar-gen2.c | 1 -
>  1 file changed, 1 deletion(-)
> 


Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski at oss.qualcomm.com>

Best regards,
Krzysztof


From palash.kambar at oss.qualcomm.com  Mon Aug  3 06:08:51 2026
From: palash.kambar at oss.qualcomm.com (palash.kambar at oss.qualcomm.com)
Date: Mon,  3 Aug 2026 18:38:51 +0530
Subject: [PATCH v5 1/2] phy: qcom-qmp-ufs: Add UFS PHY support on Hawi
In-Reply-To: <20260803130852.1432714-1-palash.kambar@oss.qualcomm.com>
References: <20260803130852.1432714-1-palash.kambar@oss.qualcomm.com>
Message-ID: <20260803130852.1432714-2-palash.kambar@oss.qualcomm.com>

From: Palash Kambar <palash.kambar at oss.qualcomm.com>

Add the init sequence tables and config for the UFS QMP phy found in
the Hawi SoC.

Reviewed-by: Manivannan Sadhasivam <mani at kernel.org>
Reviewed-by: Dmitry Baryshkov <dmitry.baryshkov at oss.qualcomm.com>
Signed-off-by: Palash Kambar <palash.kambar at oss.qualcomm.com>
---
 .../phy/qualcomm/phy-qcom-qmp-pcs-ufs-v7.h    |  24 +++
 .../qualcomm/phy-qcom-qmp-qserdes-com-v8.h    |  13 +-
 .../phy-qcom-qmp-qserdes-txrx-ufs-v8.h        |  37 +++++
 drivers/phy/qualcomm/phy-qcom-qmp-ufs.c       | 139 ++++++++++++++++++
 4 files changed, 212 insertions(+), 1 deletion(-)
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-pcs-ufs-v7.h
 create mode 100644 drivers/phy/qualcomm/phy-qcom-qmp-qserdes-txrx-ufs-v8.h

diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-pcs-ufs-v7.h b/drivers/phy/qualcomm/phy-qcom-qmp-pcs-ufs-v7.h
new file mode 100644
index 000000000000..e80d3dd6a190
--- /dev/null
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-pcs-ufs-v7.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2026, The Linux Foundation. All rights reserved.
+ */
+
+#ifndef QCOM_PHY_QMP_PCS_UFS_V7_H_
+#define QCOM_PHY_QMP_PCS_UFS_V7_H_
+
+/* Only for QMP V7 PHY - UFS PCS registers */
+#define QPHY_V7_PCS_UFS_PHY_START			0x000
+#define QPHY_V7_PCS_UFS_POWER_DOWN_CONTROL		0x004
+#define QPHY_V7_PCS_UFS_SW_RESET			0x008
+#define QPHY_V7_PCS_UFS_PCS_CTRL1			0x01C
+#define QPHY_V7_PCS_UFS_PLL_CNTL			0x028
+#define QPHY_V7_PCS_UFS_TX_LARGE_AMP_DRV_LVL		0x02C
+#define QPHY_V7_PCS_UFS_TX_HSGEAR_CAPABILITY		0x060
+#define QPHY_V7_PCS_UFS_RX_HSGEAR_CAPABILITY		0x094
+#define QPHY_V7_PCS_UFS_LINECFG_DISABLE			0x140
+#define QPHY_V7_PCS_UFS_RX_SIGDET_CTRL2			0x150
+#define QPHY_V7_PCS_UFS_READY_STATUS			0x16c
+#define QPHY_V7_PCS_UFS_TX_MID_TERM_CTRL1		0x1b8
+#define QPHY_V7_PCS_UFS_MULTI_LANE_CTRL1		0x1c0
+
+#endif
diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-com-v8.h b/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-com-v8.h
index d8ac4c4a2c31..d416113bcb3c 100644
--- a/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-com-v8.h
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-com-v8.h
@@ -1,6 +1,6 @@
 /* SPDX-License-Identifier: GPL-2.0 */
 /*
- * Copyright (c) 2025 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2026, The Linux Foundation. All rights reserved.
  */
 
 #ifndef QCOM_PHY_QMP_QSERDES_COM_V8_H_
@@ -71,5 +71,16 @@
 #define QSERDES_V8_COM_ADDITIONAL_MISC			0x1b4
 #define QSERDES_V8_COM_CMN_STATUS			0x2c8
 #define QSERDES_V8_COM_C_READY_STATUS			0x2f0
+#define QSERDES_V8_COM_PLL_IVCO_MODE1				0xf8
+#define QSERDES_V8_COM_CMN_IETRIM				0xfc
+#define QSERDES_V8_COM_CMN_IPTRIM				0x100
+#define QSERDES_V8_COM_VCO_TUNE_CTRL				0x13c
+#define QSERDES_V8_COM_ADAPTIVE_ANALOG_CONFIG			0x268
+#define QSERDES_V8_COM_CP_CTRL_ADAPTIVE_MODE0			0x26c
+#define QSERDES_V8_COM_PLL_RCCTRL_ADAPTIVE_MODE0		0x270
+#define QSERDES_V8_COM_PLL_CCTRL_ADAPTIVE_MODE0			0x274
+#define QSERDES_V8_COM_CP_CTRL_ADAPTIVE_MODE1			0x278
+#define QSERDES_V8_COM_PLL_RCCTRL_ADAPTIVE_MODE1		0x27c
+#define QSERDES_V8_COM_PLL_CCTRL_ADAPTIVE_MODE1			0x280
 
 #endif
diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-txrx-ufs-v8.h b/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-txrx-ufs-v8.h
new file mode 100644
index 000000000000..5f923c3e64ec
--- /dev/null
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-qserdes-txrx-ufs-v8.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2026, The Linux Foundation. All rights reserved.
+ */
+
+#ifndef QCOM_PHY_QMP_QSERDES_TXRX_UFS_V8_H_
+#define QCOM_PHY_QMP_QSERDES_TXRX_UFS_V8_H_
+
+#define QSERDES_UFS_V8_TX_RES_CODE_LANE_OFFSET_TX		(0x34)
+#define QSERDES_UFS_V8_TX_RES_CODE_LANE_OFFSET_RX		(0x38)
+#define QSERDES_UFS_V8_TX_LANE_MODE_1				(0x80)
+#define QSERDES_UFS_V8_RX_UCDR_FO_GAIN_RATE2			(0x1BC)
+#define QSERDES_UFS_V8_RX_UCDR_FO_GAIN_RATE4			(0x1C4)
+#define QSERDES_UFS_V8_RX_UCDR_SO_GAIN_RATE4			(0x1DC)
+#define QSERDES_UFS_V8_RX_EQ_OFFSET_ADAPTOR_CNTRL1		(0x2C8)
+#define QSERDES_UFS_V8_RX_UCDR_PI_CONTROLS			(0x1E4)
+#define QSERDES_UFS_V8_RX_OFFSET_ADAPTOR_CNTRL3			(0x2D0)
+#define QSERDES_UFS_V8_RX_UCDR_FASTLOCK_COUNT_HIGH_RATE4	(0x120)
+#define QSERDES_UFS_V8_RX_UCDR_FASTLOCK_FO_GAIN_RATE4		(0xD4)
+#define QSERDES_UFS_V8_RX_UCDR_FASTLOCK_SO_GAIN_RATE4		(0xEC)
+#define QSERDES_UFS_V8_RX_VGA_CAL_MAN_VAL			(0x288)
+#define QSERDES_UFS_V8_RX_EQU_ADAPTOR_CNTRL4			(0x2B0)
+#define QSERDES_UFS_V8_RX_MODE_RATE_0_1_B4			(0x324)
+#define QSERDES_UFS_V8_RX_MODE_RATE4_SA_B7			(0x3B4)
+#define QSERDES_UFS_V8_RX_MODE_RATE4_SA_B9			(0x3BC)
+#define QSERDES_UFS_V8_RX_MODE_RATE4_SB_B7			(0x3E0)
+#define QSERDES_UFS_V8_RX_MODE_RATE4_SB_B9			(0x3E8)
+#define QSERDES_UFS_V8_RX_MODE_RATE5_SA_B7			(0x40C)
+#define QSERDES_UFS_V8_RX_MODE_RATE5_SA_B9			(0x414)
+#define QSERDES_UFS_V8_RX_MODE_RATE5_SB_B7			(0x438)
+#define QSERDES_UFS_V8_RX_MODE_RATE5_SB_B9			(0x440)
+#define QSERDES_UFS_V8_RX_UCDR_SO_SATURATION			(0xF4)
+#define QSERDES_UFS_V8_RX_TERM_BW_CTRL0				(0x1AC)
+#define QSERDES_UFS_V8_RX_DLL0_FTUNE_CTRL			(0x498)
+#define QSERDES_UFS_V8_RX_SIGDET_CAL_TRIM			(0x4d0)
+
+#endif
diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-ufs.c b/drivers/phy/qualcomm/phy-qcom-qmp-ufs.c
index 0f4ad24aa405..d4aca22c181e 100644
--- a/drivers/phy/qualcomm/phy-qcom-qmp-ufs.c
+++ b/drivers/phy/qualcomm/phy-qcom-qmp-ufs.c
@@ -29,9 +29,11 @@
 #include "phy-qcom-qmp-pcs-ufs-v4.h"
 #include "phy-qcom-qmp-pcs-ufs-v5.h"
 #include "phy-qcom-qmp-pcs-ufs-v6.h"
+#include "phy-qcom-qmp-pcs-ufs-v7.h"
 
 #include "phy-qcom-qmp-qserdes-txrx-ufs-v6.h"
 #include "phy-qcom-qmp-qserdes-txrx-ufs-v7.h"
+#include "phy-qcom-qmp-qserdes-txrx-ufs-v8.h"
 
 /* QPHY_PCS_READY_STATUS bit */
 #define PCS_READY				BIT(0)
@@ -84,6 +86,13 @@ static const unsigned int ufsphy_v6_regs_layout[QPHY_LAYOUT_SIZE] = {
 	[QPHY_PCS_POWER_DOWN_CONTROL]	= QPHY_V6_PCS_UFS_POWER_DOWN_CONTROL,
 };
 
+static const unsigned int ufsphy_v7_regs_layout[QPHY_LAYOUT_SIZE] = {
+	[QPHY_START_CTRL]		= QPHY_V7_PCS_UFS_PHY_START,
+	[QPHY_PCS_READY_STATUS]		= QPHY_V7_PCS_UFS_READY_STATUS,
+	[QPHY_SW_RESET]			= QPHY_V7_PCS_UFS_SW_RESET,
+	[QPHY_PCS_POWER_DOWN_CONTROL]	= QPHY_V7_PCS_UFS_POWER_DOWN_CONTROL,
+};
+
 static const struct qmp_phy_init_tbl milos_ufsphy_serdes[] = {
 	QMP_PHY_INIT_CFG(QSERDES_V6_COM_SYSCLK_EN_SEL, 0xd9),
 	QMP_PHY_INIT_CFG(QSERDES_V6_COM_CMN_CONFIG_1, 0x16),
@@ -1307,6 +1316,11 @@ static const struct regulator_bulk_data sm8750_ufsphy_vreg_l[] = {
 	{ .supply = "vdda-pll", .init_load_uA = 18300 },
 };
 
+static const struct regulator_bulk_data hawi_ufsphy_vreg_l[] = {
+	{ .supply = "vdda-phy", .init_load_uA = 324000 },
+	{ .supply = "vdda-pll", .init_load_uA = 27000 },
+};
+
 static const struct qmp_ufs_offsets qmp_ufs_offsets = {
 	.serdes		= 0,
 	.pcs		= 0xc00,
@@ -1325,6 +1339,15 @@ static const struct qmp_ufs_offsets qmp_ufs_offsets_v6 = {
 	.rx2		= 0x1a00,
 };
 
+static const struct qmp_ufs_offsets qmp_ufs_offsets_v7 = {
+	.serdes		= 0,
+	.pcs		= 0x0400,
+	.tx		= 0x2000,
+	.rx		= 0x2000,
+	.tx2		= 0x3000,
+	.rx2		= 0x3000,
+};
+
 static const struct qmp_phy_cfg milos_ufsphy_cfg = {
 	.lanes			= 2,
 
@@ -1845,6 +1868,119 @@ static const struct qmp_phy_cfg sm8750_ufsphy_cfg = {
 
 };
 
+static const struct qmp_phy_init_tbl hawi_ufsphy_serdes[] = {
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_SYSCLK_EN_SEL, 0xd9),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_CMN_CONFIG_1, 0x16),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_HSCLK_SEL_1, 0x11),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_HSCLK_HS_SWITCH_SEL_1, 0x00),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_LOCK_CMP_EN, 0x01),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_LOCK_CMP_CFG, 0x60),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_PLL_IVCO, 0x1f),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_PLL_IVCO_MODE1, 0x1f),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_CMN_IETRIM, 0x07),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_CMN_IPTRIM, 0x20),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_VCO_TUNE_MAP, 0x04),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_VCO_TUNE_CTRL, 0x40),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_ADAPTIVE_ANALOG_CONFIG, 0x06),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_DEC_START_MODE0, 0x41),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_CP_CTRL_MODE0, 0x06),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_PLL_RCTRL_MODE0, 0x18),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_PLL_CCTRL_MODE0, 0x14),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_CP_CTRL_ADAPTIVE_MODE0, 0x06),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_PLL_RCCTRL_ADAPTIVE_MODE0, 0x18),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_PLL_CCTRL_ADAPTIVE_MODE0, 0x14),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_LOCK_CMP1_MODE0, 0x7f),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_LOCK_CMP2_MODE0, 0x06),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_BIN_VCOCAL_CMP_CODE1_MODE0, 0x92),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_BIN_VCOCAL_CMP_CODE2_MODE0, 0x1e),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_DEC_START_MODE1, 0x4c),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_CP_CTRL_MODE1, 0x06),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_PLL_RCTRL_MODE1, 0x18),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_PLL_CCTRL_MODE1, 0x14),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_CP_CTRL_ADAPTIVE_MODE1, 0x06),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_PLL_RCCTRL_ADAPTIVE_MODE1, 0x18),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_PLL_CCTRL_ADAPTIVE_MODE1, 0x14),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_LOCK_CMP1_MODE1, 0x99),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_LOCK_CMP2_MODE1, 0x07),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_BIN_VCOCAL_CMP_CODE1_MODE1, 0xbe),
+	QMP_PHY_INIT_CFG(QSERDES_V8_COM_BIN_VCOCAL_CMP_CODE2_MODE1, 0x23),
+};
+
+static const struct qmp_phy_init_tbl hawi_ufsphy_tx[] = {
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_TX_LANE_MODE_1, 0x0c),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_TX_RES_CODE_LANE_OFFSET_TX, 0x07),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_TX_RES_CODE_LANE_OFFSET_RX, 0x17),
+};
+
+static const struct qmp_phy_init_tbl hawi_ufsphy_rx[] = {
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_UCDR_FO_GAIN_RATE2, 0x0c),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_UCDR_FO_GAIN_RATE4, 0x0c),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_UCDR_SO_GAIN_RATE4, 0x04),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_EQ_OFFSET_ADAPTOR_CNTRL1, 0x14),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_UCDR_PI_CONTROLS, 0x07),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_OFFSET_ADAPTOR_CNTRL3, 0x0e),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_UCDR_FASTLOCK_COUNT_HIGH_RATE4, 0x02),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_UCDR_FASTLOCK_FO_GAIN_RATE4, 0x1c),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_UCDR_FASTLOCK_SO_GAIN_RATE4, 0x06),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_VGA_CAL_MAN_VAL, 0x8e),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_EQU_ADAPTOR_CNTRL4, 0x0f),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_MODE_RATE_0_1_B4, 0xb8),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_MODE_RATE4_SA_B7, 0x66),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_MODE_RATE4_SA_B9, 0x1f),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_MODE_RATE4_SB_B7, 0x66),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_MODE_RATE4_SB_B9, 0x1f),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_MODE_RATE5_SA_B7, 0x66),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_MODE_RATE5_SA_B9, 0x1f),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_MODE_RATE5_SB_B7, 0x66),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_MODE_RATE5_SB_B9, 0x1f),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_UCDR_SO_SATURATION, 0x1f),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_TERM_BW_CTRL0, 0xfa),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_DLL0_FTUNE_CTRL, 0x30),
+	QMP_PHY_INIT_CFG(QSERDES_UFS_V8_RX_SIGDET_CAL_TRIM, 0x77),
+};
+
+static const struct qmp_phy_init_tbl hawi_ufsphy_pcs[] = {
+	QMP_PHY_INIT_CFG(QPHY_V7_PCS_UFS_TX_MID_TERM_CTRL1, 0x43),
+	QMP_PHY_INIT_CFG(QPHY_V7_PCS_UFS_PCS_CTRL1, 0x42),
+	QMP_PHY_INIT_CFG(QPHY_V7_PCS_UFS_TX_LARGE_AMP_DRV_LVL, 0x0f),
+	QMP_PHY_INIT_CFG(QPHY_V7_PCS_UFS_RX_SIGDET_CTRL2, 0x68),
+	QMP_PHY_INIT_CFG(QPHY_V7_PCS_UFS_MULTI_LANE_CTRL1, 0x02),
+};
+
+static const struct qmp_phy_init_tbl hawi_ufsphy_g5_pcs[] = {
+	QMP_PHY_INIT_CFG(QPHY_V7_PCS_UFS_PLL_CNTL, 0x3b),
+	QMP_PHY_INIT_CFG(QPHY_V7_PCS_UFS_TX_HSGEAR_CAPABILITY, 0x05),
+	QMP_PHY_INIT_CFG(QPHY_V7_PCS_UFS_RX_HSGEAR_CAPABILITY, 0x05),
+};
+
+static const struct qmp_phy_cfg hawi_ufsphy_cfg = {
+	.lanes			= 2,
+
+	.offsets		= &qmp_ufs_offsets_v7,
+	.max_supported_gear	= UFS_HS_G5,
+
+	.tbls = {
+		.serdes		= hawi_ufsphy_serdes,
+		.serdes_num	= ARRAY_SIZE(hawi_ufsphy_serdes),
+		.tx		= hawi_ufsphy_tx,
+		.tx_num		= ARRAY_SIZE(hawi_ufsphy_tx),
+		.rx		= hawi_ufsphy_rx,
+		.rx_num		= ARRAY_SIZE(hawi_ufsphy_rx),
+		.pcs		= hawi_ufsphy_pcs,
+		.pcs_num	= ARRAY_SIZE(hawi_ufsphy_pcs),
+	},
+
+	.tbls_hs_overlay[0] = {
+		.pcs		= hawi_ufsphy_g5_pcs,
+		.pcs_num	= ARRAY_SIZE(hawi_ufsphy_g5_pcs),
+		.max_gear	= UFS_HS_G5,
+	},
+
+	.vreg_list		= hawi_ufsphy_vreg_l,
+	.num_vregs		= ARRAY_SIZE(hawi_ufsphy_vreg_l),
+	.regs			= ufsphy_v7_regs_layout,
+};
+
 static void qmp_ufs_serdes_init(struct qmp_ufs *qmp, const struct qmp_phy_cfg_tbls *tbls)
 {
 	void __iomem *serdes = qmp->serdes;
@@ -2259,6 +2395,9 @@ static int qmp_ufs_probe(struct platform_device *pdev)
 
 static const struct of_device_id qmp_ufs_of_match_table[] = {
 	{
+		.compatible = "qcom,hawi-qmp-ufs-phy",
+		.data = &hawi_ufsphy_cfg,
+	}, {
 		.compatible = "qcom,milos-qmp-ufs-phy",
 		.data = &milos_ufsphy_cfg,
 	}, {
-- 
2.34.1



From palash.kambar at oss.qualcomm.com  Mon Aug  3 06:08:52 2026
From: palash.kambar at oss.qualcomm.com (palash.kambar at oss.qualcomm.com)
Date: Mon,  3 Aug 2026 18:38:52 +0530
Subject: [PATCH v5 2/2] dt-bindings: phy: qcom,sc8280xp-qmp-ufs-phy: Add Hawi UFS PHY compatible
In-Reply-To: <20260803130852.1432714-1-palash.kambar@oss.qualcomm.com>
References: <20260803130852.1432714-1-palash.kambar@oss.qualcomm.com>
Message-ID: <20260803130852.1432714-3-palash.kambar@oss.qualcomm.com>

From: Palash Kambar <palash.kambar at oss.qualcomm.com>

Document QMP UFS PHY compatible for Hawi SoC.

Reviewed-by: Manivannan Sadhasivam <mani at kernel.org>
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski at oss.qualcomm.com>
Signed-off-by: Palash Kambar <palash.kambar at oss.qualcomm.com>
---
 .../devicetree/bindings/phy/qcom,sc8280xp-qmp-ufs-phy.yaml      | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Documentation/devicetree/bindings/phy/qcom,sc8280xp-qmp-ufs-phy.yaml b/Documentation/devicetree/bindings/phy/qcom,sc8280xp-qmp-ufs-phy.yaml
index b2c5c9a375a3..4efe40c0dc97 100644
--- a/Documentation/devicetree/bindings/phy/qcom,sc8280xp-qmp-ufs-phy.yaml
+++ b/Documentation/devicetree/bindings/phy/qcom,sc8280xp-qmp-ufs-phy.yaml
@@ -38,6 +38,7 @@ properties:
               - qcom,kaanapali-qmp-ufs-phy
           - const: qcom,sm8750-qmp-ufs-phy
       - enum:
+          - qcom,hawi-qmp-ufs-phy
           - qcom,milos-qmp-ufs-phy
           - qcom,msm8996-qmp-ufs-phy
           - qcom,msm8998-qmp-ufs-phy
@@ -108,6 +109,7 @@ allOf:
         compatible:
           contains:
             enum:
+              - qcom,hawi-qmp-ufs-phy
               - qcom,milos-qmp-ufs-phy
               - qcom,msm8998-qmp-ufs-phy
               - qcom,sa8775p-qmp-ufs-phy
-- 
2.34.1



From sashiko-bot at kernel.org  Mon Aug  3 06:18:15 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 13:18:15 +0000
Subject: [PATCH] phy: renesas: rcar-gen2: Return -EINVAL for
 out-of-range channel reg
In-Reply-To: <20260803-rcar-gen2-1-v1-1-4796ea6e76d8@gmail.com>
References: <20260803-rcar-gen2-1-v1-1-4796ea6e76d8@gmail.com>
Message-ID: <20260803131817.1F73A1F000E9@smtp.kernel.org>

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

Pre-existing issues:
- [High] Calling explicit `of_node_put(np)` inside a `for_each_child_of_node_scoped()` loop causes a double-free on the error path.
- [High] Storing a device node pointer in `channel->of_node` without incrementing its reference count leads to a dangling pointer.
- [High] `rcar_gen2_phy_init()` ignores the return value of `clk_prepare_enable()`, potentially accessing unclocked hardware.
- [High] Dynamic device tree node addition between node count and iteration causes out-of-bounds array access.
--

--- Patch [1]: [PATCH] phy: renesas: rcar-gen2: Return -EINVAL for out-of-range channel reg ---
commit 1182e2b5f6437e08233213f334dad41e6af137fa
Author: Felix Gu <ustc.gu at gmail.com>

phy: renesas: rcar-gen2: Return -EINVAL for out-of-range channel reg

When of_property_read_u32() succeeds but channel_num exceeds
data->num_channels, the probe function erroneously reports success despite 
no PHY provider being registered. This commit fixes the issue by returning 
-EINVAL in that specific case.

> diff --git a/drivers/phy/renesas/phy-rcar-gen2.c b/drivers/phy/renesas/phy-rcar-gen2.c
> index 6c671254c6258..f0858d8cc7056 100644
> --- a/drivers/phy/renesas/phy-rcar-gen2.c
> +++ b/drivers/phy/renesas/phy-rcar-gen2.c
> @@ -390,7 +390,7 @@ static int rcar_gen2_phy_probe(struct platform_device *pdev)
>  		error = of_property_read_u32(np, "reg", &channel_num);
>  		if (error || channel_num >= data->num_channels) {
>  			dev_err(dev, "Invalid \"reg\" property\n");
> -			return error;
> +			return error ?: -EINVAL;
>  		}
>  		channel->select_mask = select_mask[channel_num];
>  

[Severity: High]
This is a pre-existing issue, but could the explicit of_node_put(np) inside
for_each_child_of_node_scoped() cause a double-free?

The scoped iterator already automatically cleans up the node reference on
early returns. Is it possible that this explicit put corrupts the reference
count:

drivers/phy/renesas/phy-rcar-gen2.c:rcar_gen2_phy_probe() {
    ...
		for (n = 0; n < PHYS_PER_CHANNEL; n++) {
    ...
			phy->phy = devm_phy_create(dev, NULL,
						   data->gen2_phy_ops);
			if (IS_ERR(phy->phy)) {
				dev_err(dev, "Failed to create PHY\n");
				of_node_put(np);
				return PTR_ERR(phy->phy);
			}
    ...
}


[Severity: High]
This is a pre-existing issue, but I am wondering if this leaves a dangling
pointer in channel->of_node?

The scoped iterator automatically drops the reference to np when it
advances or terminates, but the pointer is saved without acquiring a
reference via of_node_get():

drivers/phy/renesas/phy-rcar-gen2.c:rcar_gen2_phy_probe() {
    ...
	for_each_child_of_node_scoped(dev->of_node, np) {
		struct rcar_gen2_channel *channel = drv->channels + i;
		...
		channel->of_node = np;
    ...
}

Can this unreferenced pointer later be used for comparison and cause
incorrect PHY instances to be returned if the device tree node is
dynamically freed?

drivers/phy/renesas/phy-rcar-gen2.c:rcar_gen2_phy_xlate() {
    ...
	for (i = 0; i < drv->num_channels; i++) {
		if (np == drv->channels[i].of_node)
			break;
	}
    ...
}


[Severity: High]
This isn't a bug introduced by this patch, but it looks like
rcar_gen2_phy_init() ignores the return value of clk_prepare_enable().

If the clock fails to enable, the driver proceeds to acquire a spinlock
and write to the USBHS_UGCTRL2 hardware register:

drivers/phy/renesas/phy-rcar-gen2.c:rcar_gen2_phy_init() {
    ...
	clk_prepare_enable(drv->clk);

	spin_lock_irqsave(&drv->lock, flags);
	ugctrl2 = readl(drv->base + USBHS_UGCTRL2);
    ...
}

Would this access unclocked hardware and result in a fatal bus error?


[Severity: High]
This isn't a bug introduced by this patch, but might there be an
out-of-bounds array access here if child nodes are dynamically added?

The probe function allocates the drv->channels array exactly to the
current child count, but iterates over potentially modified children using
an unbounded index i to write to drv->channels + i:

drivers/phy/renesas/phy-rcar-gen2.c:rcar_gen2_phy_probe() {
    ...
	drv->num_channels = of_get_child_count(dev->of_node);
	drv->channels = devm_kcalloc(dev, drv->num_channels,
				     sizeof(struct rcar_gen2_channel),
				     GFP_KERNEL);
    ...
	for_each_child_of_node_scoped(dev->of_node, np) {
		struct rcar_gen2_channel *channel = drv->channels + i;
        ...
		i++;
	}
    ...
}

Could a device tree overlay added between the count and the loop cause a
heap buffer overflow?

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260803-rcar-gen2-1-v1-1-4796ea6e76d8 at gmail.com?part=1


From biju.das.jz at bp.renesas.com  Mon Aug  3 06:21:29 2026
From: biju.das.jz at bp.renesas.com (Biju Das)
Date: Mon, 3 Aug 2026 13:21:29 +0000
Subject: [PATCH] phy: renesas: rcar-gen2: Fix double of_node_put on phy
 creation failure
In-Reply-To: <20260803-rcar-gen2-v1-1-9aa35c36d7d7@gmail.com>
References: <20260803-rcar-gen2-v1-1-9aa35c36d7d7@gmail.com>
Message-ID: <TY3PR01MB11346A09428478C8C70BF50FE86D52@TY3PR01MB11346.jpnprd01.prod.outlook.com>

Hi Felix Gu,

Thanks for the patch.

> -----Original Message-----
> From: Felix Gu <ustc.gu at gmail.com>
> Sent: 03 August 2026 13:43
> Subject: [PATCH] phy: renesas: rcar-gen2: Fix double of_node_put on phy creation failure
> 
> for_each_child_of_node_scoped() releases the node reference on scope exit, so the explicit
> of_node_put(np) in the devm_phy_create() error path drops it twice.
> 
> Drop the redundant of_node_put() and let the scoped cleanup handle it.
> 
> Fixes: b64b32791fb5 ("phy: renesas: rcar-gen2: Simplify with scoped for each OF child loop")
> Signed-off-by: Felix Gu <ustc.gu at gmail.com>
> ---
>  drivers/phy/renesas/phy-rcar-gen2.c | 1 -
>  1 file changed, 1 deletion(-)
> 
> diff --git a/drivers/phy/renesas/phy-rcar-gen2.c b/drivers/phy/renesas/phy-rcar-gen2.c
> index 6c671254c625..5a272e25e051 100644
> --- a/drivers/phy/renesas/phy-rcar-gen2.c
> +++ b/drivers/phy/renesas/phy-rcar-gen2.c
> @@ -405,7 +405,6 @@ static int rcar_gen2_phy_probe(struct platform_device *pdev)
>  						   data->gen2_phy_ops);
>  			if (IS_ERR(phy->phy)) {
>  				dev_err(dev, "Failed to create PHY\n");
> -				of_node_put(np);
>  				return PTR_ERR(phy->phy);

Now, can we use dev_err_probe() as it is probe function??

Cheers,
Biju

>  			}
>  			phy_set_drvdata(phy->phy, phy);
> 
> ---
> base-commit: 415606a7be939835db9b0d6b711887586646346d
> change-id: 20260803-rcar-gen2-140f014cd384
> 
> Best regards,
> --
> Felix Gu <ustc.gu at gmail.com>
> 


From biju.das.jz at bp.renesas.com  Mon Aug  3 06:26:07 2026
From: biju.das.jz at bp.renesas.com (Biju Das)
Date: Mon, 3 Aug 2026 13:26:07 +0000
Subject: [PATCH] phy: renesas: rcar-gen2: Return -EINVAL for out-of-range
 channel reg
In-Reply-To: <20260803-rcar-gen2-1-v1-1-4796ea6e76d8@gmail.com>
References: <20260803-rcar-gen2-1-v1-1-4796ea6e76d8@gmail.com>
Message-ID: <TY3PR01MB11346B7C0EECC1BFBDCAE069386D52@TY3PR01MB11346.jpnprd01.prod.outlook.com>

Hi Felix Gu,

Thanks for the patch.

> -----Original Message-----
> From: Felix Gu <ustc.gu at gmail.com>
> Sent: 03 August 2026 14:07
> Subject: [PATCH] phy: renesas: rcar-gen2: Return -EINVAL for out-of-range channel reg
> 
> When of_property_read_u32() succeeds but channel_num exceeds
> data->num_channels, rcar_gen2_phy_probe() returns error which is 0,
> so probe reports success even though no PHY provider is registered.
> 
> Return -EINVAL in that case.
> 
> Fixes: 1233f59f745b ("phy: Renesas R-Car Gen2 PHY driver")
> Signed-off-by: Felix Gu <ustc.gu at gmail.com>
> ---
>  drivers/phy/renesas/phy-rcar-gen2.c | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
> 
> diff --git a/drivers/phy/renesas/phy-rcar-gen2.c b/drivers/phy/renesas/phy-rcar-gen2.c
> index 6c671254c625..f0858d8cc705 100644
> --- a/drivers/phy/renesas/phy-rcar-gen2.c
> +++ b/drivers/phy/renesas/phy-rcar-gen2.c
> @@ -390,7 +390,7 @@ static int rcar_gen2_phy_probe(struct platform_device *pdev)
>  		error = of_property_read_u32(np, "reg", &channel_num);
>  		if (error || channel_num >= data->num_channels) {
>  			dev_err(dev, "Invalid \"reg\" property\n");
> -			return error;
> +			return error ?: -EINVAL;

Can we use dev_error_probe() here as well??

Cheers,
Biju

>  		}
>  		channel->select_mask = select_mask[channel_num];
> 
> 
> ---
> base-commit: 415606a7be939835db9b0d6b711887586646346d
> change-id: 20260803-rcar-gen2-1-98f440b28b35
> 
> Best regards,
> --
> Felix Gu <ustc.gu at gmail.com>


From sashiko-bot at kernel.org  Mon Aug  3 06:41:03 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 13:41:03 +0000
Subject: [PATCH v5 1/2] phy: qcom-qmp-ufs: Add UFS PHY support on Hawi
In-Reply-To: <20260803130852.1432714-2-palash.kambar@oss.qualcomm.com>
References: <20260803130852.1432714-1-palash.kambar@oss.qualcomm.com>
 <20260803130852.1432714-2-palash.kambar@oss.qualcomm.com>
Message-ID: <20260803134104.829451F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Missing UFS_HS_G4 PHY configuration fallback overlay for the Hawi SoC, leading to uninitialized critical registers.
--

--- Patch [1]: [PATCH v5 1/2] phy: qcom-qmp-ufs: Add UFS PHY support on Hawi ---
commit 4d0cc515a612b40d0f5a7fc9e44b9fbc6b1527fb
Author: Palash Kambar <palash.kambar at oss.qualcomm.com>
Subject: phy: qcom-qmp-ufs: Add UFS PHY support on Hawi

Add the init sequence tables and config for the UFS QMP phy found in
the Hawi SoC.

> diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-ufs.c b/drivers/phy/qualcomm/phy-qcom-qmp-ufs.c
> index 0f4ad24aa4058..d4aca22c181e4 100644
> --- a/drivers/phy/qualcomm/phy-qcom-qmp-ufs.c
> +++ b/drivers/phy/qualcomm/phy-qcom-qmp-ufs.c

[ ... ]

> @@ -1845,6 +1868,119 @@ static const struct qmp_phy_cfg sm8750_ufsphy_cfg = {
>  
>  };
>  
> +static const struct qmp_phy_init_tbl hawi_ufsphy_serdes[] = {

[ ... ]

> +static const struct qmp_phy_cfg hawi_ufsphy_cfg = {
> +	.lanes			= 2,
> +
> +	.offsets		= &qmp_ufs_offsets_v7,
> +	.max_supported_gear	= UFS_HS_G5,
> +
> +	.tbls = {
> +		.serdes		= hawi_ufsphy_serdes,
> +		.serdes_num	= ARRAY_SIZE(hawi_ufsphy_serdes),
> +		.tx		= hawi_ufsphy_tx,
> +		.tx_num		= ARRAY_SIZE(hawi_ufsphy_tx),
> +		.rx		= hawi_ufsphy_rx,
> +		.rx_num		= ARRAY_SIZE(hawi_ufsphy_rx),
> +		.pcs		= hawi_ufsphy_pcs,
> +		.pcs_num	= ARRAY_SIZE(hawi_ufsphy_pcs),
> +	},
> +
> +	.tbls_hs_overlay[0] = {
> +		.pcs		= hawi_ufsphy_g5_pcs,
> +		.pcs_num	= ARRAY_SIZE(hawi_ufsphy_g5_pcs),
> +		.max_gear	= UFS_HS_G5,
> +	},

[Severity: High]
Does this missing G4 overlay cause initialization to fail when a device
requests HS-G4?

If a UFS 3.1 device is attached (max gear G4) or link training drops to G4,
the host driver will call phy_set_mode() with UFS_HS_G4. Since the
tbls_hs_overlay array only defines an entry for UFS_HS_G5,
qmp_ufs_get_gear_overlay() will return -EINVAL.

This leaves critical PCS registers like QPHY_V7_PCS_UFS_PLL_CNTL,
TX_HSGEAR_CAPABILITY, and RX_HSGEAR_CAPABILITY at their hardware defaults,
which could lead to link failures. Should there be a fallback G4 overlay
defined here?

> +
> +	.vreg_list		= hawi_ufsphy_vreg_l,
> +	.num_vregs		= ARRAY_SIZE(hawi_ufsphy_vreg_l),
> +	.regs			= ufsphy_v7_regs_layout,
> +};

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260803130852.1432714-1-palash.kambar at oss.qualcomm.com?part=1


From ustc.gu at gmail.com  Mon Aug  3 08:15:17 2026
From: ustc.gu at gmail.com (Felix Gu)
Date: Mon, 03 Aug 2026 23:15:17 +0800
Subject: [PATCH] phy: sunplus: fix error handling in sp_uphy_init()
Message-ID: <20260803-sunplus-usb3-v1-1-5a562524c869@gmail.com>

Fix the error paths of sp_uphy_init() to undo exactly what each stage
did: return directly if clk_prepare_enable() fails, release only the clock
if reset_control_deassert() fails, and jump to err_reset if
update_disc_vol() fails so the clock and reset are not leaked.

Fixes: 99d9ccd97385 ("phy: usb: Add USB2.0 phy driver for Sunplus SP7021")
Signed-off-by: Felix Gu <ustc.gu at gmail.com>
---
 drivers/phy/sunplus/phy-sunplus-usb2.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/drivers/phy/sunplus/phy-sunplus-usb2.c b/drivers/phy/sunplus/phy-sunplus-usb2.c
index 637a5fbae6d9..0ad4c7160d17 100644
--- a/drivers/phy/sunplus/phy-sunplus-usb2.c
+++ b/drivers/phy/sunplus/phy-sunplus-usb2.c
@@ -116,11 +116,11 @@ static int sp_uphy_init(struct phy *phy)
 
 	ret = clk_prepare_enable(usbphy->phy_clk);
 	if (ret)
-		goto err_clk;
+		return ret;
 
 	ret = reset_control_deassert(usbphy->rstc);
 	if (ret)
-		goto err_reset;
+		goto err_clk;
 
 	/* Default value modification */
 	writel(HIGH_MASK_BITS | 0x4002, usbphy->moon4_regs + UPHY_CONTROL0);
@@ -129,7 +129,7 @@ static int sp_uphy_init(struct phy *phy)
 	/* disconnect voltage */
 	ret = update_disc_vol(usbphy);
 	if (ret < 0)
-		return ret;
+		goto err_reset;
 
 	/* board uphy 0 internal register modification for tid certification */
 	val = readl(usbphy->phy_regs + CONFIG9);

---
base-commit: 415606a7be939835db9b0d6b711887586646346d
change-id: 20260803-sunplus-usb3-ba6c829bc063

Best regards,
--  
Felix Gu <ustc.gu at gmail.com>



From p.zabel at pengutronix.de  Mon Aug  3 08:22:40 2026
From: p.zabel at pengutronix.de (Philipp Zabel)
Date: Mon, 03 Aug 2026 17:22:40 +0200
Subject: [PATCH] phy: sunplus: fix error handling in sp_uphy_init()
In-Reply-To: <20260803-sunplus-usb3-v1-1-5a562524c869@gmail.com>
References: <20260803-sunplus-usb3-v1-1-5a562524c869@gmail.com>
Message-ID: <62323293f3b1f58871e6e078e2bbf5efbab62c09.camel@pengutronix.de>

On Mo, 2026-08-03 at 23:15 +0800, Felix Gu wrote:
> Fix the error paths of sp_uphy_init() to undo exactly what each stage
> did: return directly if clk_prepare_enable() fails, release only the clock
> if reset_control_deassert() fails, and jump to err_reset if
> update_disc_vol() fails so the clock and reset are not leaked.
> 
> Fixes: 99d9ccd97385 ("phy: usb: Add USB2.0 phy driver for Sunplus SP7021")
> Signed-off-by: Felix Gu <ustc.gu at gmail.com>

Reviewed-by: Philipp Zabel <p.zabel at pengutronix.de>

regards
Philipp


From sashiko-bot at kernel.org  Mon Aug  3 08:23:09 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 15:23:09 +0000
Subject: [PATCH] phy: sunplus: fix error handling in sp_uphy_init()
In-Reply-To: <20260803-sunplus-usb3-v1-1-5a562524c869@gmail.com>
References: <20260803-sunplus-usb3-v1-1-5a562524c869@gmail.com>
Message-ID: <20260803152310.42DE41F000E9@smtp.kernel.org>

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

Pre-existing issues:
- [High] Kernel panic due to dereferencing an invalid error pointer from nvmem_cell_get.
- [Medium] Memory leak of the buffer returned by nvmem_cell_read.
- [Medium] Unvalidated array access may lead to an out-of-bounds read.
--

--- Patch [1]: [PATCH] phy: sunplus: fix error handling in sp_uphy_init() ---
commit bbdc3fb921cb2657598b63724660355fd956b455
Author: Felix Gu <ustc.gu at gmail.com>

phy: sunplus: fix error handling in sp_uphy_init()

Fixes the error paths in sp_uphy_init() to correctly undo initialization
steps. It returns directly if clock enable fails, releases the clock if
reset deassert fails, and asserts reset and clock if disconnect voltage
update fails.

> diff --git a/drivers/phy/sunplus/phy-sunplus-usb2.c b/drivers/phy/sunplus/phy-sunplus-usb2.c
> index 637a5fbae6d9a..0ad4c7160d17c 100644
> --- a/drivers/phy/sunplus/phy-sunplus-usb2.c
> +++ b/drivers/phy/sunplus/phy-sunplus-usb2.c
> @@ -129,7 +129,7 @@ static int sp_uphy_init(struct phy *phy)
>  	/* disconnect voltage */
>  	ret = update_disc_vol(usbphy);

[Severity: High]
This isn't a bug introduced by this patch, but looking at update_disc_vol()
called here in drivers/phy/sunplus/phy-sunplus-usb2.c, is there a potential
dereference of an invalid pointer?

If nvmem_cell_get() fails and returns an error pointer other than
-EPROBE_DEFER (such as -ENOENT), or if it returns NULL, the code doesn't
return early. It falls through and passes this invalid pointer directly to
nvmem_cell_read():

	cell = nvmem_cell_get(usbphy->dev, disc_name);
	if (IS_ERR_OR_NULL(cell)) {
		if (PTR_ERR(cell) == -EPROBE_DEFER)
			return -EPROBE_DEFER;
	}

	otp_v = nvmem_cell_read(cell, &otp_l);
	nvmem_cell_put(cell);

Will this cause a panic when nvmem_cell_read() attempts to dereference the
cell pointer?

[Severity: Medium]
This is a pre-existing issue, but does update_disc_vol() leak the buffer
returned by nvmem_cell_read()?

	otp_v = nvmem_cell_read(cell, &otp_l);
	nvmem_cell_put(cell);

	if (!IS_ERR(otp_v)) {
		set = *(otp_v + 1);
		...

The nvmem_cell_read() function returns a dynamically allocated buffer that
the caller is responsible for freeing. It appears otp_v is never passed to
kfree() before the function returns, which would result in a memory leak.

[Severity: Medium]
This is another pre-existing issue, but could the array access in
update_disc_vol() lead to an out-of-bounds read?

	otp_v = nvmem_cell_read(cell, &otp_l);
	nvmem_cell_put(cell);

	if (!IS_ERR(otp_v)) {
		set = *(otp_v + 1);

The size of the read buffer is provided in otp_l. If a device tree
incorrectly provides a 1-byte nvmem cell, unconditionally accessing the
second byte of the buffer at *(otp_v + 1) without checking if otp_l is at
least 2 might lead to reading out-of-bounds memory.

>  	if (ret < 0)
> -		return ret;
> +		goto err_reset;

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260803-sunplus-usb3-v1-1-5a562524c869 at gmail.com?part=1


From bmasney at redhat.com  Mon Aug  3 09:03:36 2026
From: bmasney at redhat.com (Brian Masney)
Date: Mon, 3 Aug 2026 12:03:36 -0400
Subject: [PATCH v9 05/12] clk: zte: Add Clock registration infrastructure
In-Reply-To: <20260802-zx29clk-v9-5-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-5-d05530d85d28@gmail.com>
Message-ID: <anC72Cf1zKcLYWlC@redhat.com>

Hi Stefan,

On Sun, Aug 02, 2026 at 11:33:37PM +0300, Stefan D?singer wrote:
> The next patches will implement the regmap clocks and PLL driver. The
> actual hardware specific clock listing will live in a separate module.
> 
> Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>
> 
> ---
> 
> Version 9:
> Rework parent matching and clock export control. Most of the changes
> will be in the next patches. The visible change here is that the clock
> array is unified with single clocks having a type rather than one array
> per type.
> 
> Depend on HAS_IOMEM, similarly to Sashiko's flagged issue in the Reset
> patch
> 
> ZX297520V3_CLK_NO_EXPORT is gone again.
> 
> The explicit clock input array and enabling of LSP's pclk is removed
> from the clock driver. The MFD parent takes care of this instead. The
> clock driver does some sanity checking on the static clock data though.
> 
> Version 8:
> Use ZX297520V3_CLK_NO_EXPORT=(~0u) for unexported clocks. While using 0,
> and starting clock indices at 1, is a common pattern in existing
> drivers, it exposes a driver implementation detail in the hardware
> binding interface.
> 
> If desired, I can change the special index to a separate field in the
> structs.
> 
> Fix the return value if the ->init() callback fails. (Sashiko)
> 
> Version 7:
> *) Add fixed dividers to handle PLL subdivisions
> *) Never register PLLs directly as exported clocks - everything on this
> SoC goes through a gate before it leaves a controller.
> 
> Version 6:
> *) Remove auxdev now that LSP clocks also use MFD
> *) Error codepath fixes pointed out by Sashiko.
> 
> Version 5:
> 
> *) Pass the static clk data instead of calling get_match_data to prepare
> for operating as an MFD child.
> 
> *) Don't use devm_kzalloc to allocate the auxiliary_device
> structure. I guess Sashiko is right, and that's what "Because once the
> device is placed on the bus the parent driver can not tell what other
> code may have a reference to this data" is trying to tell me.
> 
> *) Fix error check for device_node_to_regmap.
> ---
>  MAINTAINERS                  |   1 +
>  drivers/clk/Kconfig          |   1 +
>  drivers/clk/Makefile         |   1 +
>  drivers/clk/zte/Kconfig      |  17 ++++
>  drivers/clk/zte/Makefile     |   5 ++
>  drivers/clk/zte/clk-regmap.c |  32 ++++++++
>  drivers/clk/zte/clk-zx.c     | 189 +++++++++++++++++++++++++++++++++++++++++++
>  drivers/clk/zte/clk-zx.h     | 142 ++++++++++++++++++++++++++++++++
>  drivers/clk/zte/pll-zx.c     |  18 +++++
>  9 files changed, 406 insertions(+)
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index b9f96455bca7..77fa86c671b8 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -3915,6 +3915,7 @@ F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-matrixcrm.yaml
>  F:	Documentation/devicetree/bindings/clock/zte,zx297520v3-topcrm.yaml
>  F:	arch/arm/boot/dts/zte/
>  F:	arch/arm/mach-zte/
> +F:	drivers/clk/zte/
>  F:	drivers/mfd/zte-zx297520v3-crm.c
>  F:	include/dt-bindings/clock/zte,zx297520v3-clk.h
>  F:	include/dt-bindings/phy/zte,zx297520v3-topcrm.h
> diff --git a/drivers/clk/Kconfig b/drivers/clk/Kconfig
> index 1717ce75a907..6f0a863951ca 100644
> --- a/drivers/clk/Kconfig
> +++ b/drivers/clk/Kconfig
> @@ -545,6 +545,7 @@ source "drivers/clk/uniphier/Kconfig"
>  source "drivers/clk/visconti/Kconfig"
>  source "drivers/clk/x86/Kconfig"
>  source "drivers/clk/xilinx/Kconfig"
> +source "drivers/clk/zte/Kconfig"
>  source "drivers/clk/zynqmp/Kconfig"
>  
>  # Kunit test cases
> diff --git a/drivers/clk/Makefile b/drivers/clk/Makefile
> index 6899e32e14aa..651fe866ddad 100644
> --- a/drivers/clk/Makefile
> +++ b/drivers/clk/Makefile
> @@ -167,5 +167,6 @@ ifeq ($(CONFIG_COMMON_CLK), y)
>  obj-$(CONFIG_X86)			+= x86/
>  endif
>  obj-y					+= xilinx/
> +obj-$(CONFIG_COMMON_CLK_ZTE)		+= zte/
>  obj-$(CONFIG_ARCH_ZYNQ)			+= zynq/
>  obj-$(CONFIG_COMMON_CLK_ZYNQMP)         += zynqmp/
> diff --git a/drivers/clk/zte/Kconfig b/drivers/clk/zte/Kconfig
> new file mode 100644
> index 000000000000..152c2423b8f0
> --- /dev/null
> +++ b/drivers/clk/zte/Kconfig
> @@ -0,0 +1,17 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +#
> +# ZTE Clock Drivers
> +#
> +
> +config COMMON_CLK_ZTE
> +	tristate "Clock driver for ZTE SoCs"
> +	depends on HAS_IOMEM
> +	depends on ARCH_ZTE || COMPILE_TEST
> +	default ARCH_ZTE
> +	select MFD_SYSCON
> +	help
> +	  This option selects common clock infrastructure for ZTE based SoCs.
> +	  You will need to enable one or more SoC specific drivers to make use
> +	  of this.
> +
> +	  Enable this if you are building a kernel for a ZTE designed board.
> diff --git a/drivers/clk/zte/Makefile b/drivers/clk/zte/Makefile
> new file mode 100644
> index 000000000000..27db07293165
> --- /dev/null
> +++ b/drivers/clk/zte/Makefile
> @@ -0,0 +1,5 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +
> +obj-$(CONFIG_COMMON_CLK_ZTE) += clk-zte.o
> +
> +clk-zte-y += clk-zx.o pll-zx.o clk-regmap.o
> diff --git a/drivers/clk/zte/clk-regmap.c b/drivers/clk/zte/clk-regmap.c
> new file mode 100644
> index 000000000000..80332dba97f6
> --- /dev/null
> +++ b/drivers/clk/zte/clk-regmap.c
> @@ -0,0 +1,32 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright (c) 2026 Stefan D?singer
> + */
> +
> +#include <linux/clk-provider.h>
> +#include <linux/device.h>
> +#include <linux/errno.h>
> +#include <linux/regmap.h>
> +
> +#include "clk-zx.h"
> +
> +struct clk_hw *zx_clk_register_gate(struct device *dev, struct regmap *regmap,
> +				    const struct zx_gate_desc *desc,
> +				    struct clk_hw * const *clocks)
> +{
> +	return ERR_PTR(-ENODEV);
> +}
> +
> +struct clk_hw *zx_clk_register_divider(struct device *dev, struct regmap *regmap,
> +				       const struct zx_div_desc *desc,
> +				       struct clk_hw * const *clocks)
> +{
> +	return ERR_PTR(-ENODEV);
> +}
> +
> +struct clk_hw *zx_clk_register_mux(struct device *dev, struct regmap *regmap,
> +				   const struct zx_mux_desc *desc,
> +				   struct clk_hw * const *clocks)
> +{
> +	return ERR_PTR(-ENODEV);
> +}
> diff --git a/drivers/clk/zte/clk-zx.c b/drivers/clk/zte/clk-zx.c
> new file mode 100644
> index 000000000000..27bbafd8ce4d
> --- /dev/null
> +++ b/drivers/clk/zte/clk-zx.c
> @@ -0,0 +1,189 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (C) 2026 Stefan D?singer
> + */
> +
> +#include <linux/clk-provider.h>
> +#include <linux/clk.h>
> +#include <linux/err.h>
> +#include <linux/errno.h>
> +#include <linux/mfd/syscon.h>
> +#include <linux/module.h>
> +
> +#include "clk-zx.h"
> +
> +static struct clk_hw *zx_clk_register_fixed_div(struct device *dev,
> +						const struct zx_fixed_divider_desc *desc,
> +						struct clk_hw * const *clocks)
> +{
> +	const struct zx_parent_desc *p = &desc->parent;
> +
> +	switch (p->type) {
> +	case ZX_PARENT_FW:
> +		return devm_clk_hw_register_fixed_factor_fwname(dev, NULL, desc->name, p->fw_name,
> +								CLK_SET_RATE_PARENT, 1, desc->div);
> +
> +	case ZX_PARENT_ID:
> +		return devm_clk_hw_register_fixed_factor_parent_hw(dev, desc->name, clocks[p->id],
> +								   CLK_SET_RATE_PARENT, 1,
> +								   desc->div);
> +	}
> +	WARN_ON_ONCE(1);
> +	return ERR_PTR(-EINVAL);
> +}
> +
> +static int zx_clk_validate(struct device *dev, struct device_node *of_node,
> +			   const struct zx_clk_data *data)
> +{
> +	const struct zx_parent_desc *parents;
> +	unsigned int i, p, num_parents;
> +	struct clk *clk;
> +
> +	/*
> +	 * Sanity check: Make sure all parents are there and write a clear message rather than
> +	 * leave potential orphans.
> +	 */

It's not common for clk drivers to have a validate function like this.
That said, I know you are reverse engineering what's in the downstream
BSP without documentation, and there is value in having this. I think
it'd be useful to clarify that in the comments because code usually gets
copied and pasted between drivers over time.

That said, once it's verified that the clock tree is correct, can these
checks be eventually dropped so that this validation logic isn't ran on
every boot?

k
> +	for (i = 0; i < data->num_clocks; ++i) {
> +		switch (data->clocks[i].type) {
> +		case ZX_CLOCK_PLL:
> +			parents = data->clocks[i].pll.parents;
> +			num_parents = data->clocks[i].pll.num_parents;
> +			break;
> +
> +		case ZX_CLOCK_FIXED_DIV:
> +			parents = &data->clocks[i].fixed_div.parent;
> +			num_parents = 1;
> +			break;
> +
> +		case ZX_CLOCK_MUX:
> +			parents = data->clocks[i].mux.parents;
> +			num_parents = data->clocks[i].mux.num_parents;
> +			break;
> +
> +		case ZX_CLOCK_DIV:
> +			parents = &data->clocks[i].div.parent;
> +			num_parents = 1;
> +			break;
> +
> +		case ZX_CLOCK_GATE:
> +			parents = &data->clocks[i].gate.parent;
> +			num_parents = 1;
> +			break;
> +
> +		default:
> +			return dev_err_probe(dev, -EINVAL, "Invalid clock entry %u\n", i);
> +		}
> +
> +		for (p = 0; p < num_parents; ++p) {
> +			switch (parents[p].type) {
> +			case ZX_PARENT_FW:
> +				clk = of_clk_get_by_name(of_node, parents[p].fw_name);
> +				if (IS_ERR(clk))
> +					return dev_err_probe(dev, PTR_ERR(clk),
> +							     "Input clk %s failure\n",
> +							     parents[p].fw_name);
> +				clk_put(clk);
> +				break;
> +
> +			case ZX_PARENT_ID:
> +				if (parents[p].id >= i)
> +					return dev_err_probe(dev, -EINVAL,
> +							     "Clock %u has parent %u\n",
> +							     i, parents[p].id);
> +				break;
> +
> +			default:
> +				return dev_err_probe(dev, -EINVAL,
> +						     "Clock %u has unexpected parent of type %u\n",
> +						     i, parents[p].type);
> +			}
> +		}
> +	}
> +
> +	return 0;
> +}
> +
> +int zx_clk_common_probe(struct device *dev, struct device_node *of_node,
> +			const struct zx_clk_data *data)
> +{
> +	struct clk_hw_onecell_data *exports;
> +	struct clk_hw **clocks;
> +	struct regmap *map;
> +	unsigned int i;
> +	int res;
> +
> +	res = zx_clk_validate(dev, of_node, data);
> +	if (res)
> +		return res;
> +
> +	map = device_node_to_regmap(of_node);
> +	if (IS_ERR(map))
> +		return PTR_ERR(map);
> +
> +	clocks = devm_kcalloc(dev, data->num_clocks, sizeof(*clocks), GFP_KERNEL);
> +	if (!clocks)
> +		return -ENOMEM;
> +
> +	if (data->init) {
> +		res = data->init(map);
> +		if (res)
> +			return res;
> +	}
> +
> +	for (i = 0; i < data->num_clocks; ++i) {
> +		struct clk_hw *hw;
> +
> +		switch (data->clocks[i].type) {
> +		case ZX_CLOCK_PLL:
> +			hw = zx_clk_register_pll(dev, map, &data->clocks[i].pll, clocks);
> +			break;
> +
> +		case ZX_CLOCK_FIXED_DIV:
> +			hw = zx_clk_register_fixed_div(dev, &data->clocks[i].fixed_div, clocks);
> +			break;
> +
> +		case ZX_CLOCK_MUX:
> +			hw = zx_clk_register_mux(dev, map, &data->clocks[i].mux, clocks);
> +			break;
> +
> +		case ZX_CLOCK_DIV:
> +			hw = zx_clk_register_divider(dev, map, &data->clocks[i].div, clocks);
> +			break;
> +
> +		case ZX_CLOCK_GATE:
> +			hw = zx_clk_register_gate(dev, map, &data->clocks[i].gate, clocks);
> +			break;
> +
> +		default:
> +			return -EINVAL;
> +		}
> +
> +		if (IS_ERR(hw))
> +			return dev_err_probe(dev, PTR_ERR(hw), "Failed to register clk %u\n", i);
> +
> +		clocks[i] = hw;
> +	}
> +
> +	exports = devm_kzalloc(dev, struct_size(exports, hws, data->num_exports), GFP_KERNEL);
> +	if (!exports)
> +		return -ENOMEM;
> +	exports->num = data->num_exports;
> +
> +	for (i = 0; i < data->num_exports; ++i) {
> +		if (data->exports[i] >= data->num_clocks)
> +			return dev_err_probe(dev, -EINVAL,
> +					     "Export %u points to out of range clock %u\n",
> +					     i, data->exports[i]);
> +
> +		exports->hws[i] = clocks[data->exports[i]];
> +	}
> +
> +	devm_kfree(dev, clocks);
> +
> +	return devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, exports);
> +}
> +EXPORT_SYMBOL_NS_GPL(zx_clk_common_probe, "ZTE_CLK");
> +
> +MODULE_AUTHOR("Stefan D?singer <stefandoesinger at gmail.com>");
> +MODULE_DESCRIPTION("ZTE common clock driver");
> +MODULE_LICENSE("GPL");
> diff --git a/drivers/clk/zte/clk-zx.h b/drivers/clk/zte/clk-zx.h
> new file mode 100644
> index 000000000000..175fbba3e0b3
> --- /dev/null
> +++ b/drivers/clk/zte/clk-zx.h
> @@ -0,0 +1,142 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +/*
> + * Copyright (C) 2026 Stefan D?singer
> + */
> +
> +#ifndef __DRV_CLK_ZX_H
> +#define __DRV_CLK_ZX_H
> +
> +#include <linux/clk-provider.h>
> +#include <linux/platform_device.h>
> +#include <linux/regmap.h>
> +#include <linux/types.h>
> +
> +#define CLK_ZX_PLL_PREPARE_IS_ENABLE	1
> +#define CLK_ZX_MAX_PARENTS		8
> +
> +enum zx_parent_type {
> +	ZX_PARENT_FW,
> +	ZX_PARENT_ID,
> +};
> +
> +struct zx_parent_desc {
> +	enum zx_parent_type		type;
> +	union {
> +		const char		*fw_name;
> +		unsigned int		id;
> +	};
> +};
> +
> +#define PARENT_FW(_name) { .type = ZX_PARENT_FW, .fw_name = (_name) }
> +
> +#define PARENT_ID(_id) { .type = ZX_PARENT_ID, .id = (_id) }
> +
> +static inline struct clk_parent_data zx_get_parent(const struct zx_parent_desc *p,
> +						   struct clk_hw * const *clocks)
> +{
> +	struct clk_parent_data ret = {};
> +
> +	switch (p->type) {
> +	case ZX_PARENT_FW:
> +		ret.fw_name = p->fw_name;
> +		ret.index = -1;
> +		return ret;
> +
> +	case ZX_PARENT_ID:
> +		ret.hw = clocks[p->id];
> +		ret.index = -1;
> +		return ret;
> +	};
> +
> +	WARN_ON_ONCE(1);
> +	return ret;
> +}
> +
> +struct zx_pll_desc {
> +	const char			*name;
> +	const struct zx_parent_desc	*parents;
> +	unsigned int			num_parents;
> +	unsigned long			rate;
> +	u16				reg;
> +	u16				flags;
> +};
> +
> +struct zx_fixed_divider_desc {
> +	const char			*name;
> +	struct zx_parent_desc		parent;
> +	unsigned int			div;
> +};
> +
> +struct zx_mux_desc {
> +	const char			*name;
> +	const struct zx_parent_desc	*parents;
> +	unsigned int			num_parents;
> +	u16				reg;
> +	u8				shift, size;
> +};
> +
> +struct zx_div_desc {
> +	const char			*name;
> +	struct zx_parent_desc		parent;
> +	u16				reg;
> +	u8				shift, size;
> +};
> +
> +struct zx_gate_desc {
> +	const char			*name;
> +	struct zx_parent_desc		parent;
> +	unsigned long			flags;
> +	u16				reg;
> +	u8				shift;
> +};
> +
> +enum zx_clock_type {
> +	ZX_CLOCK_INVALID = 0,
> +	ZX_CLOCK_PLL,
> +	ZX_CLOCK_FIXED_DIV,
> +	ZX_CLOCK_MUX,
> +	ZX_CLOCK_DIV,
> +	ZX_CLOCK_GATE,
> +};
> +
> +struct zx_clock {
> +	enum zx_clock_type type;
> +	union {
> +		struct zx_pll_desc pll;
> +		struct zx_fixed_divider_desc fixed_div;
> +		struct zx_mux_desc mux;
> +		struct zx_div_desc div;
> +		struct zx_gate_desc gate;
> +	};
> +};
> +
> +struct clk_hw *zx_clk_register_pll(struct device *dev, struct regmap *regmap,
> +				   const struct zx_pll_desc *desc,
> +				   struct clk_hw * const *clocks);
> +struct clk_hw *zx_clk_register_mux(struct device *dev, struct regmap *regmap,
> +				   const struct zx_mux_desc *desc,
> +				   struct clk_hw * const *clocks);
> +struct clk_hw *zx_clk_register_divider(struct device *dev, struct regmap *regmap,
> +				       const struct zx_div_desc *desc,
> +				       struct clk_hw * const *clocks);
> +struct clk_hw *zx_clk_register_gate(struct device *dev, struct regmap *regmap,
> +				    const struct zx_gate_desc *desc,
> +				    struct clk_hw * const *clocks);
> +
> +struct zx_clk_export {
> +	unsigned int priv;
> +	unsigned int binding;
> +};

Sashiko flagged this as unused.

Brian



From bmasney at redhat.com  Mon Aug  3 09:05:27 2026
From: bmasney at redhat.com (Brian Masney)
Date: Mon, 3 Aug 2026 12:05:27 -0400
Subject: [PATCH v9 05/12] clk: zte: Add Clock registration infrastructure
In-Reply-To: <20260802-zx29clk-v9-5-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-5-d05530d85d28@gmail.com>
Message-ID: <anC8R2Nxgv_w7Ahp@redhat.com>

On Sun, Aug 02, 2026 at 11:33:37PM +0300, Stefan D?singer wrote:
> diff --git a/drivers/clk/zte/clk-zx.h b/drivers/clk/zte/clk-zx.h
> new file mode 100644
> index 000000000000..175fbba3e0b3
> --- /dev/null
> +++ b/drivers/clk/zte/clk-zx.h
> @@ -0,0 +1,142 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +/*
> + * Copyright (C) 2026 Stefan D?singer
> + */
> +
> +#ifndef __DRV_CLK_ZX_H
> +#define __DRV_CLK_ZX_H
> +
> +#include <linux/clk-provider.h>
> +#include <linux/platform_device.h>
> +#include <linux/regmap.h>
> +#include <linux/types.h>
> +
> +#define CLK_ZX_PLL_PREPARE_IS_ENABLE	1
> +#define CLK_ZX_MAX_PARENTS		8
> +
> +enum zx_parent_type {
> +	ZX_PARENT_FW,
> +	ZX_PARENT_ID,
> +};
> +
> +struct zx_parent_desc {
> +	enum zx_parent_type		type;
> +	union {
> +		const char		*fw_name;
> +		unsigned int		id;
> +	};
> +};
> +
> +#define PARENT_FW(_name) { .type = ZX_PARENT_FW, .fw_name = (_name) }
> +
> +#define PARENT_ID(_id) { .type = ZX_PARENT_ID, .id = (_id) }
> +
> +static inline struct clk_parent_data zx_get_parent(const struct zx_parent_desc *p,
> +						   struct clk_hw * const *clocks)
> +{
> +	struct clk_parent_data ret = {};
> +
> +	switch (p->type) {
> +	case ZX_PARENT_FW:
> +		ret.fw_name = p->fw_name;
> +		ret.index = -1;
> +		return ret;
> +
> +	case ZX_PARENT_ID:
> +		ret.hw = clocks[p->id];
> +		ret.index = -1;
> +		return ret;
> +	};

Stray semicolon

Brian



From bmasney at redhat.com  Mon Aug  3 09:08:03 2026
From: bmasney at redhat.com (Brian Masney)
Date: Mon, 3 Aug 2026 12:08:03 -0400
Subject: [PATCH v9 06/12] clk: zte: Add regmap-based clocks
In-Reply-To: <20260802-zx29clk-v9-6-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-6-d05530d85d28@gmail.com>
Message-ID: <anC844zWjrph3KlX@redhat.com>

On Sun, Aug 02, 2026 at 11:33:38PM +0300, Stefan D?singer wrote:
> This is based on meson/clk-regmap.c, although slightly simplified. I
> have kept the copyright lines at the top of the file to indicate its
> origin.
> 
> I see that numerous clock drivers have their own incarnation of regmap-
> based mux/div/gate clocks. If there is any version of it that is likely
> to be elevated to shared code like clk-gate.c I'll copy that and try to
> use it as unmodified as possible.

This second paragraph doesn't have to go in the commit log and can go
below the ---. Otherwise looks fine.

> 
> Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>

Reviewed-by: Brian Masney <bmasney at redhat.com>



From bmasney at redhat.com  Mon Aug  3 09:14:47 2026
From: bmasney at redhat.com (Brian Masney)
Date: Mon, 3 Aug 2026 12:14:47 -0400
Subject: [PATCH v9 07/12] clk: zte: Add zx PLL support infrastructure
In-Reply-To: <20260802-zx29clk-v9-7-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-7-d05530d85d28@gmail.com>
Message-ID: <anC-d-cNuHoYD7px@redhat.com>

On Sun, Aug 02, 2026 at 11:33:39PM +0300, Stefan D?singer wrote:
> I am guessing how much of this is reusable among other zx chips or even
> differently named ZTE platforms (if there are any). From reading the old
> zx2967 code, I think the PLL code would be reusable there, maybe with
> platform-specific bitmasks but otherwise the same logic.
> 
> Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>
> 
> ---
> 
> Version 9:
> *) Take req->min_rate and req->max_rate into account when looking for
> possible PLL configurations (sashiko). In practice the code will still
> only ever encounter a fixed request to set dpll to 491.52 MHz.
> 
> *) The same code style changes Brian requested on the other clk patches.
> 
> Version 8:
> *) Document the behavior of unlocked PLLs better: They don't pass
> through their reference/parent, but pass through the fixed clock-26m
> oscillator, even if their reference clock is something else.
> *) dpll has working fractionals. Add this in the comment, but there is
> no actual code support for it - the LTE hardware doesn't need it.
> 
> As for Sashiko's comments on the .set_rate implementation: In practice
> .set_rate will only ever set one rate, 491.52 MHz for dpll. All other
> PLLs are bootloader configured. Dpll could be handled by writing a magic
> constant into its config.
> 
> I want to have the rate finding code as documentation, and maybe there
> is more elaborate future use for it (e.g. more flexible underclocking),
> but attempts to handle eventualities like rate searches or misconfigured
> bootloader values would be dead code.
> 
> Version 7:
> *) Always keep unknownpll enabled when prepared so dpll can acquire a
> lock in its prepare() function.
> *) Clean up error reporting a bit (Sashiko)
> 
> Version 6:
> *) Use abs_diff to compare target and candidate PLL rate (Sashiko).
> *) Use req->best_parent_rate in zx29_pll_determine_rate. Add a TODO
> comment about the parent rate flexibility.
> 
> Version 5: Fix some issues pointed out by Sashiko: NULL dev,
> zx29_pll_recalc_rate error handling, disable PLL again on enable error.
> ---
>  drivers/clk/zte/pll-zx.c | 568 ++++++++++++++++++++++++++++++++++++++++++++++-
>  1 file changed, 565 insertions(+), 3 deletions(-)
> 
> diff --git a/drivers/clk/zte/pll-zx.c b/drivers/clk/zte/pll-zx.c
> index fc76c6524a16..e61b462ec0b4 100644
> --- a/drivers/clk/zte/pll-zx.c
> +++ b/drivers/clk/zte/pll-zx.c
> @@ -4,15 +4,577 @@
>   */
>  
>  #include <linux/clk-provider.h>
> +#include <linux/clk.h>
> +#include <linux/container_of.h>
>  #include <linux/device.h>
> +#include <linux/err.h>
>  #include <linux/errno.h>
> +#include <linux/math.h>
> +#include <linux/rational.h>
>  #include <linux/regmap.h>
> +#include <linux/types.h>
> +#include <linux/units.h>
>  
>  #include "clk-zx.h"
>  
> +/*
> + * This code has only been tested with zx297520v3 PLLs, but from reading the zx296718 clock code it
> + * looks like PLL registers are similar. ZTE's sources explain the PLL register contents only in a
> + * .cmm file (A Lauterbach TRACE32 script) and some unused headers in their U-Boot code dump, which
> + * may not be accurate. When calculating the frequencies from the default PLL configuration the
> + * results match the fixed rate clocks from their clock driver.
> + *
> + * The 26 MHz and 32 kHz clocks can be easily observed with the timers. The 104 MHz output can be
> + * observed through the UART. One 122.88 MHz clock can be observed through the TDM device. All
> + * others can only be indirectly inferred, e.g. by comparing CPU speed or SDIO transfer rate between
> + * the fixed 26 MHz oscillator and the provided PLL frequency.
> + *
> + * The formula to calculate the clock is ((ref / refdiv) * fbdiv) / postdiv1 / postdiv2. The masks
> + * are given below. There are a few control flags:
> + *
> + * Bit 31: Disables the PLL, but passes clock-26m through unmodified. Whether POSTDIV_OUT_DISABLE
> + *         still matters is different between PLLs.
> + * Bit 30: Returns if the PLL is locked
> + * Bit 29: Not named in ZTE's code, but can be set. There is no obvious impact. Lock times are
> + *         unchanged, so it doesn't influence or bypass lock detection. It doesn't raise any IRQs or
> + *         influence GPIOs.
> + * Bit 27: Given its name it likely disables the Delta-Sigma Modulator, if one exists at all. The
> + *         boot ROM sets it on every PLL. Unsetting it marginally decreases the time it takes to
> + *         lock to the reference clock (from ~400 us to ~300 us).
> + * Bit 24: Bypasses the VCO, but still applies refdiv and postdiv. Doesn't matter if PLL_DISABLE=1.
> + *
> + * NB: Some PLLs have an automatic bypass logic that forwards clock-26m (REGARDLESS of reference)
> + * when they don't have a lock, regardless of reason. This can be triggered by disabling the PLL,
> + * setting an out-of-spec VCO frequency or disabling the parent. This shouldn't matter in regular
> + * operation, but caused me some confusion when reverse engineering the clock tree. E.g. clock-26m->
> + * unknownpll(disabled) -> dpll(enabled) counterintuitively results in a 26 MHz output clock.
> + */
> +
> +#define ZX29_PLL_DISABLE			BIT(31)
> +#define ZX29_PLL_LOCKED				BIT(30)
> +#define ZX29_PLL_LOCK_FILTER			BIT(29)
> +#define ZX29_PLL_DSM_DISABLE			BIT(27)
> +#define ZX29_PLL_PARENT_MASK			GENMASK(26, 25)
> +#define ZX29_PLL_PARENT_SHIFT			25
> +#define ZX29_PLL_BYPASS				BIT(24)
> +#define ZX29_PLL_REFDIV_MASK			GENMASK(23, 18)
> +#define ZX29_PLL_REFDIV_SHIFT			18
> +#define ZX29_PLL_FBDIV_MASK			GENMASK(17, 6)
> +#define ZX29_PLL_FBDIV_SHIFT			6
> +#define ZX29_PLL_POSTDIV1_MASK			GENMASK(5, 3)
> +#define ZX29_PLL_POSTDIV1_SHIFT			3
> +#define ZX29_PLL_POSTDIV2_MASK			GENMASK(2, 0)
> +#define ZX29_PLL_POSTDIV2_SHIFT			0
> +
> +/*
> + * The second register has a 24 bit fractional value, which only matters when ZX29_PLL_DSM_DISABLE
> + * is not set, and only seems to matter for dpll. ZTE's firmware does not make use of the fractional
> + * and it is unimplemented in this driver. Experimental testing confirms that it has an impact on
> + * dpll.
> + *
> + * Bits 27:24 contain more flags:
> + *
> + * Bit 27: Setting ZX29_PLL_DACAP slows down the lock time and obviates the speed gained from
> + *         !DSM_DISABLE. No other effect observed.
> + *
> + * Bit 26: ZX29_PLL_4PHASE_OUT_DISABLE is set on some PLLs on boot but not on others. It is set on
> + *         boot on mpll and upll, but not gpll, dpll or unknownpll. I am not sure what it does
> + *         either. The SDIO devices break if they are fed from gpll with this flag set, but they
> + *         work OK if they are fed from mpll without this flag set.
> + *
> + * Bit 25: ZX29_PLL_POSTDIV_OUT_DISABLE seems to disable the PLL output entirely. Whether it is
> + *         bypassed by PLL_DISABLE differs between PLLs. gpll still produces an output clock if
> + *         PLL_DISABLE = 1 and POSTDIV_DISABLE = 1, but produces no output if PLL_DISABLE = 0 and
> + *         POSTDIV_DISABLE = 1. The dpll feeder ("unknownpll") at 0x100 produces no output clock if
> + *         both PLL_DISABLE and POSTDIV_DISABLE are set to 1.
> + *
> + * Bit 24: ZX29_PLL_VCO_OUT_DISABLE probably disables the output of the VCO clock without
> + *         post-VCO-dividers, but the raw VCO output is not a possible parent of any consumer clock,
> + *         so I could not confirm this. It does not disable the VCO entirely - that's what
> + *         PLL_DISABLE does.
> + *
> + * A spinlock should not be needed. PLLs don't share their registers with anything else and the
> + * global prepare mutex and enable spinlock should be enough. Beware of conflicts in reg2 between
> + * POSTDIV_OUT_DISABLE and the fractional value in case you find out how fractional dividers work
> + * and add support for them.
> + */
> +#define ZX29_PLL_REG2_OFFSET			4
> +#define ZX29_PLL_DACAP				BIT(27)
> +#define ZX29_PLL_4PHASE_OUT_DISABLE		BIT(26)
> +#define ZX29_PLL_POSTDIV_OUT_DISABLE		BIT(25)
> +#define ZX29_PLL_VCO_OUT_DISABLE		BIT(24)
> +#define ZX29_PLL_FRACT				GENMASK(23, 0)
> +
> +/*
> + * The VCO's frequency range is limited. The stock settings run the VCO between 960 and 1248 MHz.
> + * Ad-hoc testing with gpll suggests that at least this PLL remains stable down to about 7 MHz and
> + * up to 2 GHz and produces a clock that can be used by the SDIO controller. Attempting to run the
> + * mpll VCO at 624 MHz and setting postdiv1 = postdiv2 = 1 - which should result in the same output
> + * frequency - or running it at 1872 MHz with an effective post divider of 3 crashes the CPU. Most
> + * likely the PLLs become unstable outside their core range and the SDIO controller is much more
> + * forgiving than CPU and DRAM are.
> + */
> +#define ZX29_PLL_VCO_MAX_FREQ			(1300 * HZ_PER_MHZ)
> +#define ZX29_PLL_VCO_MIN_FREQ			(900 * HZ_PER_MHZ)
> +
> +struct zx29_clk_pll {
> +	struct clk_hw	hw;
> +	struct device	*dev;
> +	struct regmap	*map;
> +	u16		reg;
> +};
> +
> +static inline struct zx29_clk_pll *to_zx29_clk_pll(struct clk_hw *hw)
> +{
> +	return container_of(hw, struct zx29_clk_pll, hw);
> +}
> +
> +static int zx29_pll_is_prepared(struct clk_hw *hw)
> +{
> +	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> +	int res;
> +
> +	res = regmap_test_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
> +	if (res < 0)
> +		return res;
> +
> +	return !res;
> +}
> +
> +static int zx29_pll_prepare(struct clk_hw *hw)
> +{
> +	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> +	u32 val = 0;
> +	int res;
> +
> +	res = regmap_clear_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
> +	if (res < 0)
> +		return res;
> +
> +	/* Lock duration is usually between 300 us and 500 us */
> +	res = regmap_read_poll_timeout(pll->map, pll->reg, val, val & ZX29_PLL_LOCKED, 50, 2000);
> +	if (res) {
> +		regmap_set_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
> +		dev_err(pll->dev, "%s: PLL prepare failed: %d. Config value 0x%08x\n",
> +			clk_hw_get_name(&pll->hw), res, val);
> +	}
> +	return res;
> +}
> +
> +static void zx29_pll_unprepare(struct clk_hw *hw)
> +{
> +	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> +
> +	regmap_set_bits(pll->map, pll->reg, ZX29_PLL_DISABLE);
> +}
> +
> +static int zx29_pll_is_enabled(struct clk_hw *hw)
> +{
> +	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> +	int res;
> +
> +	res = regmap_test_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
> +			       ZX29_PLL_POSTDIV_OUT_DISABLE);
> +	if (res < 0)
> +		return res;
> +
> +	return !res;
> +}
> +
> +static int zx29_pll_enable(struct clk_hw *hw)
> +{
> +	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> +
> +	return regmap_clear_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
> +				 ZX29_PLL_POSTDIV_OUT_DISABLE);
> +}
> +
> +static void zx29_pll_disable(struct clk_hw *hw)
> +{
> +	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> +
> +	regmap_set_bits(pll->map, pll->reg + ZX29_PLL_REG2_OFFSET,
> +			ZX29_PLL_POSTDIV_OUT_DISABLE);
> +}
> +
> +static unsigned long zx29_pll_get_rate(const struct zx29_clk_pll *pll, unsigned long parent_rate,
> +				       u32 setting)
> +{
> +	unsigned long refdiv, fbdiv, postdiv1, postdiv2, freq;
> +	const char *name = clk_hw_get_name(&pll->hw);
> +	u64 vco;
> +
> +	refdiv = (setting & ZX29_PLL_REFDIV_MASK) >> ZX29_PLL_REFDIV_SHIFT;
> +	fbdiv = (setting & ZX29_PLL_FBDIV_MASK) >> ZX29_PLL_FBDIV_SHIFT;
> +	postdiv1 = (setting & ZX29_PLL_POSTDIV1_MASK) >> ZX29_PLL_POSTDIV1_SHIFT;
> +	postdiv2 = (setting & ZX29_PLL_POSTDIV2_MASK) >> ZX29_PLL_POSTDIV2_SHIFT;
> +	dev_dbg(pll->dev, "%s: reference clock %lu Hz, PLL setting 0x%08x\n",
> +		name, parent_rate, setting);
> +
> +	if (!refdiv || !postdiv1 || !postdiv2) {
> +		dev_err(pll->dev, "%s: divide by zero (%lu, %lu, %lu)\n", name, refdiv, postdiv1,
> +			postdiv2);
> +		return 0;
> +	}
> +
> +	vco = div_u64((u64)parent_rate * fbdiv, refdiv);
> +	freq = div_u64(div_u64(vco, postdiv1), postdiv2);
> +	dev_dbg(pll->dev, "%s: refdiv %lu fbdiv %lu\n", name, refdiv, fbdiv);
> +	dev_dbg(pll->dev, "%s: postdiv1 %lu postdiv2 %lu\n", name, postdiv1, postdiv2);
> +
> +	dev_dbg(pll->dev, "%s: %lu MHz\n", name, freq / HZ_PER_MHZ);
> +
> +	return freq;
> +}
> +
> +static unsigned long zx29_pll_recalc_rate(struct clk_hw *hw, unsigned long parent_rate)
> +{
> +	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> +	u32 val;
> +	int res;
> +
> +	res = regmap_read(pll->map, pll->reg, &val);
> +	if (res < 0) {
> +		dev_err(pll->dev, "%s: Failed to read PLL settings\n", clk_hw_get_name(&pll->hw));
> +		return 0;
> +	}
> +
> +	return zx29_pll_get_rate(pll, parent_rate, val);
> +}
> +
> +static u32 zx29_pll_calc_values(const struct zx29_clk_pll *pll, unsigned long parent_rate,
> +				unsigned long rate, unsigned long min_rate, unsigned long max_rate)
> +{
> +	const unsigned int postdiv1_max = (1 << hweight32(ZX29_PLL_POSTDIV1_MASK)) - 1;
> +	const unsigned int postdiv2_max = (1 << hweight32(ZX29_PLL_POSTDIV2_MASK)) - 1;
> +	unsigned long fbdiv, refdiv, best_fbdiv = 0, best_refdiv = 0;
> +	u32 postdiv1 = 0, postdiv2 = 0, i, j, setting;
> +	const char *name = clk_hw_get_name(&pll->hw);
> +	long best = LONG_MAX;
> +
> +	/*
> +	 * This code produces the same VCO settings that the boot loader and stock firmware use for
> +	 * the standard frequencies. It has seen only very little manual testing beyond that.
> +	 *
> +	 * The goal is to find a VCO setting that gets us as close as possible to the desired output
> +	 * rate, while being within the VCO's operating limits and achievable with the input value
> +	 * range. It is iterating over possible post-VCO divider values (1-7)*(1-7) to look for
> +	 * valid VCO target frequencies and then looks for refdiv and fbdiv values to achieve the
> +	 * VCO frequency from the reference frequency.
> +	 */
> +	for (j = 1; j <= postdiv2_max; j++) {
> +		for (i = 1; i <= postdiv1_max; i++) {
> +			u64 vco = (u64)rate * i * j;
> +			unsigned long out;
> +
> +			if (vco > ZX29_PLL_VCO_MAX_FREQ || vco < ZX29_PLL_VCO_MIN_FREQ)
> +				continue;
> +
> +			rational_best_approximation(vco, parent_rate,
> +						    (1 << hweight32(ZX29_PLL_FBDIV_MASK)) - 1,
> +						    (1 << hweight32(ZX29_PLL_REFDIV_MASK)) - 1,
> +						    &fbdiv, &refdiv);
> +			setting = fbdiv << ZX29_PLL_FBDIV_SHIFT;
> +			setting |= refdiv << ZX29_PLL_REFDIV_SHIFT;
> +			setting |= i << ZX29_PLL_POSTDIV1_SHIFT;
> +			setting |= j << ZX29_PLL_POSTDIV2_SHIFT;
> +			out = zx29_pll_get_rate(pll, parent_rate, setting);
> +
> +			if (out < min_rate || out > max_rate)
> +				continue;
> +
> +			if (abs_diff(out, rate) > best)
> +				continue;
> +
> +			if (abs_diff(out, rate) < best) {
> +				postdiv1 = i;
> +				postdiv2 = j;
> +				best_fbdiv = fbdiv;
> +				best_refdiv = refdiv;
> +				best = abs_diff(out, rate);
> +
> +				if (!best)
> +					goto search_done;
> +			}
> +		}
> +	}
> +search_done:
> +
> +	if (!postdiv1) {
> +		dev_err(pll->dev, "Did not find a setting for %lu Hz, parent %lu Hz\n",
> +			rate, parent_rate);
> +		return 0;
> +	}
> +
> +	dev_dbg(pll->dev, "%s: parent rate %lu\n", name, parent_rate);
> +	dev_dbg(pll->dev, "%s: found VCO dividers %u and %u\n", name, postdiv1, postdiv2);
> +	dev_dbg(pll->dev, "%s: VCO target rate %lu\n", name, rate * postdiv1 * postdiv2);
> +
> +	dev_dbg(pll->dev, "%s: Got fbdiv = %lu refdiv = %lu\n", name, best_fbdiv, best_refdiv);
> +
> +	setting = best_fbdiv << ZX29_PLL_FBDIV_SHIFT;
> +	setting |= best_refdiv << ZX29_PLL_REFDIV_SHIFT;
> +	setting |= postdiv1 << ZX29_PLL_POSTDIV1_SHIFT;
> +	setting |= postdiv2 << ZX29_PLL_POSTDIV2_SHIFT;
> +	dev_dbg(pll->dev, "%s: Final setting 0x%08x\n", name, setting);
> +
> +	return setting;
> +}
> +
> +static int zx29_pll_determine_rate(struct clk_hw *hw, struct clk_rate_request *req)
> +{
> +	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> +	unsigned long new_rate;
> +	u32 setting;
> +
> +	/*
> +	 * TODO: DPLL can switch between two parents, one of which is another PLL. Take this into
> +	 * account when searching the config space and set req->best_parent_rate.
> +	 *
> +	 * In practice it shouldn't matter though. Dpll is always configured to a fixed frequency
> +	 * and is the only clock with a switchable parent.
> +	 */
> +	if (!req->best_parent_rate) {
> +		dev_err(pll->dev, "Did not expect best_parent_rate=0\n");
> +		return -EINVAL;
> +	}
> +
> +	setting = zx29_pll_calc_values(pll, req->best_parent_rate, req->rate, req->min_rate,
> +				       req->max_rate);
> +	if (!setting)
> +		return -EINVAL;
> +
> +	new_rate = zx29_pll_get_rate(pll, req->best_parent_rate, setting);
> +	if (new_rate != req->rate) {
> +		dev_warn(pll->dev, "Did not find an exact match. Want %lu, got %lu\n",
> +			 req->rate, new_rate);

Could this also be dev_dbg() ?

> +		req->rate = new_rate;
> +	}
> +
> +	return 0;
> +}
> +
> +static int zx29_pll_set_rate(struct clk_hw *hw, unsigned long rate,
> +			     unsigned long parent_rate)
> +{
> +	struct zx29_clk_pll *pll = to_zx29_clk_pll(hw);
> +	u32 setting;
> +
> +	/*
> +	 * TODO: Implement gradual PLL rate change. PLLs can be changed while they are running and
> +	 * downstream hardware is generally fine with that. The exception is DRAM, which reads
> +	 * incorrect values if changed too fast.
> +	 *
> +	 * Changing the mpll rate is potentially useful for over/underclocking. Gating mpll is
> +	 * unrealistic because too many devices depend on it.
> +	 */
> +	setting = zx29_pll_calc_values(pll, parent_rate, rate, rate, rate);
> +	if (!setting)
> +		return -EINVAL;
> +
> +	dev_info(pll->dev, "%s: Setting new configuration: 0x%08x\n", clk_hw_get_name(hw), setting);

dev_dbg()

Brian



From stefandoesinger at gmail.com  Mon Aug  3 10:48:18 2026
From: stefandoesinger at gmail.com (Stefan =?UTF-8?B?RMO2c2luZ2Vy?=)
Date: Mon, 03 Aug 2026 20:48:18 +0300
Subject: [PATCH v9 05/12] clk: zte: Add Clock registration infrastructure
In-Reply-To: <anC72Cf1zKcLYWlC@redhat.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-5-d05530d85d28@gmail.com> <anC72Cf1zKcLYWlC@redhat.com>
Message-ID: <ky2t5IqOTfWMdfZsXE91Gg@gmail.com>

Hi Brian,

Thanks again for the review!

Am Montag, 3. August 2026, 19:03:36 Ostafrikanische Zeit schrieb Brian Masney:
> > +	/*
> > +	 * Sanity check: Make sure all parents are there and write a clear
> > message rather than +	 * leave potential orphans.
> > +	 */
> 
> It's not common for clk drivers to have a validate function like this.
> That said, I know you are reverse engineering what's in the downstream
> BSP without documentation, and there is value in having this. I think
> it'd be useful to clarify that in the comments because code usually gets
> copied and pasted between drivers over time.
>
> That said, once it's verified that the clock tree is correct, can these
> checks be eventually dropped so that this validation logic isn't ran on
> every boot?

Yes, I plan to remove it eventually when support for the SoC is more mature. 
It has saved me some head-scratching as I have been working and re-working the 
clock tree. I'll clarify its driver specific role in the next submission.

Another option I contemplated is wrapping it in #ifdef DEBUG. What do you 
think about that?

> > +struct zx_clk_export {
> > +	unsigned int priv;
> > +	unsigned int binding;
> > +};
> 
> Sashiko flagged this as unused.

Indeed, that is a leftover from earlier - before I realized a simple unsigned 
int[] does the job fine.

---

I have a long-standing question about tristate/module support for drivers like 
this: I don't think the driver can realistically be unloaded. I have been 
testing driver unloading by removing the UART clocks from the DT (otherwise 
the clock driver is busy) and marking all clocks critical (otherwise 
unloading/unbinding will shut down the UART (and more) and lock me out of the 
system).

I have made it tristate because from early research into clock driver state of 
the art I gathered it was desired, even for drivers necessary for fundamental 
operation [0]. Did I understand this correctly? It also uncovered some linking 
errors that weren't obvious when compiling the driver into the kernel.

That said, I'd prefer to mark my init tables discardable with __initdata and 
think the module load support is going to go untested and bitrot over time.

Cheers,
Stefan

0: Chen-Yu Tsai's tak here mostly: https://www.youtube.com/watch?v=d1VIAnVb3hI
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 870 bytes
Desc: This is a digitally signed message part.
URL: <http://lists.infradead.org/pipermail/linux-phy/attachments/20260803/f4574edf/attachment.sig>

From mohd.anwar at oss.qualcomm.com  Mon Aug  3 12:27:56 2026
From: mohd.anwar at oss.qualcomm.com (Mohd Ayaan Anwar)
Date: Tue, 04 Aug 2026 00:57:56 +0530
Subject: [PATCH v2 0/6] Fix regulator voting for the Qualcomm SGMII SerDes
 PHY
Message-ID: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>

This is a long overdue v2 of the original patch series which tried to
address a SerDes power-up failure observed on the QCS8300 Ride board
using the phy-qcom-sgmii-eth driver. After going through a rabbit hole
of hardware schematics and power data, I now have a fuller picture of
this IP.

The Qualcomm SGMII SerDes PHY (used in Lemans and Monaco SoCs) is
powered by two voltage rails. The schematics call them vdda-0p9 and
vdda-1p2, so that's what we follow in the series. Additionally, the
0.9V regulator draws a peak current of 46 mA and the 1.2V regulator
draws a peak current of 15 mA.

If both rails are not enabled, we see the original issue where the
SerDes calibration times out:

  qcom-dwmac-sgmii-phy 8909000.phy: QSERDES_COM_C_READY_STATUS timed-out
  qcom-ethqos 23040000.ethernet eth0: __stmmac_open: Serdes powerup failed

Currently, the DT nodes across affected boards incorrectly describe
only a single regulator via the generic 'phy-supply' property (and in
some cases point to the wrong rail entirely). Moreover, the driver does
not set the proper current loads and just relies on an enable vote.

This series corrects this by:

  1. Fixing the DT binding to describe both named supplies.
  2. Fixing all affected board DTS to use the correct vdda-0p9-supply
     and vdda-1p2-supply properties.
  3. Adding bulk regulator support to the PHY driver so it enables both
     supplies and sets the proper current loads.

The patches are sequenced to maintain bisectability. With the driver
change, the PHY framework will continue to vote for the regulator
described by 'phy-supply'. The DT changes which follow then remove
'phy-supply' in favour of 'vdda-0p9' and 'vdda-1p2'.

Testing:
This series was tested on the following Lemans and Monaco based boards.
  1. Lemans IQ-9075 EVK with the IFP+ Mezzanine attach (dual GMAC +
 QCOM SGMII SerDes + QCA8081 PHY)
  2. Lemans QCS9100 Ride (dual GMAC + QCOM SGMII SerDes + AQR115C PHY)
  3. Monaco IQ-8275 EVK (single GMAC + QCOM SGMII SerDes + QCA8081 PHY)
  4. Monaco QCS8300 Ride (dual GMAC + QCOM SGMII SerDes + AQR115C PHY)
To conclusively test the new regulator voting, I tested builds with all
the other consumers of the regulators disabled from the DTS to ensure
that the phy-qcom-sgmii-eth driver held the only vote.

Changes since v1:
  - Fixed the DT bindings to describe the actual SerDes PHY hardware,
    which actually has two supplies rather than only one - Krzysztof.
  - Use regulator_bulk_* APIs to properly set the current loads of the
    two supplies - Konrad.
  - Fixed Lemans board DTS files that incorrectly voted for vreg_l5a;
    the correct rails per the power diagram are vreg_l4a (0.9V) and
    vreg_l1c (1.2V).
  - Link to v1: https://lore.kernel.org/linux-arm-msm/20251124-sgmiieth_serdes_regulator-v1-0-73ae8f9cbe2a at oss.qualcomm.com/T/#m0b93e06a0d6a09c4c1b65c500b475958ed5e288d.

Signed-off-by: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>

---
Mohd Ayaan Anwar (6):
      dt-bindings: phy: qcom,sa8775p-dwmac-sgmii-phy: add named voltage rails and deprecate phy-supply
      phy: qcom: sgmii-eth: vote for both voltage rails with correct current loads
      arm64: dts: qcom: lemans-evk: fix SerDes PHY regulator supplies
      arm64: dts: qcom: lemans-ride-common: fix SerDes PHY regulator supplies
      arm64: dts: qcom: monaco-evk: fix SerDes PHY regulator supplies
      arm64: dts: qcom: qcs8300-ride: fix SerDes PHY regulator supplies

 .../bindings/phy/qcom,sa8775p-dwmac-sgmii-phy.yaml | 11 +++++++
 .../boot/dts/qcom/lemans-evk-ifp-mezzanine.dtso    |  3 +-
 arch/arm64/boot/dts/qcom/lemans-evk.dts            |  3 +-
 arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi   |  8 +++--
 arch/arm64/boot/dts/qcom/monaco-evk.dts            |  3 +-
 arch/arm64/boot/dts/qcom/qcs8300-ride.dts          |  4 ++-
 drivers/phy/qualcomm/phy-qcom-sgmii-eth.c          | 37 +++++++++++++++++++---
 7 files changed, 59 insertions(+), 10 deletions(-)
---
base-commit: 415606a7be939835db9b0d6b711887586646346d
change-id: 20260803-b4-sgmiieth_serdes_regulator-0cf9e58dbb12

Best regards,
-- 
Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>



From mohd.anwar at oss.qualcomm.com  Mon Aug  3 12:27:57 2026
From: mohd.anwar at oss.qualcomm.com (Mohd Ayaan Anwar)
Date: Tue, 04 Aug 2026 00:57:57 +0530
Subject: [PATCH v2 1/6] dt-bindings: phy: qcom,sa8775p-dwmac-sgmii-phy: add
 named voltage rails and deprecate phy-supply
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260804-b4-sgmiieth_serdes_regulator-v2-1-c4bc688177dd@oss.qualcomm.com>

The Qualcomm SGMII SerDes PHY has two distinct voltage supply rails,
vdda-0p9 and vdda-1p2. The binding incorrectly described only a single
generic supply via 'phy-supply'.

Deprecate 'phy-supply' and instead add two named supply properties. The
0.9V rail draws a peak current of 46 mA and the 1.2V rail draws 15 mA.

Fixes: 97b795125704 ("dt-bindings: phy: describe the Qualcomm SGMII PHY")
Signed-off-by: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>
---
 .../devicetree/bindings/phy/qcom,sa8775p-dwmac-sgmii-phy.yaml | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/Documentation/devicetree/bindings/phy/qcom,sa8775p-dwmac-sgmii-phy.yaml b/Documentation/devicetree/bindings/phy/qcom,sa8775p-dwmac-sgmii-phy.yaml
index 90fc8c039219c739eae05cc17108a9a2fc6193df..e573abcb937df914ced25ec87add7955bbba7b87 100644
--- a/Documentation/devicetree/bindings/phy/qcom,sa8775p-dwmac-sgmii-phy.yaml
+++ b/Documentation/devicetree/bindings/phy/qcom,sa8775p-dwmac-sgmii-phy.yaml
@@ -35,6 +35,17 @@ properties:
   phy-supply:
     description:
       Phandle to a regulator that provides power to the PHY.
+    deprecated: true
+
+  vdda-0p9-supply:
+    description:
+      Phandle to a 0.9V regulator supply to the PHY, which draws a peak
+      current of 46 mA.
+
+  vdda-1p2-supply:
+    description:
+      Phandle to a 1.2V regulator supply to the PHY, which draws a peak
+      current of 15 mA.
 
   "#phy-cells":
     const: 0

-- 
2.34.1



From mohd.anwar at oss.qualcomm.com  Mon Aug  3 12:27:58 2026
From: mohd.anwar at oss.qualcomm.com (Mohd Ayaan Anwar)
Date: Tue, 04 Aug 2026 00:57:58 +0530
Subject: [PATCH v2 2/6] phy: qcom: sgmii-eth: vote for both voltage rails
 with correct current loads
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260804-b4-sgmiieth_serdes_regulator-v2-2-c4bc688177dd@oss.qualcomm.com>

The SerDes PHY has two voltage supply rails, vdda-0p9 and vdda-1p2,
that must both be enabled for calibration to succeed. Without them:

  qcom-dwmac-sgmii-phy 8909000.phy: QSERDES_COM_C_READY_STATUS timed-out
  qcom-ethqos 23040000.ethernet eth0: __stmmac_open: Serdes powerup failed

The driver relied solely on the PHY framework's implicit enable of
'phy-supply', which only voted for a single rail and set no current
load. Use devm_regulator_bulk_get_const() to acquire both supplies and
set the peak current loads (46 mA for vdda-0p9, 15 mA for vdda-1p2)
as required by the hardware.

Fixes: 601d06277007 ("phy: qcom: add the SGMII SerDes PHY driver")
Signed-off-by: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>
---
 drivers/phy/qualcomm/phy-qcom-sgmii-eth.c | 37 +++++++++++++++++++++++++++----
 1 file changed, 33 insertions(+), 4 deletions(-)

diff --git a/drivers/phy/qualcomm/phy-qcom-sgmii-eth.c b/drivers/phy/qualcomm/phy-qcom-sgmii-eth.c
index f48faa2929a682be6024ee349fac6fd1b6464cb3..7137200c3f310a4d2642bb9bdbe3adb7589b2241 100644
--- a/drivers/phy/qualcomm/phy-qcom-sgmii-eth.c
+++ b/drivers/phy/qualcomm/phy-qcom-sgmii-eth.c
@@ -11,6 +11,7 @@
 #include <linux/phy/phy.h>
 #include <linux/platform_device.h>
 #include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
 
 #include "phy-qcom-qmp-pcs-sgmii.h"
 #include "phy-qcom-qmp-qserdes-com-v5.h"
@@ -26,7 +27,15 @@
 #define QSERDES_PCS_SGMIIPHY_READY			BIT(7)
 #define QSERDES_COM_C_PLL_LOCKED			BIT(1)
 
+static const struct regulator_bulk_data qcom_dwmac_sgmii_phy_vregs[] = {
+	{ .supply = "vdda-0p9", .init_load_uA = 46000 },
+	{ .supply = "vdda-1p2", .init_load_uA = 15000 },
+};
+
+#define QCOM_SGMII_NUM_SUPPLIES		ARRAY_SIZE(qcom_dwmac_sgmii_phy_vregs)
+
 struct qcom_dwmac_sgmii_phy_data {
+	struct regulator_bulk_data *vregs;
 	struct regmap *regmap;
 	struct clk *refclk;
 	phy_interface_t interface;
@@ -273,13 +282,24 @@ static int qcom_dwmac_sgmii_phy_power_on(struct phy *phy)
 	struct qcom_dwmac_sgmii_phy_data *data = phy_get_drvdata(phy);
 	int ret;
 
-	ret = clk_prepare_enable(data->refclk);
-	if (ret < 0)
+	ret = regulator_bulk_enable(QCOM_SGMII_NUM_SUPPLIES, data->vregs);
+	if (ret)
 		return ret;
 
+	ret = clk_prepare_enable(data->refclk);
+	if (ret)
+		goto err_disable_regulators;
+
 	ret = qcom_dwmac_sgmii_phy_calibrate(phy);
-	if (ret < 0)
-		clk_disable_unprepare(data->refclk);
+	if (ret)
+		goto err_disable_clk;
+
+	return 0;
+
+err_disable_clk:
+	clk_disable_unprepare(data->refclk);
+err_disable_regulators:
+	regulator_bulk_disable(QCOM_SGMII_NUM_SUPPLIES, data->vregs);
 
 	return ret;
 }
@@ -296,6 +316,8 @@ static int qcom_dwmac_sgmii_phy_power_off(struct phy *phy)
 
 	clk_disable_unprepare(data->refclk);
 
+	regulator_bulk_disable(QCOM_SGMII_NUM_SUPPLIES, data->vregs);
+
 	return 0;
 }
 
@@ -357,6 +379,7 @@ static int qcom_dwmac_sgmii_phy_probe(struct platform_device *pdev)
 	struct phy_provider *provider;
 	void __iomem *base;
 	struct phy *phy;
+	int ret;
 
 	data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
 	if (!data)
@@ -381,6 +404,12 @@ static int qcom_dwmac_sgmii_phy_probe(struct platform_device *pdev)
 	if (IS_ERR(data->refclk))
 		return PTR_ERR(data->refclk);
 
+	ret = devm_regulator_bulk_get_const(dev, QCOM_SGMII_NUM_SUPPLIES,
+					    qcom_dwmac_sgmii_phy_vregs,
+					    &data->vregs);
+	if (ret)
+		return ret;
+
 	provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
 	if (IS_ERR(provider))
 		return PTR_ERR(provider);

-- 
2.34.1



From mohd.anwar at oss.qualcomm.com  Mon Aug  3 12:27:59 2026
From: mohd.anwar at oss.qualcomm.com (Mohd Ayaan Anwar)
Date: Tue, 04 Aug 2026 00:57:59 +0530
Subject: [PATCH v2 3/6] arm64: dts: qcom: lemans-evk: fix SerDes PHY
 regulator supplies
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260804-b4-sgmiieth_serdes_regulator-v2-3-c4bc688177dd@oss.qualcomm.com>

The SerDes PHY nodes were configured with 'phy-supply = <&vreg_l5a>',
which was incorrect on two counts: vreg_l5a is not a supply the PHY
requires, and only one rail was described whereas the hardware needs
two.

The PHY requires vdda-0p9 (vreg_l4a) and vdda-1p2 (vreg_l1c). Replace
the incorrect phy-supply with these two named properties on both the
base EVK node and the IFP mezzanine overlay.

Fixes: 71ee90ed1756 ("arm64: dts: qcom: lemans-evk: Enable 2.5G Ethernet interface")
Signed-off-by: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>
---
 arch/arm64/boot/dts/qcom/lemans-evk-ifp-mezzanine.dtso | 3 ++-
 arch/arm64/boot/dts/qcom/lemans-evk.dts                | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/arch/arm64/boot/dts/qcom/lemans-evk-ifp-mezzanine.dtso b/arch/arm64/boot/dts/qcom/lemans-evk-ifp-mezzanine.dtso
index 6cfa0bc1440e91d32df0695ae4059050e4e20b56..8651c412d0288fcf2ab69fbfaed86f68fb2b4fc5 100644
--- a/arch/arm64/boot/dts/qcom/lemans-evk-ifp-mezzanine.dtso
+++ b/arch/arm64/boot/dts/qcom/lemans-evk-ifp-mezzanine.dtso
@@ -291,7 +291,8 @@ pcie at 0,1 {
 };
 
 &serdes1 {
-	phy-supply = <&vreg_l5a>;
+	vdda-0p9-supply = <&vreg_l4a>;
+	vdda-1p2-supply = <&vreg_l1c>;
 
 	status = "okay";
 };
diff --git a/arch/arm64/boot/dts/qcom/lemans-evk.dts b/arch/arm64/boot/dts/qcom/lemans-evk.dts
index 0c53640c42a65c62375e71f7ae79cbc834a05888..5c046b9bfb3cdec59cffc1dfdbf9f2a51dc9721b 100644
--- a/arch/arm64/boot/dts/qcom/lemans-evk.dts
+++ b/arch/arm64/boot/dts/qcom/lemans-evk.dts
@@ -978,7 +978,8 @@ &sdhc {
 };
 
 &serdes0 {
-	phy-supply = <&vreg_l5a>;
+	vdda-0p9-supply = <&vreg_l4a>;
+	vdda-1p2-supply = <&vreg_l1c>;
 
 	status = "okay";
 };

-- 
2.34.1



From mohd.anwar at oss.qualcomm.com  Mon Aug  3 12:28:00 2026
From: mohd.anwar at oss.qualcomm.com (Mohd Ayaan Anwar)
Date: Tue, 04 Aug 2026 00:58:00 +0530
Subject: [PATCH v2 4/6] arm64: dts: qcom: lemans-ride-common: fix SerDes
 PHY regulator supplies
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260804-b4-sgmiieth_serdes_regulator-v2-4-c4bc688177dd@oss.qualcomm.com>

Both SerDes PHY nodes were configured with 'phy-supply = <&vreg_l5a>',
which was incorrect on two counts: vreg_l5a is not a supply the PHY
requires, and only one rail was described where the hardware needs two.

The PHY requires vdda-0p9 (vreg_l4a) and vdda-1p2 (vreg_l1c). Replace
the incorrect phy-supply with these two named properties on both nodes.

Fixes: 5ef26fb8b3ed ("arm64: dts: qcom: sa8775p-ride: enable the SerDes PHY")
Signed-off-by: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>
---
 arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi b/arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi
index cefb8ff0080631e25e9dd3a05cd810c6955bf1fe..923fd8736f31df2eb5198b27ddf52c171f7b2943 100644
--- a/arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi
+++ b/arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi
@@ -856,12 +856,16 @@ &qup_uart17_rx {
 };
 
 &serdes0 {
-	phy-supply = <&vreg_l5a>;
+	vdda-0p9-supply = <&vreg_l4a>;
+	vdda-1p2-supply = <&vreg_l1c>;
+
 	status = "okay";
 };
 
 &serdes1 {
-	phy-supply = <&vreg_l5a>;
+	vdda-0p9-supply = <&vreg_l4a>;
+	vdda-1p2-supply = <&vreg_l1c>;
+
 	status = "okay";
 };
 

-- 
2.34.1



From mohd.anwar at oss.qualcomm.com  Mon Aug  3 12:28:01 2026
From: mohd.anwar at oss.qualcomm.com (Mohd Ayaan Anwar)
Date: Tue, 04 Aug 2026 00:58:01 +0530
Subject: [PATCH v2 5/6] arm64: dts: qcom: monaco-evk: fix SerDes PHY
 regulator supplies
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260804-b4-sgmiieth_serdes_regulator-v2-5-c4bc688177dd@oss.qualcomm.com>

The SerDes PHY node was configured with 'phy-supply = <&vreg_l4a>',
which only described one of the two supplies the hardware requires.
While vreg_l4a is the correct 0.9V rail, the 1.2V rail (vreg_l5a)
was missing entirely.

Replace phy-supply with the two named properties vdda-0p9-supply
(vreg_l4a) and vdda-1p2-supply (vreg_l5a).

Fixes: 117d6bc9326b ("arm64: dts: qcom: qcs8300: Add Monaco EVK board")
Signed-off-by: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>
---
 arch/arm64/boot/dts/qcom/monaco-evk.dts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/arch/arm64/boot/dts/qcom/monaco-evk.dts b/arch/arm64/boot/dts/qcom/monaco-evk.dts
index 9d17ef7d2caf130564efc2d0159ac0de39126e80..f787e78b35ce48d7544116e4383561b81def8ec7 100644
--- a/arch/arm64/boot/dts/qcom/monaco-evk.dts
+++ b/arch/arm64/boot/dts/qcom/monaco-evk.dts
@@ -706,7 +706,8 @@ &sdhc_1 {
 };
 
 &serdes0 {
-	phy-supply = <&vreg_l4a>;
+	vdda-0p9-supply = <&vreg_l4a>;
+	vdda-1p2-supply = <&vreg_l5a>;
 
 	status = "okay";
 };

-- 
2.34.1



From mohd.anwar at oss.qualcomm.com  Mon Aug  3 12:28:02 2026
From: mohd.anwar at oss.qualcomm.com (Mohd Ayaan Anwar)
Date: Tue, 04 Aug 2026 00:58:02 +0530
Subject: [PATCH v2 6/6] arm64: dts: qcom: qcs8300-ride: fix SerDes PHY
 regulator supplies
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260804-b4-sgmiieth_serdes_regulator-v2-6-c4bc688177dd@oss.qualcomm.com>

The SerDes PHY node was configured with 'phy-supply = <&vreg_l5a>',
which only covered the 1.2V rail. The 0.9V rail (vreg_l4a) was missing
entirely.

Replace phy-supply with the two named properties vdda-0p9-supply
(vreg_l4a) and vdda-1p2-supply (vreg_l5a).

Fixes: 787cb3b4c434 ("arm64: dts: qcom: qcs8300-ride: enable ethernet0")
Signed-off-by: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>
---
 arch/arm64/boot/dts/qcom/qcs8300-ride.dts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/arch/arm64/boot/dts/qcom/qcs8300-ride.dts b/arch/arm64/boot/dts/qcom/qcs8300-ride.dts
index 1ff39530ea3deebe454416d70a289e44c2d89e3f..76c34215dbdf07e520fe96d350f52da8c4485c97 100644
--- a/arch/arm64/boot/dts/qcom/qcs8300-ride.dts
+++ b/arch/arm64/boot/dts/qcom/qcs8300-ride.dts
@@ -702,7 +702,9 @@ &remoteproc_gpdsp {
 };
 
 &serdes0 {
-	phy-supply = <&vreg_l5a>;
+	vdda-0p9-supply = <&vreg_l4a>;
+	vdda-1p2-supply = <&vreg_l5a>;
+
 	status = "okay";
 };
 

-- 
2.34.1



From sashiko-bot at kernel.org  Mon Aug  3 12:36:28 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 19:36:28 +0000
Subject: [PATCH v2 6/6] arm64: dts: qcom: qcs8300-ride: fix SerDes PHY
 regulator supplies
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-6-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
 <20260804-b4-sgmiieth_serdes_regulator-v2-6-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260803193628.E6F681F000E9@smtp.kernel.org>

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

Pre-existing issues:
- [High] Incomplete device tree migration for SerDes PHY regulator supplies breaks Ethernet on Monaco Monza SOM.
--

--- Patch [6]: [PATCH v2 6/6] arm64: dts: qcom: qcs8300-ride: fix SerDes PHY regulator supplies ---
commit 8e38978ea8c058004487a19a9a664f9946b8306c
Author: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>

arm64: dts: qcom: qcs8300-ride: fix SerDes PHY regulator supplies

This commit fixes the SerDes PHY node in qcs8300-ride.dts by replacing the
single phy-supply property with the required vdda-0p9-supply and
vdda-1p2-supply properties. This ensures both the 1.2V and 0.9V rails are
properly provided to the PHY.

> diff --git a/arch/arm64/boot/dts/qcom/qcs8300-ride.dts b/arch/arm64/boot/dts/qcom/qcs8300-ride.dts
> index 1ff39530ea3de..76c34215dbdf0 100644
> --- a/arch/arm64/boot/dts/qcom/qcs8300-ride.dts
> +++ b/arch/arm64/boot/dts/qcom/qcs8300-ride.dts
> @@ -702,7 +702,9 @@ &remoteproc_gpdsp {
>  };
>  
>  &serdes0 {
> -	phy-supply = <&vreg_l5a>;
> +	vdda-0p9-supply = <&vreg_l4a>;
> +	vdda-1p2-supply = <&vreg_l5a>;
> +
>  	status = "okay";
>  };

[Severity: High]
This isn't a bug introduced by this patch, but I noticed that another file in
the tree, arch/arm64/boot/dts/qcom/monaco-monza-som.dtsi, still has the exact
same configuration you are fixing here. It retains the deprecated phy-supply
property and lacks the required 0.9V rail:

&serdes0 {
	phy-supply = <&vreg_l5a>;

	status = "okay";
};

Since the PHY driver strictly expects the two named regulator supplies via
devm_regulator_bulk_get_const, won't leaving this old configuration break
Ethernet on the Monaco Monza SOM during boot by either failing to probe
or timing out on calibration? 

Should monaco-monza-som.dtsi be updated similarly in this patch or as a
follow-up in this series?

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd at oss.qualcomm.com?part=6


From sashiko-bot at kernel.org  Mon Aug  3 12:36:49 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 19:36:49 +0000
Subject: [PATCH v2 1/6] dt-bindings: phy: qcom,sa8775p-dwmac-sgmii-phy:
 add named voltage rails and deprecate phy-supply
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-1-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
 <20260804-b4-sgmiieth_serdes_regulator-v2-1-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260803193650.3D5A71F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The device tree binding deprecates `phy-supply` in favor of new named regulators `vdda-0p9-supply` and `vdda-1p2-supply`, but the driver (`phy-qcom-sgmii-eth.c`) was not updated to request or enable them. When device trees are updated to follow the new binding and drop the deprecated `phy-supply`, the PHY subsystem core (`phy-core.c`) will no longer find and enable the generic `phy` regulator, leaving the PHY unpowered.
--

--- Patch [1]: [PATCH v2 1/6] dt-bindings: phy: qcom,sa8775p-dwmac-sgmii-phy: add named voltage rails and deprecate phy-supply ---
commit 3cc6ac4d3b35571e8657dd8e304e3f7c33b822ec
Author: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>

dt-bindings: phy: qcom,sa8775p-dwmac-sgmii-phy: add named voltage rails and deprecate phy-supply

The Qualcomm SGMII SerDes PHY has two distinct voltage supply rails. This
commit deprecates the generic 'phy-supply' property and instead adds two
specific named supply properties.

> diff --git a/Documentation/devicetree/bindings/phy/qcom,sa8775p-dwmac-sgmii-phy.yaml b/Documentation/devicetree/bindings/phy/qcom,sa8775p-dwmac-sgmii-phy.yaml
> index 90fc8c039219c..e573abcb937df 100644
> --- a/Documentation/devicetree/bindings/phy/qcom,sa8775p-dwmac-sgmii-phy.yaml
> +++ b/Documentation/devicetree/bindings/phy/qcom,sa8775p-dwmac-sgmii-phy.yaml
> @@ -35,6 +35,17 @@ properties:
>    phy-supply:
>      description:
>        Phandle to a regulator that provides power to the PHY.
> +    deprecated: true
> +
> +  vdda-0p9-supply:
> +    description:
> +      Phandle to a 0.9V regulator supply to the PHY, which draws a peak
> +      current of 46 mA.
> +
> +  vdda-1p2-supply:
> +    description:
> +      Phandle to a 1.2V regulator supply to the PHY, which draws a peak
> +      current of 15 mA.

[Severity: High]
Does this change cause a functional regression when device trees are updated
to follow the new binding?

When device tree authors update their nodes to use vdda-0p9-supply and
vdda-1p2-supply instead of the deprecated phy-supply, it appears the
regulators will remain unpowered.

The PHY subsystem core (phy-core.c) looks for the generic phy-supply during
phy_create(). If it is missing from the device tree, the core will not
enable it.

At the same time, the corresponding driver does not seem to have been
updated to fetch and enable these new specific regulators:

drivers/phy/qualcomm/phy-qcom-sgmii-eth.c:qcom_dwmac_sgmii_phy_probe()

Should qcom_dwmac_sgmii_phy_probe() be updated alongside this binding to
request the new regulators (for example, using devm_regulator_bulk_get()),
so the PHY doesn't fail to initialize when phy-supply is omitted?

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd at oss.qualcomm.com?part=1


From sashiko-bot at kernel.org  Mon Aug  3 12:37:00 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 19:37:00 +0000
Subject: [PATCH v2 3/6] arm64: dts: qcom: lemans-evk: fix SerDes PHY
 regulator supplies
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-3-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
 <20260804-b4-sgmiieth_serdes_regulator-v2-3-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260803193701.42D331F000E9@smtp.kernel.org>

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

Pre-existing issues:
- [High] Incomplete fix for SGMII SerDes PHY regulator supplies leaving downstream boards broken.
--

--- Patch [3]: [PATCH v2 3/6] arm64: dts: qcom: lemans-evk: fix SerDes PHY regulator supplies ---
commit 06c318320f4f12178e107aa08e3b5435de261d50
Author: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>

arm64: dts: qcom: lemans-evk: fix SerDes PHY regulator supplies

This commit fixes the SerDes PHY regulator supplies for the lemans-evk board
and its IFP mezzanine overlay. It replaces the incorrect phy-supply with
vdda-0p9-supply and vdda-1p2-supply.

[ ... ]

> diff --git a/arch/arm64/boot/dts/qcom/lemans-evk.dts b/arch/arm64/boot/dts/qcom/lemans-evk.dts
> index 0c53640c42a65..5c046b9bfb3cd 100644
> --- a/arch/arm64/boot/dts/qcom/lemans-evk.dts
> +++ b/arch/arm64/boot/dts/qcom/lemans-evk.dts

[ ... ]

> @@ -978,7 +978,8 @@ &sdhc {
>  };
>  
>  &serdes0 {
> -	phy-supply = <&vreg_l5a>;
> +	vdda-0p9-supply = <&vreg_l4a>;
> +	vdda-1p2-supply = <&vreg_l1c>;

[Severity: High]
This isn't a bug introduced by this patch, but should this fix be applied to
other boards using this PHY?

It looks like several other device trees still define the deprecated
phy-supply property, such as in
arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi:

&serdes0 {
	phy-supply = <&vreg_l5a>;
	status = "okay";
};

And similarly in arch/arm64/boot/dts/qcom/qcs8300-ride.dts, 
arch/arm64/boot/dts/qcom/monaco-monza-som.dtsi, and 
arch/arm64/boot/dts/qcom/monaco-evk.dts.

Booting these unmodified boards could result in the PHY driver either failing
to probe or falling back to dummy regulators and failing calibration, breaking
SGMII Ethernet connectivity.

Could the fix be extended to cover these other boards as well to ensure they
receive both required voltage supplies?

>  
>  	status = "okay";
>  };

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd at oss.qualcomm.com?part=3


From sashiko-bot at kernel.org  Mon Aug  3 12:38:33 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 19:38:33 +0000
Subject: [PATCH v2 2/6] phy: qcom: sgmii-eth: vote for both voltage
 rails with correct current loads
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-2-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
 <20260804-b4-sgmiieth_serdes_regulator-v2-2-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260803193833.ECF8D1F000E9@smtp.kernel.org>

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

Pre-existing issues:
- [High] The PHY provider is registered before its driver data is set, creating a race condition where a consumer could trigger a NULL pointer dereference.
--

--- Patch [2]: [PATCH v2 2/6] phy: qcom: sgmii-eth: vote for both voltage rails with correct current loads ---
commit 1203fb5a9ba082470a0383befe64f62c484ac7d5
Author: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>

phy: qcom: sgmii-eth: vote for both voltage rails with correct current loads

This commit updates the driver to explicitly vote for both voltage supply
rails with their correct peak current loads using
devm_regulator_bulk_get_const(), rather than relying on the PHY framework's
implicit single-rail voting. It also adds the necessary regulator
enable/disable calls in the power path.

> diff --git a/drivers/phy/qualcomm/phy-qcom-sgmii-eth.c b/drivers/phy/qualcomm/phy-qcom-sgmii-eth.c
> index f48faa2929a68..7137200c3f310 100644
> --- a/drivers/phy/qualcomm/phy-qcom-sgmii-eth.c
> +++ b/drivers/phy/qualcomm/phy-qcom-sgmii-eth.c

[ ... ]

> @@ -381,6 +404,12 @@ static int qcom_dwmac_sgmii_phy_probe(struct platform_device *pdev)
>  	if (IS_ERR(data->refclk))
>  		return PTR_ERR(data->refclk);
>  
> +	ret = devm_regulator_bulk_get_const(dev, QCOM_SGMII_NUM_SUPPLIES,
> +					    qcom_dwmac_sgmii_phy_vregs,
> +					    &data->vregs);
> +	if (ret)
> +		return ret;
> +
>  	provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
>  	if (IS_ERR(provider))
>  		return PTR_ERR(provider);

[Severity: High]
This is a pre-existing issue, but does registering the PHY provider before
setting the driver data create a race condition here?

Looking at the end of qcom_dwmac_sgmii_phy_probe():

	provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
	if (IS_ERR(provider))
		return PTR_ERR(provider);

	phy_set_drvdata(phy, data);

By calling devm_of_phy_provider_register() first, the PHY is made available
to consumers before phy_set_drvdata() initializes the private data.

If a consumer (such as a deferred probe workqueue) concurrently looks up the
PHY and immediately invokes an operation like phy_power_on(), could
qcom_dwmac_sgmii_phy_power_on() read a NULL driver data pointer and cause
a NULL pointer dereference when attempting to access data->vregs?

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd at oss.qualcomm.com?part=2


From sashiko-bot at kernel.org  Mon Aug  3 12:43:44 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 19:43:44 +0000
Subject: [PATCH v2 4/6] arm64: dts: qcom: lemans-ride-common: fix SerDes
 PHY regulator supplies
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-4-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
 <20260804-b4-sgmiieth_serdes_regulator-v2-4-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260803194345.579601F00A3A@smtp.kernel.org>

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

Pre-existing issues:
- [High] Incomplete refactoring: missing DT updates for new PHY regulator requirements on Monaco and QCS8300 boards
--

--- Patch [4]: [PATCH v2 4/6] arm64: dts: qcom: lemans-ride-common: fix SerDes PHY regulator supplies ---
commit defd43449ca452b489164f877878995171548517
Author: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>

arm64: dts: qcom: lemans-ride-common: fix SerDes PHY regulator supplies

This commit replaces the incorrect phy-supply property with the required
vdda-0p9-supply and vdda-1p2-supply properties for both SerDes PHY nodes in
the lemans-ride-common device tree.

> diff --git a/arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi b/arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi
> index cefb8ff008063..923fd8736f31d 100644
> --- a/arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi
> +++ b/arch/arm64/boot/dts/qcom/lemans-ride-common.dtsi
> @@ -856,12 +856,16 @@ &qup_uart17_rx {
>  };
>  
>  &serdes0 {
> -	phy-supply = <&vreg_l5a>;
> +	vdda-0p9-supply = <&vreg_l4a>;
> +	vdda-1p2-supply = <&vreg_l1c>;
> +

[Severity: High]
This is a pre-existing issue, but shouldn't other boards in the tree that
use the deprecated phy-supply for serdes0 also be updated?

For example, arch/arm64/boot/dts/qcom/monaco-evk.dts still uses:

&serdes0 {
	phy-supply = <&vreg_l4a>;

	status = "okay";
};

arch/arm64/boot/dts/qcom/monaco-monza-som.dtsi still uses:

&serdes0 {
	phy-supply = <&vreg_l5a>;

	status = "okay";
};

And arch/arm64/boot/dts/qcom/qcs8300-ride.dts still uses:

&serdes0 {
	phy-supply = <&vreg_l5a>;
	status = "okay";
};

Without these updates, will the driver fall back to dummy regulators and fail
to place the required voltage and load votes for these boards? This might
result in PHY calibration timing out or Ethernet failing when shared rails
enter runtime PM suspend.

>  	status = "okay";
>  };
>  
>  &serdes1 {
> -	phy-supply = <&vreg_l5a>;
> +	vdda-0p9-supply = <&vreg_l4a>;
> +	vdda-1p2-supply = <&vreg_l1c>;
> +
>  	status = "okay";
>  };

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd at oss.qualcomm.com?part=4


From sashiko-bot at kernel.org  Mon Aug  3 12:47:37 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Mon, 03 Aug 2026 19:47:37 +0000
Subject: [PATCH v2 5/6] arm64: dts: qcom: monaco-evk: fix SerDes PHY
 regulator supplies
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-5-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
 <20260804-b4-sgmiieth_serdes_regulator-v2-5-c4bc688177dd@oss.qualcomm.com>
Message-ID: <20260803194738.4E0D51F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Incomplete device tree migration causes a functional regression on qcs8300-ride and monaco-monza-som boards, where the SerDes PHY will fail to power up because its 0.9V regulator rail is not enabled.
--

--- Patch [5]: [PATCH v2 5/6] arm64: dts: qcom: monaco-evk: fix SerDes PHY regulator supplies ---
commit 77126601ae0cd79ff3e3276e8f35420b21a2d2f5
Author: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>

arm64: dts: qcom: monaco-evk: fix SerDes PHY regulator supplies

The commit updates the SerDes PHY node in the monaco-evk device tree to use
explicit named regulator supplies (vdda-0p9-supply and vdda-1p2-supply)
instead of the deprecated phy-supply property. This ensures both required
rails are properly described and enabled.

> diff --git a/arch/arm64/boot/dts/qcom/monaco-evk.dts b/arch/arm64/boot/dts/qcom/monaco-evk.dts
> index 9d17ef7d2caf1..f787e78b35ce4 100644
> --- a/arch/arm64/boot/dts/qcom/monaco-evk.dts
> +++ b/arch/arm64/boot/dts/qcom/monaco-evk.dts
> @@ -706,7 +706,8 @@ &sdhc_1 {
>  };
>  
>  &serdes0 {
> -	phy-supply = <&vreg_l4a>;
> +	vdda-0p9-supply = <&vreg_l4a>;
> +	vdda-1p2-supply = <&vreg_l5a>;

[Severity: High]
Does this migration to vdda-0p9-supply and vdda-1p2-supply need to be applied
to other boards as well?

Looking at arch/arm64/boot/dts/qcom/qcs8300-ride.dts and
arch/arm64/boot/dts/qcom/monaco-monza-som.dtsi, they still use the deprecated
phy-supply property for serdes0:

arch/arm64/boot/dts/qcom/qcs8300-ride.dts:serdes0 {
	phy-supply = <&vreg_l5a>;
	status = "okay";
};

If the qcom-dwmac-sgmii-phy driver was updated in this series to require
explicit vdda-0p9 and vdda-1p2 supplies, devm_regulator_bulk_get_const() will
fall back to dummy regulators for these missing named supplies.

This would leave the 0.9V physical rail unpowered on these other boards, which
causes the PHY calibration to time out and the Ethernet interface to fail to
initialize.

Should these device trees also be updated to ensure their SerDes PHYs do not
fail to power up?

>  
>  	status = "okay";
>  };

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd at oss.qualcomm.com?part=5


From jason98166 at gmail.com  Mon Aug  3 22:38:40 2026
From: jason98166 at gmail.com (=?UTF-8?B?5qWK5pm65oiQ?=)
Date: Tue, 4 Aug 2026 13:38:40 +0800
Subject: [PATCH v2 3/4] phy: rockchip-samsung-dcphy: model TX and RX as
 separate PHYs
In-Reply-To: <7e55b8bc-d11d-4b25-99ee-d5d181be158f@collabora.com>
References: <20260726-dcphy-rx-v1-v2-0-cf9cc34a412a@gmail.com>
 <20260726-dcphy-rx-v1-v2-3-cf9cc34a412a@gmail.com> <7e55b8bc-d11d-4b25-99ee-d5d181be158f@collabora.com>
Message-ID: <CAPwA33Jnp6N7a=2tcrgEzFxXRpbxEO25ktrRDiea--9Uy5upNQ@mail.gmail.com>

Hi Michael,

Thanks for the review.

> This may be bike-shedding, but I am not a big fan of the notion of
> "direction". This is a combo PHY that in essence contains different
> PHYs, so why not call this "samsung_mipi_phy" or something?
>
>         struct samsung_mipi_phy phys[2];
>
> which seems clean and simple to me.

That reads better - the per-PHY structure becomes samsung_mipi_phy.

Taking your other naming comment together with this one:

> Again, this may be bike-shedding, but "pd" seems misleading to me.
> Please reconsider the naming. Maybe "samsung_phy" for the individual
> PHY and "samsung" for the complete combo PHY?

Sure, "pd" becomes "samsung_phy".

> This approach with the "first" variable seems unintuitive. If you need
> reference counting on the BIAS block, then use a state variable
> "bias_powered", use a mutex that protects it, and call
> samsung_mipi_dcphy_bias_block_{en,dis}able (from
> samsung_mipi_dcphy_power{on, off}) unconditionally. Those methods shall
> acquire the mutex, check the state variable, {en,dis}able the BIAS
> block, set the state variable accordingly, release the mutex.
>
> [...]
>
> Not sure whether you actually need to track the power status of the
> individual PHYs (maybe you actually need to track the BIAS block status
> alone) but anyway I would appreciate if
> "samsung_mipi_dphy_tx_power_{on,off}" would set the "pd->powered"
> variable.

Both of these lead to the same change. You are right to be unsure
about the per-PHY flag - I checked, and its only readers are the two
"first" computations, which only decide about the shared block. So the
two flags become one count in the parent, taken and dropped inside
samsung_mipi_dphy_{tx,rx}_power_{on,off}, and the "first" computations
go away. I went for a count rather than a single flag because the
release side has to distinguish "the other direction is still using
it" from "nobody is left".

The shared-block helper will use lockdep_assert_held() rather than
take the mutex itself, since samsung_mipi_dcphy_power_{on,off} already
hold it, as you noted.

One thing I am unsure about is the enable/disable pairing. I could not
find an enable for this block - the four BIAS registers only select
bias currents, reference voltages, the HS output level and the BGR
chopper divider, while PLL_CON0 in the same common block does have a
PLL_EN. If that reading is right, there is nothing to turn off and the
release side would only drop the count - in which case maybe the
"enable" name is worth revisiting as well. Does that match your
understanding of this block?

Separately, I noticed "first" also guards the APB reset, and only in
the transmitter path - once 4/4 enables the receiver, a receiver-first
power-on does not get one. I plan to move it into the same shared-block
path so both directions behave the same, and check both power-on
orders on the board before sending v3.

I would like to fold all of this into v3 together with whatever comes
out of the PHY_TYPE_CSI/PHY_TYPE_DSI discussion in patch 1/4, since
that decides how the two PHYs are told apart.

Thanks,
Jason


Michael Riesch <michael.riesch at collabora.com> ? 2026?8?3??? ??8:05???
>
> Hi Jason,
>
> Thanks for the patch!
>
> On 7/26/26 16:55, Jason Yang via B4 Relay wrote:
> > From: Jason Yang <jason98166 at gmail.com>
> >
> > The DC-PHY drives a MIPI DSI transmitter and a MIPI CSI receiver, and on
> > RK3588 both can be wired to the same PHY as independent consumers. The
> > PHY core reference-counts power_on() per struct phy, so a single struct
> > phy cannot bring the two directions up independently.
> >
> > Register one struct phy per direction and move the per-direction state
> > (direction, PHY type, lane count and powered flag) into its own driver
> > data. of_xlate() accepts the legacy single cell as the transmitter and
> > an optional second cell that selects the direction, so existing
> > single-cell DSI device trees keep resolving to the transmitter.
> > configure() rejects the not yet supported receiver before touching any
> > state, and each direction keeps its own lane count.
> >
> > The PHY's common block contains a single BIAS block shared by both
> > directions (RK3588 TRM section 22.2), so it must be programmed only
> > once. Whichever direction powers on first enables it; each direction
> > records its powered state under the per-provider mutex so that powering
> > one direction on does not disturb an already running one. The APB reset
> > is issued on the transmitter bring-up path.
> > The receiver bring-up itself is added in a later change.
> >
> > Signed-off-by: Jason Yang <jason98166 at gmail.com>
> > Assisted-by: Claude:claude-opus-4-8
> > ---
> >  drivers/phy/rockchip/phy-rockchip-samsung-dcphy.c | 143 +++++++++++++++-------
> >  1 file changed, 102 insertions(+), 41 deletions(-)
> >
> > diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-dcphy.c b/drivers/phy/rockchip/phy-rockchip-samsung-dcphy.c
> > index d0d77421bd4b..95eb1200cab4 100644
> > --- a/drivers/phy/rockchip/phy-rockchip-samsung-dcphy.c
> > +++ b/drivers/phy/rockchip/phy-rockchip-samsung-dcphy.c
> > @@ -5,6 +5,7 @@
> >   *      Guochun Huang <hero.huang at rock-chips.com>
> >   */
> >
> > +#include <dt-bindings/phy/rockchip,rk3588-mipi-dcphy.h>
> >  #include <dt-bindings/phy/phy.h>
> >  #include <linux/bitfield.h>
> >  #include <linux/clk.h>
> > @@ -13,6 +14,7 @@
> >  #include <linux/kernel.h>
> >  #include <linux/mfd/syscon.h>
> >  #include <linux/module.h>
> > +#include <linux/mutex.h>
> >  #include <linux/of.h>
> >  #include <linux/phy/phy.h>
> >  #include <linux/platform_device.h>
> > @@ -280,6 +282,19 @@ struct samsung_mipi_dcphy_plat_data {
> >       u32 dphy_tx_max_lane_kbps;
> >  };
> >
> > +struct samsung_mipi_dcphy;
> > +
> > +/* One PHY per direction: transmitter (DSI) and receiver (CSI). */
> > +struct samsung_mipi_dcphy_dir {
>
> This may be bike-shedding, but I am not a big fan of the notion of
> "direction". This is a combo PHY that in essence contains different
> PHYs, so why not call this "samsung_mipi_phy" or something?
>
> > +     struct phy *phy;
> > +     struct samsung_mipi_dcphy *parent;
> > +     u8 dir;
> > +     u8 type;
> > +     unsigned int lanes;
> > +     /* Written under the parent's lock. */
> > +     bool powered;
> > +};
> > +
> >  struct samsung_mipi_dcphy {
> >       struct device *dev;
> >       struct clk *ref_clk;
> > @@ -290,9 +305,9 @@ struct samsung_mipi_dcphy {
> >       struct reset_control *s_phy_rst;
> >       struct reset_control *apb_rst;
> >       struct reset_control *grf_apb_rst;
> > -     unsigned int lanes;
> > -     struct phy *phy;
> > -     u8 type;
> > +     struct samsung_mipi_dcphy_dir phys[2];
>
> This would look like this then:
>
>         struct samsung_mipi_phy phys[2];
>
> which seems clean and simple to me.
>
> > +     /* Serialises the two directions' access to the shared PHY state. */
> > +     struct mutex lock;
> >
> >       const struct samsung_mipi_dcphy_plat_data *pdata;
> >       struct {
> > @@ -995,7 +1010,7 @@ static void samsung_mipi_dphy_lane_enable(struct samsung_mipi_dcphy *samsung)
> >       regmap_update_bits(samsung->regmap, DPHY_MC_GNR_CON0,
> >                          PHY_ENABLE, PHY_ENABLE);
> >
> > -     switch (samsung->lanes) {
> > +     switch (samsung->phys[RK_DCPHY_DIR_TX].lanes) {
> >       case 4:
> >               regmap_write(samsung->regmap, DPHY_MD3_GNR_CON1,
> >                            T_PHY_READY(0x2000));
> > @@ -1026,7 +1041,7 @@ static void samsung_mipi_dphy_lane_enable(struct samsung_mipi_dcphy *samsung)
> >
> >  static void samsung_mipi_dphy_lane_disable(struct samsung_mipi_dcphy *samsung)
> >  {
> > -     switch (samsung->lanes) {
> > +     switch (samsung->phys[RK_DCPHY_DIR_TX].lanes) {
> >       case 4:
> >               regmap_update_bits(samsung->regmap, DPHY_MD3_GNR_CON0,
> >                                  PHY_ENABLE, 0);
> > @@ -1334,11 +1349,23 @@ samsung_mipi_dphy_data_lane_timing_init(struct samsung_mipi_dcphy *samsung)
> >
> >  static int samsung_mipi_dphy_tx_power_on(struct samsung_mipi_dcphy *samsung)
> >  {
> > +     bool first = !samsung->phys[RK_DCPHY_DIR_RX].powered;
>
> Note to myself: the lock is acquired in the calling method, all is well.
>
> >       int ret;
> >
> > +     /*
> > +      * The shared BIAS block is brought up by whichever direction powers
> > +      * on first, leaving an already active peer undisturbed.
> > +      */
> > +     if (first) {
> > +             reset_control_assert(samsung->apb_rst);
> > +             udelay(1);
> > +             reset_control_deassert(samsung->apb_rst);
> > +     }
> > +
> >       reset_control_assert(samsung->m_phy_rst);
> >
> > -     samsung_mipi_dcphy_bias_block_enable(samsung);
> > +     if (first)
> > +             samsung_mipi_dcphy_bias_block_enable(samsung);
>
> This approach with the "first" variable seems unintuitive. If you need
> reference counting on the BIAS block, then use a state variable
> "bias_powered", use a mutex that protects it, and call
> samsung_mipi_dcphy_bias_block_{en,dis}able (from
> samsung_mipi_dcphy_power{on, off}) unconditionally. Those methods shall
> acquire the mutex, check the state variable, {en,dis}able the BIAS
> block, set the state variable accordingly, release the mutex.
>
>
> >       samsung_mipi_dcphy_pll_configure(samsung);
> >       samsung_mipi_dphy_clk_lane_timing_init(samsung);
> >       samsung_mipi_dphy_data_lane_timing_init(samsung);
> > @@ -1368,34 +1395,44 @@ static int samsung_mipi_dphy_tx_power_off(struct samsung_mipi_dcphy *samsung)
> >
> >  static int samsung_mipi_dcphy_power_on(struct phy *phy)
> >  {
> > -     struct samsung_mipi_dcphy *samsung = phy_get_drvdata(phy);
> > -
> > -     reset_control_assert(samsung->apb_rst);
> > -     udelay(1);
> > -     reset_control_deassert(samsung->apb_rst);
> > +     struct samsung_mipi_dcphy_dir *pd = phy_get_drvdata(phy);
>
> Again, this may be bike-shedding, but "pd" seems misleading to me.
> Please reconsider the naming. Maybe "samsung_phy" for the individual PHY
> and "samsung" for the complete combo PHY? Or "phy_data" and
> "dcphy_data"? ...?
>
> > +     struct samsung_mipi_dcphy *samsung = pd->parent;
> > +     int ret;
> >
> > -     switch (samsung->type) {
> > -     case PHY_TYPE_DPHY:
> > -             return samsung_mipi_dphy_tx_power_on(samsung);
> > -     default:
> > -             /* CPHY part to be implemented later */
> > +     if (pd->type != PHY_TYPE_DPHY)
> >               return -EOPNOTSUPP;
> > -     }
> >
> > -     return 0;
> > +     mutex_lock(&samsung->lock);
> > +     if (pd->dir == RK_DCPHY_DIR_RX)
> > +             ret = -EOPNOTSUPP;
> > +     else
> > +             ret = samsung_mipi_dphy_tx_power_on(samsung);
> > +     if (!ret)
> > +             pd->powered = true;
>
> Not sure whether you actually need to track the power status of the
> individual PHYs (maybe you actually need to track the BIAS block status
> alone) but anyway I would appreciate if
> "samsung_mipi_dphy_tx_power_{on,off}" would set the "pd->powered" variable.
>
> Best regards,
> Michael
>
>
> > +     mutex_unlock(&samsung->lock);
> > +
> > +     return ret;
> >  }
> >
> >  static int samsung_mipi_dcphy_power_off(struct phy *phy)
> >  {
> > -     struct samsung_mipi_dcphy *samsung = phy_get_drvdata(phy);
> > +     struct samsung_mipi_dcphy_dir *pd = phy_get_drvdata(phy);
> > +     struct samsung_mipi_dcphy *samsung = pd->parent;
> > +     int ret;
> >
> > -     switch (samsung->type) {
> > -     case PHY_TYPE_DPHY:
> > -             return samsung_mipi_dphy_tx_power_off(samsung);
> > -     default:
> > -             /* CPHY part to be implemented later */
> > +     if (pd->type != PHY_TYPE_DPHY)
> >               return -EOPNOTSUPP;
> > -     }
> > +
> > +     if (pd->dir == RK_DCPHY_DIR_RX)
> > +             return -EOPNOTSUPP;
> > +
> > +     mutex_lock(&samsung->lock);
> > +     ret = samsung_mipi_dphy_tx_power_off(samsung);
> > +     if (!ret)
> > +             pd->powered = false;
> > +     mutex_unlock(&samsung->lock);
> > +
> > +     return ret;
> >  }
> >
> >  static int
> > @@ -1488,10 +1525,14 @@ samsung_mipi_dcphy_pll_calc_rate(struct samsung_mipi_dcphy *samsung,
> >  static int samsung_mipi_dcphy_configure(struct phy *phy,
> >                                       union phy_configure_opts *opts)
> >  {
> > -     struct samsung_mipi_dcphy *samsung = phy_get_drvdata(phy);
> > +     struct samsung_mipi_dcphy_dir *pd = phy_get_drvdata(phy);
> > +     struct samsung_mipi_dcphy *samsung = pd->parent;
> >       unsigned long long target_rate = opts->mipi_dphy.hs_clk_rate;
> >
> > -     samsung->lanes = opts->mipi_dphy.lanes > 4 ? 4 : opts->mipi_dphy.lanes;
> > +     if (pd->dir == RK_DCPHY_DIR_RX)
> > +             return -EOPNOTSUPP;
> > +
> > +     pd->lanes = opts->mipi_dphy.lanes > 4 ? 4 : opts->mipi_dphy.lanes;
> >
> >       samsung_mipi_dcphy_pll_calc_rate(samsung, target_rate);
> >       opts->mipi_dphy.hs_clk_rate = samsung->pll.rate;
> > @@ -1501,16 +1542,16 @@ static int samsung_mipi_dcphy_configure(struct phy *phy,
> >
> >  static int samsung_mipi_dcphy_init(struct phy *phy)
> >  {
> > -     struct samsung_mipi_dcphy *samsung = phy_get_drvdata(phy);
> > +     struct samsung_mipi_dcphy_dir *pd = phy_get_drvdata(phy);
> >
> > -     return pm_runtime_resume_and_get(samsung->dev);
> > +     return pm_runtime_resume_and_get(pd->parent->dev);
> >  }
> >
> >  static int samsung_mipi_dcphy_exit(struct phy *phy)
> >  {
> > -     struct samsung_mipi_dcphy *samsung = phy_get_drvdata(phy);
> > +     struct samsung_mipi_dcphy_dir *pd = phy_get_drvdata(phy);
> >
> > -     pm_runtime_put(samsung->dev);
> > +     pm_runtime_put(pd->parent->dev);
> >
> >       return 0;
> >  }
> > @@ -1536,19 +1577,29 @@ static struct phy *samsung_mipi_dcphy_xlate(struct device *dev,
> >                                           const struct of_phandle_args *args)
> >  {
> >       struct samsung_mipi_dcphy *samsung = dev_get_drvdata(dev);
> > +     struct samsung_mipi_dcphy_dir *pd;
> > +     u8 dir = RK_DCPHY_DIR_TX;
> >
> > -     if (args->args_count != 1) {
> > +     if (args->args_count < 1 || args->args_count > 2) {
> >               dev_err(dev, "invalid number of arguments\n");
> >               return ERR_PTR(-EINVAL);
> >       }
> >
> > -     if (samsung->type != PHY_NONE && samsung->type != args->args[0])
> > -             dev_warn(dev, "phy type select %d overwriting type %d\n",
> > -                      args->args[0], samsung->type);
> > +     if (args->args_count == 2) {
> > +             if (args->args[1] > RK_DCPHY_DIR_RX) {
> > +                     dev_err(dev, "invalid direction %u\n", args->args[1]);
> > +                     return ERR_PTR(-EINVAL);
> > +             }
> > +             dir = args->args[1];
> > +     }
> >
> > -     samsung->type = args->args[0];
> > +     pd = &samsung->phys[dir];
> > +     if (pd->type != PHY_NONE && pd->type != args->args[0])
> > +             dev_warn(dev, "phy type select %u overwriting type %u\n",
> > +                      args->args[0], pd->type);
> > +     pd->type = args->args[0];
> >
> > -     return samsung->phy;
> > +     return pd->phy;
> >  }
> >
> >  static int samsung_mipi_dcphy_probe(struct platform_device *pdev)
> > @@ -1559,6 +1610,7 @@ static int samsung_mipi_dcphy_probe(struct platform_device *pdev)
> >       struct phy_provider *phy_provider;
> >       struct resource *res;
> >       void __iomem *regs;
> > +     unsigned int i;
> >       int ret;
> >
> >       samsung = devm_kzalloc(dev, sizeof(*samsung), GFP_KERNEL);
> > @@ -1568,6 +1620,7 @@ static int samsung_mipi_dcphy_probe(struct platform_device *pdev)
> >       samsung->dev = dev;
> >       samsung->pdata = device_get_match_data(dev);
> >       platform_set_drvdata(pdev, samsung);
> > +     mutex_init(&samsung->lock);
> >
> >       res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
> >       regs = devm_ioremap_resource(dev, res);
> > @@ -1613,11 +1666,19 @@ static int samsung_mipi_dcphy_probe(struct platform_device *pdev)
> >               return dev_err_probe(dev, PTR_ERR(samsung->grf_apb_rst),
> >                                    "Failed to get system grf_apb_rst control\n");
> >
> > -     samsung->phy = devm_phy_create(dev, NULL, &samsung_mipi_dcphy_ops);
> > -     if (IS_ERR(samsung->phy))
> > -             return dev_err_probe(dev, PTR_ERR(samsung->phy), "Failed to create MIPI DC-PHY\n");
> > +     for (i = 0; i < ARRAY_SIZE(samsung->phys); i++) {
> > +             struct phy *phy = devm_phy_create(dev, NULL,
> > +                                               &samsung_mipi_dcphy_ops);
> >
> > -     phy_set_drvdata(samsung->phy, samsung);
> > +             if (IS_ERR(phy))
> > +                     return dev_err_probe(dev, PTR_ERR(phy),
> > +                                          "Failed to create MIPI DC-PHY\n");
> > +
> > +             samsung->phys[i].phy = phy;
> > +             samsung->phys[i].parent = samsung;
> > +             samsung->phys[i].dir = i;
> > +             phy_set_drvdata(phy, &samsung->phys[i]);
> > +     }
> >
> >       ret = devm_pm_runtime_enable(dev);
> >       if (ret)
> >
>


From krzk at kernel.org  Mon Aug  3 23:19:19 2026
From: krzk at kernel.org (Krzysztof Kozlowski)
Date: Tue, 4 Aug 2026 08:19:19 +0200
Subject: [PATCH v2 1/6] dt-bindings: phy: qcom,sa8775p-dwmac-sgmii-phy:
 add named voltage rails and deprecate phy-supply
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-1-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
 <20260804-b4-sgmiieth_serdes_regulator-v2-1-c4bc688177dd@oss.qualcomm.com>
Message-ID: <ec53e1de-06c8-4156-9e2b-0466446a253f@kernel.org>

On 03/08/2026 21:27, Mohd Ayaan Anwar wrote:
> The Qualcomm SGMII SerDes PHY has two distinct voltage supply rails,
> vdda-0p9 and vdda-1p2. The binding incorrectly described only a single
> generic supply via 'phy-supply'.
> 
> Deprecate 'phy-supply' and instead add two named supply properties. The
> 0.9V rail draws a peak current of 46 mA and the 1.2V rail draws 15 mA.
> 
> Fixes: 97b795125704 ("dt-bindings: phy: describe the Qualcomm SGMII PHY")
> Signed-off-by: Mohd Ayaan Anwar <mohd.anwar at oss.qualcomm.com>
> ---

Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski at oss.qualcomm.com>

Best regards,
Krzysztof


From krzk at kernel.org  Mon Aug  3 23:37:52 2026
From: krzk at kernel.org (Krzysztof Kozlowski)
Date: Tue, 4 Aug 2026 08:37:52 +0200
Subject: [PATCH v9 01/12] dt-bindings: clk: zte: Add zx297520v3 top clock
 and reset controller
In-Reply-To: <20260802-zx29clk-v9-1-d05530d85d28@gmail.com>
References: <20260802-zx29clk-v9-0-d05530d85d28@gmail.com>
 <20260802-zx29clk-v9-1-d05530d85d28@gmail.com>
Message-ID: <20260804-tough-reindeer-of-zoology-6ccd59@quoll>

On Sun, Aug 02, 2026 at 11:33:33PM +0300, Stefan D?singer wrote:
> These SoCs have 3 clock and reset controllers: Top, Matrix and LSP.
> Clocks go from oscillator -> top -> matrix -> LSP, with a register in
> top controlling most clocks that get passed to matrix and a register in
> matrix controlling all the clocks that get passed to LSP.
> 
> Generally every device has two clocks (one work clock, and one that
> connects it to the bus, I call it PCLK), two reset bits (I don't know
> what the difference is - sometimes asserting one is enough to reset the
> device, sometimes both need to be asserted). PCLK and WCLK are
> controlled by individual gates. Some devices have a mux and/or a
> divider for their work clock. Some devices, like the GPIO controller,
> only have reset bits and no clocks.
> 
> The top clock controller is fed by a 26 MHz external oscillator and has 4
> PLLs to generate other clock rates. ZTE's kernel mostly relies on the
> boot ROM to set up PLLs, but one LTE-related PLL is not configured
> on some boards. Therefore my driver contains code to program PLLs. It
> produces identical settings as the boot ROM for the pre-programmed
> frequencies.
> 
> Not all clocks will have an explicit user in the end. I am defining a
> lot of them simply to shut them off. The boot loader sets up a few of
> the proprietary timers, which will send regular IRQs (although the
> kernel of course doesn't need to listen to them). I don't plan to add a
> driver for the proprietary timer as I see no use for them - the ARM arch
> timer works just fine. I will add a driver for the very similar
> proprietary watchdog though.
> 
> The clock list in this patch is pretty complete but not exhaustive.
> There are other bits that are enabled, but I couldn't deduce what they
> are controlling by trial and error. Some of them seem to do nothing.
> Others cause an instant hang of the board when disabled. It is quite
> likely that a handful more clocks will be added in the future, but not a
> large number.
> 
> Signed-off-by: Stefan D?singer <stefandoesinger at gmail.com>
> 
> ---
> 
> Changes v8->v9:

Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski at oss.qualcomm.com>

Best regards,
Krzysztof



From peter.wang at mediatek.com  Mon Aug  3 23:52:40 2026
From: peter.wang at mediatek.com (=?utf-8?B?UGV0ZXIgV2FuZyAo546L5L+h5Y+LKQ==?=)
Date: Tue, 4 Aug 2026 06:52:40 +0000
Subject: [PATCH v10 23/26] scsi: ufs: mediatek: Add MT8196 compatible,
 update copyright
In-Reply-To: <20260730-mt8196-ufs-v10-23-bc5ecef202dc@collabora.com>
References: <20260730-mt8196-ufs-v10-0-bc5ecef202dc@collabora.com>
	 <20260730-mt8196-ufs-v10-23-bc5ecef202dc@collabora.com>
Message-ID: <5bd5a02970531b954d2b51ff85e2e685b3a93eb0.camel@mediatek.com>

On Thu, 2026-07-30 at 11:49 +0200, Louis-Alexis Eyraud wrote:
> From: Nicolas Frattaroli <nicolas.frattaroli at collabora.com>
> 
> THe MT8196's UFS controller has a new compatible. Add the necessary
> struct definitions to support it.
> 
> Also update the copyrights and authors, without tabs following spaces
> to
> avoid checkpatch errors, to list myself as having contributed to this
> driver after the preceding rework patches.
> 
> Reviewed-by: AngeloGioacchino Del Regno
> <angelogioacchino.delregno at collabora.com>
> Reviewed-by: Peter Wang <peter.wang at mediatek.com>
> Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli at collabora.com>
> Signed-off-by: Louis-Alexis Eyraud <louisalexis.eyraud at collabora.com>
> ---
> ?drivers/ufs/host/ufs-mediatek.c | 17 +++++++++++++++--
> ?1 file changed, 15 insertions(+), 2 deletions(-)
> 
> diff --git a/drivers/ufs/host/ufs-mediatek.c b/drivers/ufs/host/ufs-
> mediatek.c
> index 5dd72b826181..decf2f5814b4 100644
> --- a/drivers/ufs/host/ufs-mediatek.c
> +++ b/drivers/ufs/host/ufs-mediatek.c
> @@ -1,9 +1,11 @@
> ?// SPDX-License-Identifier: GPL-2.0
> ?/*
> ? * Copyright (C) 2019 MediaTek Inc.
> + * Copyright (C) 2025 Collabora Ltd.
> 

2026


> ? * Authors:
> - *	Stanley Chu <stanley.chu at mediatek.com>
> - *	Peter Wang <peter.wang at mediatek.com>
> + *????? Stanley Chu <stanley.chu at mediatek.com>
> + *????? Peter Wang <peter.wang at mediatek.com>
> + *????? Nicolas Frattaroli <nicolas.frattaroli at collabora.com> (Major
> cleanups)
> ? */
> 
The name and email should be updated to yours?

Thanks
Peter


From kernel at airkyi.com  Tue Aug  4 00:07:25 2026
From: kernel at airkyi.com (Chaoyi Chen)
Date: Tue,  4 Aug 2026 15:07:25 +0800
Subject: [PATCH v6 2/7] drm/bridge: Implement generic USB Type-C DP HPD bridge
In-Reply-To: <20260804070730.68-1-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
Message-ID: <20260804070730.68-3-kernel@airkyi.com>

From: Chaoyi Chen <chaoyi.chen at rock-chips.com>

The HPD function of Type-C DP is implemented through
drm_connector_oob_hotplug_event(). For embedded DP, it is required
that the DRM connector fwnode corresponds to the Type-C port fwnode.

To describe the relationship between the DP controller and the Type-C
port device, we usually using drm_bridge to build a bridge chain.

Now several USB-C controller drivers have already implemented the DP
HPD bridge function provided by aux-hpd-bridge.c, it will build a DP
HPD bridge on USB-C connector port device.

But this requires the USB-C controller driver to manually register the
HPD bridge. If the driver does not implement this feature, the bridge
will not be create.

So this patch implements a generic DP HPD bridge based on
aux-hpd-bridge.c. It will monitor Type-C bus events, and when a
Type-C port device containing the DP svid is registered, it will
create an HPD bridge for it without the need for the USB-C controller
driver to implement it.

Signed-off-by: Chaoyi Chen <chaoyi.chen at rock-chips.com>
Reviewed-by: Heikki Krogerus <heikki.krogerus at linux.intel.com>
Reviewed-by: Nicolas Frattaroli <nicolas.frattaroli at collabora.com>
---
Changes in v6:
- Avoid duplicate registration of the same DisplayPort HPD bridge. 

(no changes since v5)

Changes in v4:
- Scan the entire typec_bus and attempt to register the hpd bridge,
  so as not to miss devices that were already added during initialization.

(no changes since v3)

Changes in v2:
- Add copyright text.
- Remove useless goto.
---
 drivers/gpu/drm/bridge/Kconfig                | 10 +++
 drivers/gpu/drm/bridge/Makefile               |  1 +
 .../gpu/drm/bridge/aux-hpd-typec-dp-bridge.c  | 67 +++++++++++++++++++
 3 files changed, 78 insertions(+)
 create mode 100644 drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c

diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig
index 4a57d49b4c6d..9739b2a19758 100644
--- a/drivers/gpu/drm/bridge/Kconfig
+++ b/drivers/gpu/drm/bridge/Kconfig
@@ -30,6 +30,16 @@ config DRM_AUX_HPD_BRIDGE
 	  Simple bridge that terminates the bridge chain and provides HPD
 	  support.
 
+if DRM_AUX_HPD_BRIDGE
+config DRM_AUX_HPD_TYPEC_BRIDGE
+	tristate
+	depends on TYPEC || !TYPEC
+	default TYPEC
+	help
+	  Simple bridge that terminates the bridge chain and provides HPD
+	  support. It build bridge on each USB-C connector device node.
+endif
+
 menu "Display Interface Bridges"
 	depends on DRM && DRM_BRIDGE
 
diff --git a/drivers/gpu/drm/bridge/Makefile b/drivers/gpu/drm/bridge/Makefile
index 15cc821d85b7..d88a9e1ccc9a 100644
--- a/drivers/gpu/drm/bridge/Makefile
+++ b/drivers/gpu/drm/bridge/Makefile
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
 obj-$(CONFIG_DRM_AUX_BRIDGE) += aux-bridge.o
 obj-$(CONFIG_DRM_AUX_HPD_BRIDGE) += aux-hpd-bridge.o
+obj-$(CONFIG_DRM_AUX_HPD_TYPEC_BRIDGE) += aux-hpd-typec-dp-bridge.o
 obj-$(CONFIG_DRM_CHIPONE_ICN6211) += chipone-icn6211.o
 obj-$(CONFIG_DRM_CHRONTEL_CH7033) += chrontel-ch7033.o
 obj-$(CONFIG_DRM_CROS_EC_ANX7688) += cros-ec-anx7688.o
diff --git a/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c b/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c
new file mode 100644
index 000000000000..682bc192e093
--- /dev/null
+++ b/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2026 Rockchip Electronics Co., Ltd.
+ *
+ * Author: Chaoyi Chen <chaoyi.chen at rock-chips.com>
+ */
+#include <linux/of.h>
+#include <linux/usb/typec_altmode.h>
+#include <linux/usb/typec_dp.h>
+
+#include <drm/bridge/aux-bridge.h>
+
+static int drm_typec_bus_event(struct notifier_block *nb, unsigned long action,
+			       void *data)
+{
+	struct device *dev = (struct device *)data;
+	struct typec_altmode *alt = to_typec_altmode(dev);
+	struct device_node *np;
+
+	if (action != BUS_NOTIFY_ADD_DEVICE)
+		return NOTIFY_OK;
+
+	/*
+	 * alt->dev.parent->parent : USB-C controller device
+	 * alt->dev.parent         : USB-C connector device
+	 */
+	if (is_typec_port_altmode(&alt->dev) && alt->svid == USB_TYPEC_DP_SID) {
+		np = to_of_node(alt->dev.parent->fwnode);
+		if (!drm_dev_has_dp_hpd_bridge(alt->dev.parent->parent, np))
+			drm_dp_hpd_bridge_register(alt->dev.parent->parent, np);
+	}
+
+	return NOTIFY_OK;
+}
+
+static struct notifier_block drm_typec_event_nb = {
+	.notifier_call = drm_typec_bus_event,
+};
+
+static int check_device_already_added(struct device *dev, void *data)
+{
+	drm_typec_bus_event(NULL, BUS_NOTIFY_ADD_DEVICE, dev);
+	return 0;
+}
+
+static void drm_aux_hpd_typec_dp_bridge_module_exit(void)
+{
+	bus_unregister_notifier(&typec_bus, &drm_typec_event_nb);
+}
+
+static int __init drm_aux_hpd_typec_dp_bridge_module_init(void)
+{
+	bus_register_notifier(&typec_bus, &drm_typec_event_nb);
+	/*
+	 * Before module initialization, some devices may have already been added.
+	 * Register the HPD bridge for these devices.
+	 */
+	bus_for_each_dev(&typec_bus, NULL, NULL, check_device_already_added);
+	return 0;
+}
+
+module_init(drm_aux_hpd_typec_dp_bridge_module_init);
+module_exit(drm_aux_hpd_typec_dp_bridge_module_exit);
+
+MODULE_AUTHOR("Chaoyi Chen <chaoyi.chen at rock-chips.com>");
+MODULE_DESCRIPTION("DRM TYPEC DP HPD BRIDGE");
+MODULE_LICENSE("GPL");
-- 
2.53.0



From kernel at airkyi.com  Tue Aug  4 00:07:23 2026
From: kernel at airkyi.com (Chaoyi Chen)
Date: Tue,  4 Aug 2026 15:07:23 +0800
Subject: [PATCH v6 0/7] drm/bridge: Implement generic USB Type-C DP HPD bridge
Message-ID: <20260804070730.68-1-kernel@airkyi.com>

From: Chaoyi Chen <chaoyi.chen at rock-chips.com>

This series is split from the v15 "Add Type-C DP support for RK3399 EVB
IND board" series [1]. It focuses on the DRM bridge and Rockchip
platform CDN-DP controller changes.

[1] https://lore.kernel.org/all/20260304094152.92-1-kernel at airkyi.com/

====
1. Generic Type-C DP HPD bridge

Currently, several USB-C controller drivers register their own DP HPD
bridge via aux-hpd-bridge.c, each duplicating the same logic. For
devicetree based platforms, the USB-C controller may vary across boards,
and not every USB-C controller driver implements this feature. Patch 1
implements a generic DP HPD bridge that monitors Type-C bus events and
automatically creates an HPD bridge when a Type-C port device with DP
SVID is registered.

2. Multiple bridge model for CDN-DP

The RK3399 has two USB/DP combo PHY and one CDN-DP controller. Patch 5
introduces a multi-bridge model where each PHY port gets a separate
encoder and bridge, allowing flexible selection of the output PHY port.
This is based on the DRM AUX HPD bridge rather than extcon.

====
Patch 1 adds new API to check whether a DisplayPort HPD bridge has
	already been registered(Sebastian).
Patch 2 adds generic USB Type-C DP HPD bridge (Dmitry, Heikki).
Patch 3 adds module soft depend for USB Type-C DP HPD bridge(Sebastian).
Patch 4 adds new API drm_aux_bridge_register_from_node() (Neil).
Patch 5 adds DRM AUX bridge support for RK3399 USBDP PHY (Neil).
Patch 6 drops CDN-DP's extcon dependency when Type-C is present (Dmitry).
Patch 7 adds multiple bridges to support PHY port selection (Dmitry, Luca).

Changes in v6:
- Link to v5: https://lore.kernel.org/all/20260730013349.51-1-kernel at airkyi.com/
- Avoid duplicate registration of the same DisplayPort HPD bridge.

Changes in v5:
- Link to v4: https://lore.kernel.org/all/20260727064408.101-1-kernel at airkyi.com/
- Add module soft depend for USB Type-C DP HPD bridge.

Changes in v4:
- Link to v3: https://lore.kernel.org/all/20260717072323.96-1-kernel at airkyi.com/
- Scan the entire typec_bus and attempt to register the hpd bridge,
  so as not to miss devices that were already added during initialization.

Changes in v3:
- Link to v2: https://lore.kernel.org/all/20260608070805.88-1-kernel at airkyi.com/
  (Sorry, I forgot to add the "v2" title, so it looks like v1.)
- Remove now-redundant call to drm_connector_attach_encoder()
- Rebase to v7.2-rc2

Changes in v2:
- Link to v1: https://lore.kernel.org/all/20260521032854.103-1-kernel at airkyi.com/
- Add copyright text.
- Remove useless goto.
- Fix incorrect function names in the documentation comments.
- Fix the check logic for dp->active_port (0 is valid).
- Fix the uninitialized prev_port.
- Remove duplicate logic from cdn_dp_switch_port().
- Fix the prototype of cdn_dp_bridge_hpd_notify().
- Properly release connector->fwnode.
- Properly release next_bridge.

Chaoyi Chen (7):
  drm/bridge: aux-hpd-bridge: Add drm_dev_has_dp_hpd_bridge()
  drm/bridge: Implement generic USB Type-C DP HPD bridge
  drm/display: Add soft depend for aux-hpd-typec-dp-bridge module
  drm/bridge: aux: Add drm_aux_bridge_register_from_node()
  phy: rockchip: phy-rockchip-typec: Add DRM AUX bridge
  drm/rockchip: cdn-dp: Support handle lane info without extcon
  drm/rockchip: cdn-dp: Add multiple bridges to support PHY port
    selection

 drivers/gpu/drm/bridge/Kconfig                |  10 +
 drivers/gpu/drm/bridge/Makefile               |   1 +
 drivers/gpu/drm/bridge/aux-bridge.c           |  26 +-
 drivers/gpu/drm/bridge/aux-hpd-bridge.c       |  42 +-
 .../gpu/drm/bridge/aux-hpd-typec-dp-bridge.c  |  67 ++++
 .../gpu/drm/display/drm_display_helper_mod.c  |   1 +
 drivers/gpu/drm/rockchip/Kconfig              |   1 +
 drivers/gpu/drm/rockchip/cdn-dp-core.c        | 358 ++++++++++++++----
 drivers/gpu/drm/rockchip/cdn-dp-core.h        |  18 +-
 drivers/phy/rockchip/Kconfig                  |   2 +
 drivers/phy/rockchip/phy-rockchip-typec.c     |  13 +-
 include/drm/bridge/aux-bridge.h               |  12 +
 12 files changed, 479 insertions(+), 72 deletions(-)
 create mode 100644 drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c

--
2.53.0



From kernel at airkyi.com  Tue Aug  4 00:07:24 2026
From: kernel at airkyi.com (Chaoyi Chen)
Date: Tue,  4 Aug 2026 15:07:24 +0800
Subject: [PATCH v6 1/7] drm/bridge: aux-hpd-bridge: Add drm_dev_has_dp_hpd_bridge()
In-Reply-To: <20260804070730.68-1-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
Message-ID: <20260804070730.68-2-kernel@airkyi.com>

From: Chaoyi Chen <chaoyi.chen at rock-chips.com>

Add a new API to check whether a DisplayPort HPD bridge has already
been registered. This helps avoid duplicate registration of the same
HPD bridge, although the current framework allows doing so.

Suggested-by: Sebastian Reichel <sebastian.reichel at collabora.com>
Signed-off-by: Chaoyi Chen <chaoyi.chen at rock-chips.com>
---
 drivers/gpu/drm/bridge/aux-hpd-bridge.c | 42 ++++++++++++++++++++++++-
 include/drm/bridge/aux-bridge.h         |  6 ++++
 2 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/drivers/gpu/drm/bridge/aux-hpd-bridge.c b/drivers/gpu/drm/bridge/aux-hpd-bridge.c
index f02a38a2638a..a56c88eba005 100644
--- a/drivers/gpu/drm/bridge/aux-hpd-bridge.c
+++ b/drivers/gpu/drm/bridge/aux-hpd-bridge.c
@@ -12,6 +12,8 @@
 #include <drm/drm_bridge.h>
 #include <drm/bridge/aux-bridge.h>
 
+#define DRM_AUX_HPD_BRIDGE_NAME	"dp_hpd_bridge"
+
 static DEFINE_IDA(drm_aux_hpd_bridge_ida);
 
 struct drm_aux_hpd_bridge_data {
@@ -36,6 +38,44 @@ static void drm_aux_hpd_bridge_free_adev(void *_adev)
 	auxiliary_device_uninit(_adev);
 }
 
+static int hpd_bridge_match(struct device *dev, const void *data)
+{
+	const struct device_node *np = data;
+	struct auxiliary_device *adev;
+
+	if (!dev_is_auxiliary(dev))
+		return 0;
+
+	adev = to_auxiliary_dev(dev);
+	if (strcmp(adev->name, DRM_AUX_HPD_BRIDGE_NAME))
+		return 0;
+
+	return adev->dev.platform_data == np;
+}
+
+/**
+ * drm_dev_has_dp_hpd_bridge - check whether a HPD DisplayPort bridge is registered
+ * @parent: device instance providing this bridge
+ * @np: device node pointer corresponding to this bridge instance
+ *
+ * Walk the children of @parent and check whether a HPD DisplayPort bridge for
+ * the given @np has already been registered via devm_drm_dp_hpd_bridge_add().
+ *
+ * Return: true if a HPD bridge for @parent / @np already exists, false otherwise
+ */
+bool drm_dev_has_dp_hpd_bridge(struct device *parent, struct device_node *np)
+{
+	struct device *child;
+
+	child = device_find_child(parent, np, hpd_bridge_match);
+	if (child) {
+		put_device(child);
+		return true;
+	}
+	return false;
+}
+EXPORT_SYMBOL_GPL(drm_dev_has_dp_hpd_bridge);
+
 /**
  * devm_drm_dp_hpd_bridge_alloc - allocate a HPD DisplayPort bridge
  * @parent: device instance providing this bridge
@@ -63,7 +103,7 @@ struct auxiliary_device *devm_drm_dp_hpd_bridge_alloc(struct device *parent, str
 	}
 
 	adev->id = ret;
-	adev->name = "dp_hpd_bridge";
+	adev->name = DRM_AUX_HPD_BRIDGE_NAME;
 	adev->dev.parent = parent;
 	adev->dev.release = drm_aux_hpd_bridge_release;
 	adev->dev.platform_data = of_node_get(np);
diff --git a/include/drm/bridge/aux-bridge.h b/include/drm/bridge/aux-bridge.h
index c2f5a855512f..cca07a8e2d45 100644
--- a/include/drm/bridge/aux-bridge.h
+++ b/include/drm/bridge/aux-bridge.h
@@ -25,6 +25,7 @@ struct auxiliary_device *devm_drm_dp_hpd_bridge_alloc(struct device *parent, str
 int devm_drm_dp_hpd_bridge_add(struct device *dev, struct auxiliary_device *adev);
 struct device *drm_dp_hpd_bridge_register(struct device *parent,
 					  struct device_node *np);
+bool drm_dev_has_dp_hpd_bridge(struct device *parent, struct device_node *np);
 void drm_aux_hpd_bridge_notify(struct device *dev, enum drm_connector_status status);
 #else
 static inline struct auxiliary_device *devm_drm_dp_hpd_bridge_alloc(struct device *parent,
@@ -44,6 +45,11 @@ static inline struct device *drm_dp_hpd_bridge_register(struct device *parent,
 	return NULL;
 }
 
+static inline bool drm_dev_has_dp_hpd_bridge(struct device *parent, struct device_node *np)
+{
+	return false;
+}
+
 static inline void drm_aux_hpd_bridge_notify(struct device *dev, enum drm_connector_status status)
 {
 }
-- 
2.53.0



From kernel at airkyi.com  Tue Aug  4 00:07:29 2026
From: kernel at airkyi.com (Chaoyi Chen)
Date: Tue,  4 Aug 2026 15:07:29 +0800
Subject: [PATCH v6 6/7] drm/rockchip: cdn-dp: Support handle lane info without extcon
In-Reply-To: <20260804070730.68-1-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
Message-ID: <20260804070730.68-7-kernel@airkyi.com>

From: Chaoyi Chen <chaoyi.chen at rock-chips.com>

This patch add support for get PHY lane info without help of extcon.

There is no extcon needed if the Type-C controller is present. In this
case, the lane info can be get from PHY instead of extcon.

The extcon device should still be supported if Type-C controller is
not present.

Signed-off-by: Chaoyi Chen <chaoyi.chen at rock-chips.com>
Reviewed-by: Heiko Stuebner <heiko at sntech.de>
---

(no changes since v1)

---
 drivers/gpu/drm/rockchip/cdn-dp-core.c | 25 +++++++++++++++++--------
 1 file changed, 17 insertions(+), 8 deletions(-)

diff --git a/drivers/gpu/drm/rockchip/cdn-dp-core.c b/drivers/gpu/drm/rockchip/cdn-dp-core.c
index 190cce9530c8..74e1f5422a6e 100644
--- a/drivers/gpu/drm/rockchip/cdn-dp-core.c
+++ b/drivers/gpu/drm/rockchip/cdn-dp-core.c
@@ -157,6 +157,9 @@ static int cdn_dp_get_port_lanes(struct cdn_dp_port *port)
 	int dptx;
 	u8 lanes;
 
+	if (!edev)
+		return phy_get_bus_width(port->phy);
+
 	dptx = extcon_get_state(edev, EXTCON_DISP_DP);
 	if (dptx > 0) {
 		extcon_get_property(edev, EXTCON_DISP_DP,
@@ -220,7 +223,7 @@ static bool cdn_dp_check_sink_connection(struct cdn_dp_device *dp)
 	 * some docks need more time to power up.
 	 */
 	while (time_before(jiffies, timeout)) {
-		if (!extcon_get_state(port->extcon, EXTCON_DISP_DP))
+		if (port->extcon && !extcon_get_state(port->extcon, EXTCON_DISP_DP))
 			return false;
 
 		if (!cdn_dp_get_sink_count(dp, &sink_count))
@@ -386,11 +389,14 @@ static int cdn_dp_enable_phy(struct cdn_dp_device *dp, struct cdn_dp_port *port)
 		goto err_power_on;
 	}
 
-	ret = extcon_get_property(port->extcon, EXTCON_DISP_DP,
-				  EXTCON_PROP_USB_TYPEC_POLARITY, &property);
-	if (ret) {
-		DRM_DEV_ERROR(dp->dev, "get property failed\n");
-		goto err_power_on;
+	property.intval = 0;
+	if (port->extcon) {
+		ret = extcon_get_property(port->extcon, EXTCON_DISP_DP,
+					  EXTCON_PROP_USB_TYPEC_POLARITY, &property);
+		if (ret) {
+			DRM_DEV_ERROR(dp->dev, "get property failed\n");
+			goto err_power_on;
+		}
 	}
 
 	port->lanes = cdn_dp_get_port_lanes(port);
@@ -1027,6 +1033,9 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
 	for (i = 0; i < dp->ports; i++) {
 		port = dp->port[i];
 
+		if (!port->extcon)
+			continue;
+
 		port->event_nb.notifier_call = cdn_dp_pd_event;
 		ret = devm_extcon_register_notifier(dp->dev, port->extcon,
 						    EXTCON_DISP_DP,
@@ -1119,14 +1128,14 @@ static int cdn_dp_probe(struct platform_device *pdev)
 		    PTR_ERR(phy) == -EPROBE_DEFER)
 			return -EPROBE_DEFER;
 
-		if (IS_ERR(extcon) || IS_ERR(phy))
+		if (IS_ERR(phy) || PTR_ERR(extcon) != -ENODEV)
 			continue;
 
 		port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL);
 		if (!port)
 			return -ENOMEM;
 
-		port->extcon = extcon;
+		port->extcon = IS_ERR(extcon) ? NULL : extcon;
 		port->phy = phy;
 		port->dp = dp;
 		port->id = i;
-- 
2.53.0



From kernel at airkyi.com  Tue Aug  4 00:07:30 2026
From: kernel at airkyi.com (Chaoyi Chen)
Date: Tue,  4 Aug 2026 15:07:30 +0800
Subject: [PATCH v6 7/7] drm/rockchip: cdn-dp: Add multiple bridges to support PHY port selection
In-Reply-To: <20260804070730.68-1-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
Message-ID: <20260804070730.68-8-kernel@airkyi.com>

From: Chaoyi Chen <chaoyi.chen at rock-chips.com>

The RK3399 has two USB/DP combo PHY and one CDN-DP controller. And
the CDN-DP can be switched to output to one of the PHYs. If both ports
are plugged into DP, DP will select the first port for output.

This patch adds support for multiple bridges, enabling users to flexibly
select the output port. For each PHY port, a separate encoder and bridge
are registered.

The change is based on the DRM AUX HPD bridge, rather than the
extcon approach. This requires the DT to correctly describe the
connections between the first bridge in bridge chain and DP
controller. For example, the bridge chain may be like this:

PHY aux birdge -> fsa4480 analog audio switch bridge ->
onnn,nb7vpq904m USB reminder bridge -> USB-C controller AUX HPD bridge

In this case, the connection relationships among the PHY aux bridge
and the DP contorller need to be described in DT.

In addition, the cdn_dp_parse_next_bridge_dt() will parses it and
determines whether to register one or two bridges.

Since there is only one DP controller, only one of the PHY ports can
output at a time. The key is how to switch between different PHYs,
which is handled by cdn_dp_switch_port() and cdn_dp_enable().

There are two cases:

1. Neither bridge is enabled. In this case, both bridges can
independently read the EDID, and the PHY port may switch before
reading the EDID.

2. One bridge is already enabled. In this case, other bridges are not
allowed to read the EDID. So we will try to return the cached EDID.

Since the scenario of two ports plug in at the same time is rare,
I don't have a board which support two TypeC connector to test this.
Therefore, I tested forced switching on a single PHY port, as well as
output using a fake PHY port alongside a real PHY port.

Signed-off-by: Chaoyi Chen <chaoyi.chen at rock-chips.com>
Reviewed-by: Luca Ceresoli <luca.ceresoli at bootlin.com>
Reviewed-by: Heiko Stuebner <heiko at sntech.de>
---

(no changes since v4)

Changes in v3:
- Remove now-redundant call to drm_connector_attach_encoder()

Changes in v2:
- Fix the check logic for dp->active_port (0 is valid).
- Fix the uninitialized prev_port.
- Remove duplicate logic from cdn_dp_switch_port().
- Fix the prototype of cdn_dp_bridge_hpd_notify().
- Properly release connector->fwnode.
- Properly release next_bridge.
---
 drivers/gpu/drm/rockchip/Kconfig       |   1 +
 drivers/gpu/drm/rockchip/cdn-dp-core.c | 333 ++++++++++++++++++++-----
 drivers/gpu/drm/rockchip/cdn-dp-core.h |  18 +-
 3 files changed, 294 insertions(+), 58 deletions(-)

diff --git a/drivers/gpu/drm/rockchip/Kconfig b/drivers/gpu/drm/rockchip/Kconfig
index e7f49fe845ea..ce70f91c13e6 100644
--- a/drivers/gpu/drm/rockchip/Kconfig
+++ b/drivers/gpu/drm/rockchip/Kconfig
@@ -60,6 +60,7 @@ config ROCKCHIP_CDN_DP
 	select DRM_DISPLAY_HELPER
 	select DRM_BRIDGE_CONNECTOR
 	select DRM_DISPLAY_DP_HELPER
+	select DRM_AUX_HPD_BRIDGE
 	help
 	  This selects support for Rockchip SoC specific extensions
 	  for the cdn DP driver. If you want to enable Dp on
diff --git a/drivers/gpu/drm/rockchip/cdn-dp-core.c b/drivers/gpu/drm/rockchip/cdn-dp-core.c
index 74e1f5422a6e..5e513f6930cb 100644
--- a/drivers/gpu/drm/rockchip/cdn-dp-core.c
+++ b/drivers/gpu/drm/rockchip/cdn-dp-core.c
@@ -28,16 +28,17 @@
 #include "cdn-dp-core.h"
 #include "cdn-dp-reg.h"
 
-static inline struct cdn_dp_device *bridge_to_dp(struct drm_bridge *bridge)
+static int cdn_dp_switch_port(struct cdn_dp_device *dp, struct cdn_dp_port *prev_port,
+			      struct cdn_dp_port *port);
+
+static inline struct cdn_dp_bridge *bridge_to_dp_bridge(struct drm_bridge *bridge)
 {
-	return container_of(bridge, struct cdn_dp_device, bridge);
+	return container_of(bridge, struct cdn_dp_bridge, bridge);
 }
 
-static inline struct cdn_dp_device *encoder_to_dp(struct drm_encoder *encoder)
+static inline struct cdn_dp_device *bridge_to_dp(struct drm_bridge *bridge)
 {
-	struct rockchip_encoder *rkencoder = to_rockchip_encoder(encoder);
-
-	return container_of(rkencoder, struct cdn_dp_device, encoder);
+	return bridge_to_dp_bridge(bridge)->parent;
 }
 
 #define GRF_SOC_CON9		0x6224
@@ -192,14 +193,27 @@ static int cdn_dp_get_sink_count(struct cdn_dp_device *dp, u8 *sink_count)
 static struct cdn_dp_port *cdn_dp_connected_port(struct cdn_dp_device *dp)
 {
 	struct cdn_dp_port *port;
-	int i, lanes;
+	int i, lanes[MAX_PHY];
 
 	for (i = 0; i < dp->ports; i++) {
 		port = dp->port[i];
-		lanes = cdn_dp_get_port_lanes(port);
-		if (lanes)
+		lanes[i] = cdn_dp_get_port_lanes(port);
+		if (!dp->next_bridge_valid)
 			return port;
 	}
+
+	if (dp->next_bridge_valid) {
+		/* If more than one port is available, pick the last active port */
+		if (dp->active_port >= 0 && lanes[dp->active_port])
+			return dp->port[dp->active_port];
+
+		/* If the last active port is not available, pick an available port in order */
+		for (i = 0; i < dp->bridge_count; i++) {
+			if (lanes[i])
+				return dp->port[i];
+		}
+	}
+
 	return NULL;
 }
 
@@ -254,12 +268,45 @@ static const struct drm_edid *
 cdn_dp_bridge_edid_read(struct drm_bridge *bridge, struct drm_connector *connector)
 {
 	struct cdn_dp_device *dp = bridge_to_dp(bridge);
-	const struct drm_edid *drm_edid;
+	struct cdn_dp_bridge *dp_bridge = bridge_to_dp_bridge(bridge);
+	struct cdn_dp_port *port = dp->port[dp_bridge->id];
+	struct cdn_dp_port *prev_port = NULL;
+	const struct drm_edid *drm_edid = NULL;
+	int i, ret;
 
 	mutex_lock(&dp->lock);
+
+	/* More than one port is available */
+	if (dp->bridge_count > 1 && !port->phy_enabled) {
+		for (i = 0; i < dp->bridge_count; i++) {
+			/* Another port already enable */
+			if (dp->bridge_list[i] != dp_bridge && dp->bridge_list[i]->enabled)
+				goto get_cache;
+			/* Find already enabled port */
+			if (dp->port[i]->phy_enabled)
+				prev_port = dp->port[i];
+		}
+
+		/* Switch to current port */
+		if (prev_port) {
+			ret = cdn_dp_switch_port(dp, prev_port, port);
+			if (ret)
+				goto get_cache;
+		}
+	}
+
 	drm_edid = drm_edid_read_custom(connector, cdn_dp_get_edid_block, dp);
+	/* replace edid cache */
+	if (dp->edid_cache[dp_bridge->id])
+		drm_edid_free(dp->edid_cache[dp_bridge->id]);
+	dp->edid_cache[dp_bridge->id] = drm_edid_dup(drm_edid);
+
 	mutex_unlock(&dp->lock);
+	return drm_edid;
 
+get_cache:
+	drm_edid = drm_edid_dup(dp->edid_cache[dp_bridge->id]);
+	mutex_unlock(&dp->lock);
 	return drm_edid;
 }
 
@@ -268,12 +315,13 @@ cdn_dp_bridge_mode_valid(struct drm_bridge *bridge,
 			 const struct drm_display_info *display_info,
 			 const struct drm_display_mode *mode)
 {
+	struct cdn_dp_bridge *dp_bridge = bridge_to_dp_bridge(bridge);
 	struct cdn_dp_device *dp = bridge_to_dp(bridge);
 	u32 requested, actual, rate, sink_max, source_max = 0;
 	u8 lanes, bpc;
 
 	/* If DP is disconnected, every mode is invalid */
-	if (!dp->connected)
+	if (!dp_bridge->connected || !dp->connected)
 		return MODE_BAD;
 
 	switch (display_info->bpc) {
@@ -551,6 +599,50 @@ static bool cdn_dp_check_link_status(struct cdn_dp_device *dp)
 	return drm_dp_channel_eq_ok(link_status, min(port->lanes, sink_lanes));
 }
 
+static int cdn_dp_switch_port(struct cdn_dp_device *dp, struct cdn_dp_port *prev_port,
+			      struct cdn_dp_port *port)
+{
+	int ret;
+
+	ret = cdn_dp_disable_phy(dp, prev_port);
+	if (ret)
+		goto out;
+	ret = cdn_dp_enable_phy(dp, port);
+	if (ret)
+		goto out;
+
+	ret = cdn_dp_get_sink_capability(dp);
+	if (ret) {
+		cdn_dp_disable_phy(dp, port);
+		goto out;
+	}
+
+	dp->lanes = port->lanes;
+
+	if (!cdn_dp_check_link_status(dp)) {
+		dev_info(dp->dev, "Connected with sink; re-train link\n");
+
+		ret = cdn_dp_train_link(dp);
+		if (ret) {
+			dev_err(dp->dev, "Training link failed: %d\n", ret);
+			goto out;
+		}
+
+		ret = cdn_dp_set_video_status(dp, CONTROL_VIDEO_IDLE);
+		if (ret) {
+			dev_err(dp->dev, "Failed to idle video %d\n", ret);
+			goto out;
+		}
+
+		ret = cdn_dp_config_video(dp);
+		if (ret)
+			dev_err(dp->dev, "Failed to configure video: %d\n", ret);
+	}
+
+out:
+	return ret;
+}
+
 static void cdn_dp_display_info_update(struct cdn_dp_device *dp,
 				       struct drm_display_info *display_info)
 {
@@ -572,6 +664,7 @@ static void cdn_dp_display_info_update(struct cdn_dp_device *dp,
 static void cdn_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_atomic_commit *state)
 {
 	struct cdn_dp_device *dp = bridge_to_dp(bridge);
+	struct cdn_dp_bridge *dp_bridge = bridge_to_dp_bridge(bridge);
 	struct drm_connector *connector;
 	int ret, val;
 
@@ -581,7 +674,7 @@ static void cdn_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_at
 
 	cdn_dp_display_info_update(dp, &connector->display_info);
 
-	ret = drm_of_encoder_active_endpoint_id(dp->dev->of_node, &dp->encoder.encoder);
+	ret = drm_of_encoder_active_endpoint_id(dp->dev->of_node, &dp_bridge->encoder.encoder);
 	if (ret < 0) {
 		DRM_DEV_ERROR(dp->dev, "Could not get vop id, %d", ret);
 		return;
@@ -600,6 +693,9 @@ static void cdn_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_at
 
 	mutex_lock(&dp->lock);
 
+	if (dp->next_bridge_valid)
+		dp->active_port = dp_bridge->id;
+
 	ret = cdn_dp_enable(dp);
 	if (ret) {
 		DRM_DEV_ERROR(dp->dev, "Failed to enable bridge %d\n",
@@ -632,6 +728,7 @@ static void cdn_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_at
 		goto out;
 	}
 
+	dp_bridge->enabled = true;
 out:
 	mutex_unlock(&dp->lock);
 }
@@ -639,9 +736,11 @@ static void cdn_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_at
 static void cdn_dp_bridge_atomic_disable(struct drm_bridge *bridge, struct drm_atomic_commit *state)
 {
 	struct cdn_dp_device *dp = bridge_to_dp(bridge);
+	struct cdn_dp_bridge *dp_bridge = bridge_to_dp_bridge(bridge);
 	int ret;
 
 	mutex_lock(&dp->lock);
+	dp_bridge->enabled = false;
 
 	if (dp->active) {
 		ret = cdn_dp_disable(dp);
@@ -828,6 +927,17 @@ static int cdn_dp_audio_mute_stream(struct drm_bridge *bridge,
 	return ret;
 }
 
+static void cdn_dp_bridge_hpd_notify(struct drm_bridge *bridge,
+				     struct drm_connector *connector,
+				     enum drm_connector_status status)
+{
+	struct cdn_dp_bridge *dp_bridge = bridge_to_dp_bridge(bridge);
+	struct cdn_dp_device *dp = bridge_to_dp(bridge);
+
+	dp->bridge_list[dp_bridge->id]->connected = status == connector_status_connected;
+	schedule_work(&dp->event_work);
+}
+
 static const struct drm_bridge_funcs cdn_dp_bridge_funcs = {
 	.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
 	.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
@@ -838,6 +948,7 @@ static const struct drm_bridge_funcs cdn_dp_bridge_funcs = {
 	.atomic_disable = cdn_dp_bridge_atomic_disable,
 	.mode_valid = cdn_dp_bridge_mode_valid,
 	.mode_set = cdn_dp_bridge_mode_set,
+	.hpd_notify = cdn_dp_bridge_hpd_notify,
 
 	.dp_audio_prepare = cdn_dp_audio_prepare,
 	.dp_audio_mute_stream = cdn_dp_audio_mute_stream,
@@ -886,7 +997,8 @@ static void cdn_dp_pd_event_work(struct work_struct *work)
 {
 	struct cdn_dp_device *dp = container_of(work, struct cdn_dp_device,
 						event_work);
-	int ret;
+	bool connected;
+	int i, ret;
 
 	mutex_lock(&dp->lock);
 
@@ -945,9 +1057,12 @@ static void cdn_dp_pd_event_work(struct work_struct *work)
 
 out:
 	mutex_unlock(&dp->lock);
-	drm_bridge_hpd_notify(&dp->bridge,
-			      dp->connected ? connector_status_connected
-					    : connector_status_disconnected);
+	for (i = 0; i < dp->bridge_count; i++) {
+		connected = dp->connected && dp->bridge_list[i]->connected;
+		drm_bridge_hpd_notify(&dp->bridge_list[i]->bridge,
+				      connected ? connector_status_connected
+						: connector_status_disconnected);
+	}
 }
 
 static int cdn_dp_pd_event(struct notifier_block *nb,
@@ -967,28 +1082,16 @@ static int cdn_dp_pd_event(struct notifier_block *nb,
 	return NOTIFY_DONE;
 }
 
-static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
+static int cdn_bridge_add(struct device *dev,
+			  struct drm_bridge *bridge,
+			  struct drm_bridge *next_bridge,
+			  struct drm_encoder *encoder)
 {
 	struct cdn_dp_device *dp = dev_get_drvdata(dev);
-	struct drm_encoder *encoder;
+	struct drm_device *drm_dev = dp->drm_dev;
+	struct drm_bridge *last_bridge __free(drm_bridge_put) = NULL;
 	struct drm_connector *connector;
-	struct cdn_dp_port *port;
-	struct drm_device *drm_dev = data;
-	int ret, i;
-
-	ret = cdn_dp_parse_dt(dp);
-	if (ret < 0)
-		return ret;
-
-	dp->drm_dev = drm_dev;
-	dp->connected = false;
-	dp->active = false;
-	dp->active_port = -1;
-	dp->fw_loaded = false;
-
-	INIT_WORK(&dp->event_work, cdn_dp_pd_event_work);
-
-	encoder = &dp->encoder.encoder;
+	int ret;
 
 	encoder->possible_crtcs = drm_of_find_possible_crtcs(drm_dev,
 							     dev->of_node);
@@ -1003,26 +1106,35 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
 
 	drm_encoder_helper_add(encoder, &cdn_dp_encoder_helper_funcs);
 
-	dp->bridge.ops =
-			DRM_BRIDGE_OP_DETECT |
-			DRM_BRIDGE_OP_EDID |
-			DRM_BRIDGE_OP_HPD |
-			DRM_BRIDGE_OP_DP_AUDIO;
-	dp->bridge.of_node = dp->dev->of_node;
-	dp->bridge.type = DRM_MODE_CONNECTOR_DisplayPort;
-	dp->bridge.hdmi_audio_dev = dp->dev;
-	dp->bridge.hdmi_audio_max_i2s_playback_channels = 8;
-	dp->bridge.hdmi_audio_spdif_playback = 1;
-	dp->bridge.hdmi_audio_dai_port = -1;
-
-	ret = devm_drm_bridge_add(dev, &dp->bridge);
+	bridge->ops =
+		DRM_BRIDGE_OP_DETECT |
+		DRM_BRIDGE_OP_EDID |
+		DRM_BRIDGE_OP_HPD |
+		DRM_BRIDGE_OP_DP_AUDIO;
+	bridge->of_node = dp->dev->of_node;
+	bridge->type = DRM_MODE_CONNECTOR_DisplayPort;
+	bridge->hdmi_audio_dev = dp->dev;
+	bridge->hdmi_audio_max_i2s_playback_channels = 8;
+	bridge->hdmi_audio_spdif_playback = 1;
+	bridge->hdmi_audio_dai_port = -1;
+
+	ret = devm_drm_bridge_add(dev, bridge);
 	if (ret)
 		return ret;
 
-	ret = drm_bridge_attach(encoder, &dp->bridge, NULL, DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+	ret = drm_bridge_attach(encoder, bridge, NULL, DRM_BRIDGE_ATTACH_NO_CONNECTOR);
 	if (ret)
 		return ret;
 
+	if (next_bridge) {
+		ret = drm_bridge_attach(encoder, next_bridge, bridge,
+					DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+		if (ret)
+			return ret;
+
+		last_bridge = drm_bridge_chain_get_last_bridge(bridge->encoder);
+	}
+
 	connector = drm_bridge_connector_init(drm_dev, encoder);
 	if (IS_ERR(connector)) {
 		ret = PTR_ERR(connector);
@@ -1030,6 +1142,101 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
 		return ret;
 	}
 
+	if (last_bridge) {
+		fwnode_handle_put(connector->fwnode);
+		connector->fwnode = fwnode_handle_get(of_fwnode_handle(last_bridge->of_node));
+	}
+
+	return 0;
+}
+
+static int cdn_dp_parse_next_bridge_dt(struct cdn_dp_device *dp)
+{
+	struct device_node *np = dp->dev->of_node;
+	struct device_node *port __free(device_node) = of_graph_get_port_by_id(np, 1);
+	struct drm_bridge *bridge;
+	int count = 0;
+	int ret = 0;
+	int i;
+
+	/* If device use extcon, do not use hpd bridge */
+	for (i = 0; i < dp->ports; i++) {
+		if (dp->port[i]->extcon) {
+			dp->bridge_count = 1;
+			return 0;
+		}
+	}
+
+	/* One endpoint may correspond to one next bridge. */
+	for_each_of_graph_port_endpoint(port, dp_ep) {
+		struct device_node *next_bridge_node __free(device_node) =
+			of_graph_get_remote_port_parent(dp_ep);
+
+		bridge = of_drm_find_bridge(next_bridge_node);
+		if (!bridge) {
+			ret = -EPROBE_DEFER;
+			goto out;
+		}
+
+		dp->next_bridge_valid = true;
+		dp->next_bridge_list[count] = drm_bridge_get(bridge);
+		count++;
+	}
+
+out:
+	dp->bridge_count = count ? count : 1;
+	return ret;
+}
+
+static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
+{
+	struct cdn_dp_device *dp = dev_get_drvdata(dev);
+	struct drm_bridge *bridge, *next_bridge;
+	struct drm_encoder *encoder;
+	struct cdn_dp_port *port;
+	struct drm_device *drm_dev = data;
+	struct cdn_dp_bridge *dp_bridge;
+	int ret, i;
+
+	ret = cdn_dp_parse_dt(dp);
+	if (ret < 0)
+		return ret;
+
+	ret = cdn_dp_parse_next_bridge_dt(dp);
+	if (ret)
+		goto out;
+
+	dp->drm_dev = drm_dev;
+	dp->connected = false;
+	dp->active = false;
+	dp->active_port = -1;
+	dp->fw_loaded = false;
+
+	for (i = 0; i < dp->bridge_count; i++) {
+		dp_bridge = devm_drm_bridge_alloc(dev, struct cdn_dp_bridge, bridge,
+						    &cdn_dp_bridge_funcs);
+		if (IS_ERR(dp_bridge)) {
+			ret = PTR_ERR(dp_bridge);
+			goto out;
+		}
+		dp_bridge->id = i;
+		dp_bridge->parent = dp;
+		if (!dp->next_bridge_valid)
+			dp_bridge->connected = true;
+		dp->bridge_list[i] = dp_bridge;
+	}
+
+	for (i = 0; i < dp->bridge_count; i++) {
+		encoder = &dp->bridge_list[i]->encoder.encoder;
+		bridge = &dp->bridge_list[i]->bridge;
+		next_bridge = dp->next_bridge_list[i];
+		ret = cdn_bridge_add(dev, bridge, next_bridge, encoder);
+		if (ret)
+			goto out;
+	}
+
+	INIT_WORK(&dp->event_work, cdn_dp_pd_event_work);
+
 	for (i = 0; i < dp->ports; i++) {
 		port = dp->port[i];
 
@@ -1043,7 +1250,7 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
 		if (ret) {
 			DRM_DEV_ERROR(dev,
 				      "register EXTCON_DISP_DP notifier err\n");
-			return ret;
+			goto out;
 		}
 	}
 
@@ -1052,15 +1259,29 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
 	schedule_work(&dp->event_work);
 
 	return 0;
+
+out:
+	for (i = 0; i < MAX_PHY; i++)
+		drm_bridge_put(dp->next_bridge_list[i]);
+
+	return ret;
 }
 
 static void cdn_dp_unbind(struct device *dev, struct device *master, void *data)
 {
 	struct cdn_dp_device *dp = dev_get_drvdata(dev);
-	struct drm_encoder *encoder = &dp->encoder.encoder;
+	struct drm_encoder *encoder;
+	int i;
 
 	cancel_work_sync(&dp->event_work);
-	encoder->funcs->destroy(encoder);
+	for (i = 0; i < dp->bridge_count; i++) {
+		encoder = &dp->bridge_list[i]->encoder.encoder;
+		encoder->funcs->destroy(encoder);
+		drm_bridge_put(dp->next_bridge_list[i]);
+	}
+
+	for (i = 0; i < MAX_PHY; i++)
+		drm_edid_free(dp->edid_cache[i]);
 
 	pm_runtime_disable(dev);
 	if (dp->fw_loaded)
@@ -1111,10 +1332,10 @@ static int cdn_dp_probe(struct platform_device *pdev)
 	int ret;
 	int i;
 
-	dp = devm_drm_bridge_alloc(dev, struct cdn_dp_device, bridge,
-				   &cdn_dp_bridge_funcs);
-	if (IS_ERR(dp))
-		return PTR_ERR(dp);
+	dp = devm_kzalloc(dev, sizeof(*dp), GFP_KERNEL);
+	if (!dp)
+		return -ENOMEM;
+
 	dp->dev = dev;
 
 	match = of_match_node(cdn_dp_dt_ids, pdev->dev.of_node);
diff --git a/drivers/gpu/drm/rockchip/cdn-dp-core.h b/drivers/gpu/drm/rockchip/cdn-dp-core.h
index e9c30b9fd543..c10e423bbf06 100644
--- a/drivers/gpu/drm/rockchip/cdn-dp-core.h
+++ b/drivers/gpu/drm/rockchip/cdn-dp-core.h
@@ -38,6 +38,8 @@ enum vic_pxl_encoding_format {
 	Y_ONLY = 0x10,
 };
 
+struct cdn_dp_device;
+
 struct video_info {
 	bool h_sync_polarity;
 	bool v_sync_polarity;
@@ -63,16 +65,28 @@ struct cdn_dp_port {
 	u8 id;
 };
 
+struct cdn_dp_bridge {
+	struct cdn_dp_device *parent;
+	struct drm_bridge bridge;
+	struct rockchip_encoder encoder;
+	bool connected;
+	bool enabled;
+	int id;
+};
+
 struct cdn_dp_device {
 	struct device *dev;
 	struct drm_device *drm_dev;
-	struct drm_bridge bridge;
-	struct rockchip_encoder encoder;
+	int bridge_count;
+	struct cdn_dp_bridge *bridge_list[MAX_PHY];
+	struct drm_bridge *next_bridge_list[MAX_PHY];
+	const struct drm_edid *edid_cache[MAX_PHY];
 	struct drm_display_mode mode;
 	struct platform_device *audio_pdev;
 	struct work_struct event_work;
 
 	struct mutex lock;
+	bool next_bridge_valid;
 	bool connected;
 	bool active;
 	bool suspended;
-- 
2.53.0



From kernel at airkyi.com  Tue Aug  4 00:07:27 2026
From: kernel at airkyi.com (Chaoyi Chen)
Date: Tue,  4 Aug 2026 15:07:27 +0800
Subject: [PATCH v6 4/7] drm/bridge: aux: Add drm_aux_bridge_register_from_node()
In-Reply-To: <20260804070730.68-1-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
Message-ID: <20260804070730.68-5-kernel@airkyi.com>

From: Chaoyi Chen <chaoyi.chen at rock-chips.com>

The drm_aux_bridge_register() uses the device->of_node as the
bridge->of_node.

This patch adds drm_aux_bridge_register_from_node() to allow
specifying the of_node corresponding to the bridge.

Signed-off-by: Chaoyi Chen <chaoyi.chen at rock-chips.com>
Reviewed-by: Neil Armstrong <neil.armstrong at linaro.org>
Reviewed-by: Nicolas Frattaroli <nicolas.frattaroli at collabora.com>
Reviewed-by: Sebastian Reichel <sebastian.reichel at collabora.com>
---

(no changes since v3)

Changes in v2:
- Fix incorrect function names in the documentation comments.
---
 drivers/gpu/drm/bridge/aux-bridge.c | 26 +++++++++++++++++++++++---
 include/drm/bridge/aux-bridge.h     |  6 ++++++
 2 files changed, 29 insertions(+), 3 deletions(-)

diff --git a/drivers/gpu/drm/bridge/aux-bridge.c b/drivers/gpu/drm/bridge/aux-bridge.c
index 1ed21a8713bf..06a1466f49bd 100644
--- a/drivers/gpu/drm/bridge/aux-bridge.c
+++ b/drivers/gpu/drm/bridge/aux-bridge.c
@@ -33,8 +33,9 @@ static void drm_aux_bridge_unregister_adev(void *_adev)
 }
 
 /**
- * drm_aux_bridge_register - Create a simple bridge device to link the chain
+ * drm_aux_bridge_register_from_node - Create a simple bridge device to link the chain
  * @parent: device instance providing this bridge
+ * @np: device node pointer corresponding to this bridge instance
  *
  * Creates a simple DRM bridge that doesn't implement any drm_bridge
  * operations. Such bridges merely fill a place in the bridge chain linking
@@ -42,7 +43,7 @@ static void drm_aux_bridge_unregister_adev(void *_adev)
  *
  * Return: zero on success, negative error code on failure
  */
-int drm_aux_bridge_register(struct device *parent)
+int drm_aux_bridge_register_from_node(struct device *parent, struct device_node *np)
 {
 	struct auxiliary_device *adev;
 	int ret;
@@ -62,7 +63,10 @@ int drm_aux_bridge_register(struct device *parent)
 	adev->dev.parent = parent;
 	adev->dev.release = drm_aux_bridge_release;
 
-	device_set_of_node_from_dev(&adev->dev, parent);
+	if (np)
+		device_set_node(&adev->dev, of_fwnode_handle(np));
+	else
+		device_set_of_node_from_dev(&adev->dev, parent);
 
 	ret = auxiliary_device_init(adev);
 	if (ret) {
@@ -80,6 +84,22 @@ int drm_aux_bridge_register(struct device *parent)
 
 	return devm_add_action_or_reset(parent, drm_aux_bridge_unregister_adev, adev);
 }
+EXPORT_SYMBOL_GPL(drm_aux_bridge_register_from_node);
+
+/**
+ * drm_aux_bridge_register - Create a simple bridge device to link the chain
+ * @parent: device instance providing this bridge
+ *
+ * Creates a simple DRM bridge that doesn't implement any drm_bridge
+ * operations. Such bridges merely fill a place in the bridge chain linking
+ * surrounding DRM bridges.
+ *
+ * Return: zero on success, negative error code on failure
+ */
+int drm_aux_bridge_register(struct device *parent)
+{
+	return drm_aux_bridge_register_from_node(parent, NULL);
+}
 EXPORT_SYMBOL_GPL(drm_aux_bridge_register);
 
 struct drm_aux_bridge_data {
diff --git a/include/drm/bridge/aux-bridge.h b/include/drm/bridge/aux-bridge.h
index cca07a8e2d45..67f74ed287d9 100644
--- a/include/drm/bridge/aux-bridge.h
+++ b/include/drm/bridge/aux-bridge.h
@@ -13,11 +13,17 @@ struct auxiliary_device;
 
 #if IS_ENABLED(CONFIG_DRM_AUX_BRIDGE)
 int drm_aux_bridge_register(struct device *parent);
+int drm_aux_bridge_register_from_node(struct device *parent, struct device_node *np);
 #else
 static inline int drm_aux_bridge_register(struct device *parent)
 {
 	return 0;
 }
+
+static inline int drm_aux_bridge_register_from_node(struct device *parent, struct device_node *np)
+{
+	return 0;
+}
 #endif
 
 #if IS_ENABLED(CONFIG_DRM_AUX_HPD_BRIDGE)
-- 
2.53.0



From kernel at airkyi.com  Tue Aug  4 00:07:26 2026
From: kernel at airkyi.com (Chaoyi Chen)
Date: Tue,  4 Aug 2026 15:07:26 +0800
Subject: [PATCH v6 3/7] drm/display: Add soft depend for aux-hpd-typec-dp-bridge module
In-Reply-To: <20260804070730.68-1-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
Message-ID: <20260804070730.68-4-kernel@airkyi.com>

From: Chaoyi Chen <chaoyi.chen at rock-chips.com>

The aux-hpd-typec-dp-bridge module serves as a generic TypeC
DisplayPort HPD bridge and is not required by any other
module. Therefore, it will not be auto-loaded.

Given that the drm_display_helper module houses DisplayPort-related
helper code, add a MODULE_SOFTDEP() within it to suggest loading the
aux-hpd-typec-dp-bridge module beforehand.

Suggested-by: Sebastian Reichel <sebastian.reichel at collabora.com>
Signed-off-by: Chaoyi Chen <chaoyi.chen at rock-chips.com>
---

(no changes since v6)

---
 drivers/gpu/drm/display/drm_display_helper_mod.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/drivers/gpu/drm/display/drm_display_helper_mod.c b/drivers/gpu/drm/display/drm_display_helper_mod.c
index d8a6e6228773..f0152d6b0b2d 100644
--- a/drivers/gpu/drm/display/drm_display_helper_mod.c
+++ b/drivers/gpu/drm/display/drm_display_helper_mod.c
@@ -18,5 +18,6 @@ static void __exit drm_display_helper_module_exit(void)
 	drm_dp_aux_dev_exit();
 }
 
+MODULE_SOFTDEP("pre: aux-hpd-typec-dp-bridge");
 module_init(drm_display_helper_module_init);
 module_exit(drm_display_helper_module_exit);
-- 
2.53.0



From kernel at airkyi.com  Tue Aug  4 00:07:28 2026
From: kernel at airkyi.com (Chaoyi Chen)
Date: Tue,  4 Aug 2026 15:07:28 +0800
Subject: [PATCH v6 5/7] phy: rockchip: phy-rockchip-typec: Add DRM AUX bridge
In-Reply-To: <20260804070730.68-1-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
Message-ID: <20260804070730.68-6-kernel@airkyi.com>

From: Chaoyi Chen <chaoyi.chen at rock-chips.com>

Using the DRM_AUX_BRIDGE helper to create the transparent DRM bridge
device.

Signed-off-by: Chaoyi Chen <chaoyi.chen at rock-chips.com>
Reviewed-by: Neil Armstrong <neil.armstrong at linaro.org>
Reviewed-by: Sebastian Reichel <sebastian.reichel at collabora.com>
---

(no changes since v1)

---
 drivers/phy/rockchip/Kconfig              |  2 ++
 drivers/phy/rockchip/phy-rockchip-typec.c | 13 +++++++++++--
 2 files changed, 13 insertions(+), 2 deletions(-)

diff --git a/drivers/phy/rockchip/Kconfig b/drivers/phy/rockchip/Kconfig
index 14698571b607..9173d3b4fef4 100644
--- a/drivers/phy/rockchip/Kconfig
+++ b/drivers/phy/rockchip/Kconfig
@@ -119,6 +119,8 @@ config PHY_ROCKCHIP_SNPS_PCIE3
 config PHY_ROCKCHIP_TYPEC
 	tristate "Rockchip TYPEC PHY Driver"
 	depends on OF && (ARCH_ROCKCHIP || COMPILE_TEST)
+	depends on DRM || DRM=n
+	select DRM_AUX_BRIDGE if DRM_BRIDGE
 	select EXTCON
 	select GENERIC_PHY
 	select RESET_CONTROLLER
diff --git a/drivers/phy/rockchip/phy-rockchip-typec.c b/drivers/phy/rockchip/phy-rockchip-typec.c
index d9701b6106d5..48070b50416e 100644
--- a/drivers/phy/rockchip/phy-rockchip-typec.c
+++ b/drivers/phy/rockchip/phy-rockchip-typec.c
@@ -54,6 +54,7 @@
 
 #include <linux/mfd/syscon.h>
 #include <linux/phy/phy.h>
+#include <drm/bridge/aux-bridge.h>
 
 #define CMN_SSM_BANDGAP			(0x21 << 2)
 #define CMN_SSM_BIAS			(0x22 << 2)
@@ -1162,16 +1163,24 @@ static int rockchip_typec_phy_probe(struct platform_device *pdev)
 
 	for_each_available_child_of_node(np, child_np) {
 		struct phy *phy;
+		ret = 0;
 
-		if (of_node_name_eq(child_np, "dp-port"))
+		if (of_node_name_eq(child_np, "dp-port")) {
 			phy = devm_phy_create(dev, child_np,
 					      &rockchip_dp_phy_ops);
-		else if (of_node_name_eq(child_np, "usb3-port"))
+			ret = drm_aux_bridge_register_from_node(dev, child_np);
+		} else if (of_node_name_eq(child_np, "usb3-port"))
 			phy = devm_phy_create(dev, child_np,
 					      &rockchip_usb3_phy_ops);
 		else
 			continue;
 
+		if (ret) {
+			pm_runtime_disable(dev);
+			of_node_put(child_np);
+			return ret;
+		}
+
 		if (IS_ERR(phy)) {
 			dev_err(dev, "failed to create phy: %pOFn\n",
 				child_np);
-- 
2.53.0



From sashiko-bot at kernel.org  Tue Aug  4 00:19:02 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Tue, 04 Aug 2026 07:19:02 +0000
Subject: [PATCH v6 3/7] drm/display: Add soft depend for
 aux-hpd-typec-dp-bridge module
In-Reply-To: <20260804070730.68-4-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
 <20260804070730.68-4-kernel@airkyi.com>
Message-ID: <20260804071903.795F21F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] The `MODULE_SOFTDEP` approach fails to load the `aux-hpd-typec-dp-bridge` module if `CONFIG_DRM_DISPLAY_HELPER` is built into the kernel (`=y`) and `CONFIG_DRM_AUX_HPD_TYPEC_BRIDGE` is built as a module (`=m`).
- [Medium] Adding a soft dependency on `aux-hpd-typec-dp-bridge` to a core DRM helper unconditionally forces the loading of the USB Type-C subsystem on almost any system with a display, causing significant module bloat.
--

--- Patch [3]: [PATCH v6 3/7] drm/display: Add soft depend for aux-hpd-typec-dp-bridge module ---
commit f6b97c0e7a5ceab0d443dbadd5246c5cc93f2ec7
Author: Chaoyi Chen <chaoyi.chen at rock-chips.com>

drm/display: Add soft depend for aux-hpd-typec-dp-bridge module

This patch adds a soft dependency on the aux-hpd-typec-dp-bridge module to
the core drm_display_helper module. It attempts to ensure the bridge module
is auto-loaded before the display helper.

> diff --git a/drivers/gpu/drm/display/drm_display_helper_mod.c b/drivers/gpu/drm/display/drm_display_helper_mod.c
> index d8a6e62287736..f0152d6b0b2d7 100644
> --- a/drivers/gpu/drm/display/drm_display_helper_mod.c
> +++ b/drivers/gpu/drm/display/drm_display_helper_mod.c
[ ... ]
> @@ -18,5 +18,6 @@ static void __exit drm_display_helper_module_exit(void)
>  	drm_dp_aux_dev_exit();
>  }
>  
> +MODULE_SOFTDEP("pre: aux-hpd-typec-dp-bridge");

[Severity: Medium]
Does this soft dependency unconditionally pull the entire USB Type-C subsystem
into memory on almost every system that loads a standard DRM driver?

Since aux-hpd-typec-dp-bridge depends on typec.ko, it seems this will cause
unnecessary memory bloat on systems without Type-C displays, simply because
their DRM driver (like amdgpu, i915, or nouveau) uses drm_display_helper.

[Severity: High]
Will this approach fail to auto-load the bridge if CONFIG_DRM_DISPLAY_HELPER
is built into the kernel (=y) and CONFIG_DRM_AUX_HPD_TYPEC_BRIDGE is built
as a module (=m)?

If drm_display_helper is built-in, modprobe is never invoked for it during
boot, which means the MODULE_SOFTDEP directive is ignored. Because the
aux-hpd-typec-dp-bridge lacks a MODULE_DEVICE_TABLE or udev loading
mechanism, wouldn't it fail to load entirely in this configuration, silently
breaking Type-C DisplayPort hotplug detection?

>  module_init(drm_display_helper_module_init);
>  module_exit(drm_display_helper_module_exit);

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804070730.68-1-kernel at airkyi.com?part=3


From sashiko-bot at kernel.org  Tue Aug  4 00:22:50 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Tue, 04 Aug 2026 07:22:50 +0000
Subject: [PATCH v6 4/7] drm/bridge: aux: Add
 drm_aux_bridge_register_from_node()
In-Reply-To: <20260804070730.68-5-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
 <20260804070730.68-5-kernel@airkyi.com>
Message-ID: <20260804072251.4BFC71F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] drm_aux_bridge_register_from_node() assigns the provided device node without taking a reference, leading to a reference count underflow when the device is released or initialization fails.
--

--- Patch [4]: [PATCH v6 4/7] drm/bridge: aux: Add drm_aux_bridge_register_from_node() ---
commit 589f6bc20e86e41fcd3fb17c01ef52cecefa8a82
Author: Chaoyi Chen <chaoyi.chen at rock-chips.com>

drm/bridge: aux: Add drm_aux_bridge_register_from_node()

This commit introduces drm_aux_bridge_register_from_node() to allow
specifying the of_node corresponding to the bridge, extending the
existing drm_aux_bridge_register() functionality.

> diff --git a/drivers/gpu/drm/bridge/aux-bridge.c b/drivers/gpu/drm/bridge/aux-bridge.c
> index 1ed21a8713bfd..06a1466f49bd2 100644
> --- a/drivers/gpu/drm/bridge/aux-bridge.c
> +++ b/drivers/gpu/drm/bridge/aux-bridge.c
[ ... ]
> @@ -62,7 +63,10 @@ int drm_aux_bridge_register(struct device *parent)
>  	adev->dev.parent = parent;
>  	adev->dev.release = drm_aux_bridge_release;
>  
> -	device_set_of_node_from_dev(&adev->dev, parent);
> +	if (np)
> +		device_set_node(&adev->dev, of_fwnode_handle(np));

[Severity: High]
Does this code need to take a reference on np before setting it?

Looking at device_set_of_node_from_dev(), it correctly acquires a reference
using of_node_get(). However, device_set_node() only assigns the pointer
without taking a reference.

If initialization fails, the error path unconditionally drops a reference:

	ret = auxiliary_device_init(adev);
	if (ret) {
		of_node_put(adev->dev.of_node);
		...

Additionally, drm_aux_bridge_release() unconditionally calls of_node_put()
when the device is released.

Could assigning the node here without of_node_get() cause a reference count
underflow and a potential use-after-free for the provided node?

> +	else
> +		device_set_of_node_from_dev(&adev->dev, parent);
>  
>  	ret = auxiliary_device_init(adev);
>  	if (ret) {

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804070730.68-1-kernel at airkyi.com?part=4


From sashiko-bot at kernel.org  Tue Aug  4 00:25:34 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Tue, 04 Aug 2026 07:25:34 +0000
Subject: [PATCH v6 2/7] drm/bridge: Implement generic USB Type-C DP HPD
 bridge
In-Reply-To: <20260804070730.68-3-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
 <20260804070730.68-3-kernel@airkyi.com>
Message-ID: <20260804072535.5F5181F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Type-C DP HPD bridges are erroneously shared across multiple ports on non-DeviceTree (e.g., ACPI) systems due to NULL `of_node` matching.
- [Medium] Double registration of the DP HPD bridge due to a TOCTOU race condition between module initialization and concurrent device addition.
--

--- Patch [2]: [PATCH v6 2/7] drm/bridge: Implement generic USB Type-C DP HPD bridge ---
commit da360ed1d856c20444642867c8a203f9c48362b1
Author: Chaoyi Chen <chaoyi.chen at rock-chips.com>

drm/bridge: Implement generic USB Type-C DP HPD bridge

This patch implements a generic DP HPD bridge based on aux-hpd-bridge.c.
It monitors Type-C bus events, and when a Type-C port device containing
the DP svid is registered, it will create an HPD bridge for it without
the need for the USB-C controller driver to implement it.

> diff --git a/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c b/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c
> new file mode 100644
> index 0000000000000..682bc192e0934
> --- /dev/null
> +++ b/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c
> @@ -0,0 +1,67 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/*
> + * Copyright (C) 2026 Rockchip Electronics Co., Ltd.
> + *
> + * Author: Chaoyi Chen <chaoyi.chen at rock-chips.com>
> + */
> +#include <linux/of.h>
> +#include <linux/usb/typec_altmode.h>
> +#include <linux/usb/typec_dp.h>
> +
> +#include <drm/bridge/aux-bridge.h>
> +
> +static int drm_typec_bus_event(struct notifier_block *nb, unsigned long action,
> +			       void *data)
> +{
> +	struct device *dev = (struct device *)data;
> +	struct typec_altmode *alt = to_typec_altmode(dev);
> +	struct device_node *np;
> +
> +	if (action != BUS_NOTIFY_ADD_DEVICE)
> +		return NOTIFY_OK;
> +
> +	/*
> +	 * alt->dev.parent->parent : USB-C controller device
> +	 * alt->dev.parent         : USB-C connector device
> +	 */
> +	if (is_typec_port_altmode(&alt->dev) && alt->svid == USB_TYPEC_DP_SID) {
> +		np = to_of_node(alt->dev.parent->fwnode);
> +		if (!drm_dev_has_dp_hpd_bridge(alt->dev.parent->parent, np))
> +			drm_dp_hpd_bridge_register(alt->dev.parent->parent, np);

[Severity: High]
On non-DeviceTree systems (like ACPI platforms), will this fail to register
subsequent DP ports on a single USB-C controller?

Since to_of_node() evaluates to NULL for non-OF systems, np will be NULL.
When the first port registers the bridge, it uses NULL as the device node.
When the second port is processed, drm_dev_has_dp_hpd_bridge() will search
for an existing bridge. The underlying hpd_bridge_match() function checks:

    return adev->dev.platform_data == np;

Since adev->dev.platform_data is NULL and np is NULL, this returns true,
incorrectly concluding that the second port's bridge is already registered.
Could this silently break hotplug detection for those additional ports?

> +	}
> +
> +	return NOTIFY_OK;
> +}
> +
> +static struct notifier_block drm_typec_event_nb = {
> +	.notifier_call = drm_typec_bus_event,
> +};
> +
> +static int check_device_already_added(struct device *dev, void *data)
> +{
> +	drm_typec_bus_event(NULL, BUS_NOTIFY_ADD_DEVICE, dev);
> +	return 0;
> +}

[ ... ]

> +static int __init drm_aux_hpd_typec_dp_bridge_module_init(void)
> +{
> +	bus_register_notifier(&typec_bus, &drm_typec_event_nb);
> +	/*
> +	 * Before module initialization, some devices may have already been added.
> +	 * Register the HPD bridge for these devices.
> +	 */
> +	bus_for_each_dev(&typec_bus, NULL, NULL, check_device_already_added);

[Severity: Medium]
Could this setup a race condition resulting in a double registration of the
DP HPD bridge?

If a device is hotplugged precisely during module loading, the notifier
runs on the hotplug thread, and bus_for_each_dev() might visit the same
device on the init thread. Both threads would evaluate
!drm_dev_has_dp_hpd_bridge() as true in drm_typec_bus_event() since it is
a lockless check.

Would this cause drm_dp_hpd_bridge_register() to be called twice for the
same USB-C controller port?

> +	return 0;
> +}

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804070730.68-1-kernel at airkyi.com?part=2


From sashiko-bot at kernel.org  Tue Aug  4 00:32:07 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Tue, 04 Aug 2026 07:32:07 +0000
Subject: [PATCH v6 5/7] phy: rockchip: phy-rockchip-typec: Add DRM AUX
 bridge
In-Reply-To: <20260804070730.68-6-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
 <20260804070730.68-6-kernel@airkyi.com>
Message-ID: <20260804073208.9BD3F1F00A3A@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Passing `child_np` to `drm_aux_bridge_register_from_node()` without acquiring an additional reference leads to a refcount underflow and potential double-free.
- [Medium] The result of `devm_phy_create()` is not checked immediately, leading to potential error shadowing and broken probe deferral.
--

--- Patch [5]: [PATCH v6 5/7] phy: rockchip: phy-rockchip-typec: Add DRM AUX bridge ---
commit ed030870fca01a8999b3193c38becaaeb4276a91
Author: Chaoyi Chen <chaoyi.chen at rock-chips.com>

phy: rockchip: phy-rockchip-typec: Add DRM AUX bridge

Using the DRM_AUX_BRIDGE helper to create the transparent DRM bridge
device.

> diff --git a/drivers/phy/rockchip/phy-rockchip-typec.c b/drivers/phy/rockchip/phy-rockchip-typec.c
> index d9701b6106d55..48070b50416e5 100644
> --- a/drivers/phy/rockchip/phy-rockchip-typec.c
> +++ b/drivers/phy/rockchip/phy-rockchip-typec.c

[ ... ]

> @@ -1162,16 +1163,24 @@ static int rockchip_typec_phy_probe(struct platform_device *pdev)
>  
>  	for_each_available_child_of_node(np, child_np) {
>  		struct phy *phy;
> +		ret = 0;
>  
> -		if (of_node_name_eq(child_np, "dp-port"))
> +		if (of_node_name_eq(child_np, "dp-port")) {
>  			phy = devm_phy_create(dev, child_np,
>  					      &rockchip_dp_phy_ops);
> -		else if (of_node_name_eq(child_np, "usb3-port"))
> +			ret = drm_aux_bridge_register_from_node(dev, child_np);

[Severity: Medium]
Should we check if devm_phy_create() failed before registering the bridge?

If devm_phy_create() fails with an error like -EPROBE_DEFER, but
drm_aux_bridge_register_from_node() also fails, the code below checks 'ret'
first and returns that error. This could shadow the -EPROBE_DEFER from the
PHY creation and prevent the driver from properly probing again.

[Severity: High]
Does this code need to call of_node_get() on child_np before passing it here?

The for_each_available_child_of_node() loop only holds a reference to child_np
during the current iteration, dropping it when advancing to the next node.
However, drm_aux_bridge_register_from_node() assigns the node to the bridge
device without incrementing the reference count, and later calls of_node_put()
in its release callback.

Will this lead to a refcount underflow and use-after-free of the node memory
once the loop advances?

> +		} else if (of_node_name_eq(child_np, "usb3-port"))
>  			phy = devm_phy_create(dev, child_np,
>  					      &rockchip_usb3_phy_ops);
>  		else
>  			continue;
>  
> +		if (ret) {
> +			pm_runtime_disable(dev);
> +			of_node_put(child_np);

[Severity: High]
If drm_aux_bridge_register_from_node() fails, does this cause a double-free
of the OF node?

Because the loop reference was passed directly to the bridge registration
without incrementing the reference count, and the bridge already calls
of_node_put() on its own internal error path, calling of_node_put() here
again will drop the reference count a second time.

> +			return ret;
> +		}
> +
>  		if (IS_ERR(phy)) {
>  			dev_err(dev, "failed to create phy: %pOFn\n",
>  				child_np);

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804070730.68-1-kernel at airkyi.com?part=5


From sashiko-bot at kernel.org  Tue Aug  4 00:35:15 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Tue, 04 Aug 2026 07:35:15 +0000
Subject: [PATCH v6 6/7] drm/rockchip: cdn-dp: Support handle lane info
 without extcon
In-Reply-To: <20260804070730.68-7-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
 <20260804070730.68-7-kernel@airkyi.com>
Message-ID: <20260804073516.560F71F000E9@smtp.kernel.org>

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] Logic error in pointer error checking breaks probe for all existing boards using extcon.
- [High] Missing hotplug detection (HPD) mechanism when extcon is absent.
--

--- Patch [6]: [PATCH v6 6/7] drm/rockchip: cdn-dp: Support handle lane info without extcon ---
commit 2ec5f110f1557ebaec82cbe6b39d4e2ab7f4a88f
Author: Chaoyi Chen <chaoyi.chen at rock-chips.com>

drm/rockchip: cdn-dp: Support handle lane info without extcon

This patch add support for get PHY lane info without help of extcon.

There is no extcon needed if the Type-C controller is present. In this
case, the lane info can be get from PHY instead of extcon.

The extcon device should still be supported if Type-C controller is
not present.

> diff --git a/drivers/gpu/drm/rockchip/cdn-dp-core.c b/drivers/gpu/drm/rockchip/cdn-dp-core.c
> index 190cce9530c82..74e1f5422a6e2 100644
> --- a/drivers/gpu/drm/rockchip/cdn-dp-core.c
> +++ b/drivers/gpu/drm/rockchip/cdn-dp-core.c

[ ... ]

> @@ -1027,6 +1033,9 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
>  	for (i = 0; i < dp->ports; i++) {
>  		port = dp->port[i];
>  
> +		if (!port->extcon)
> +			continue;
> +

[Severity: High]
Does skipping the notifier registration when extcon is absent break hotplug 
detection? 

The driver relies on cdn_dp_pd_event_work() to transition dp->connected to 
true, load firmware, and train the link. Without the extcon notifier, 
event_work is never scheduled on hotplug. 

Since cdn_dp_bridge_detect() simply returns the static dp->connected state:

drivers/gpu/drm/rockchip/cdn-dp-core.c:cdn_dp_bridge_detect() {
    mutex_lock(&dp->lock);
    if (dp->connected)
        status = connector_status_connected;
    mutex_unlock(&dp->lock);
    ...
}

Could KMS polling always see the port as disconnected when hardware is 
configured without an extcon device?

>  		port->event_nb.notifier_call = cdn_dp_pd_event;
>  		ret = devm_extcon_register_notifier(dp->dev, port->extcon,
>  						    EXTCON_DISP_DP,

[ ... ]

> @@ -1119,14 +1128,14 @@ static int cdn_dp_probe(struct platform_device *pdev)
>  		    PTR_ERR(phy) == -EPROBE_DEFER)
>  			return -EPROBE_DEFER;
>  
> -		if (IS_ERR(extcon) || IS_ERR(phy))
> +		if (IS_ERR(phy) || PTR_ERR(extcon) != -ENODEV)
>  			continue;
>  

[Severity: Critical]
Will this condition accidentally skip port initialization for all valid extcon
pointers?

If extcon is a valid pointer, PTR_ERR(extcon) evaluates to its memory address,
which will never equal -ENODEV. This causes the condition to evaluate to true,
triggering the continue statement and bypassing port setup completely for any
hardware that has an extcon device.

>  		port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL);
>  		if (!port)
>  			return -ENOMEM;
>  
> -		port->extcon = extcon;
> +		port->extcon = IS_ERR(extcon) ? NULL : extcon;
>  		port->phy = phy;

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804070730.68-1-kernel at airkyi.com?part=6


From maxime.chevallier at bootlin.com  Tue Aug  4 00:35:00 2026
From: maxime.chevallier at bootlin.com (Maxime Chevallier)
Date: Tue, 4 Aug 2026 09:35:00 +0200
Subject: [PATCH v2 06/10] net: pcs: xpcs: add ANRESTART support for SGMII
 link recovery
In-Reply-To: <20260801142244.679437-8-coiaprant@gmail.com>
References: <20260801142244.679437-2-coiaprant@gmail.com>
 <20260801142244.679437-8-coiaprant@gmail.com>
Message-ID: <28ca553c-59ce-4608-b63a-6e8c4dfb6020@bootlin.com>

Hi Coia,

On 8/1/26 16:22, Coia Prant wrote:
> On some hardware using the DesignWare XPCS IP (e.g., RK3568 MAC side
> SGMII), the PCS does not automatically restart auto-negotiation when the
> link goes down and comes back up. Without an explicit ANRESTART, the link
> stays down forever.
> 
> Add BMCR_ANRESTART in two places:
> 1. In xpcs_config_aneg_c37_sgmii(), when starting AN, set ANRESTART
>    alongside ANENABLE to initiate a fresh negotiation.
> 2. In xpcs_get_state_c37_sgmii(), when link is down and AN completion is
>    detected, clear the interrupt and trigger ANRESTART to restart the
>    negotiation process.
> 
> This change has been verified on:
> - RK3568 (Photonicat board)
> - Wangxun NICs (no regression)
> 
> Tested-by: Jiawen Wu <jiawenwu at trustnetic.com>
> Signed-off-by: Coia Prant <coiaprant at gmail.com>

KSZ9477's old XPCS doesn't break with that code, tested in SGMII
mode :)

Tested-by: Maxime Chevallier <maxime.chevallier at bootlin.com>

Maxime

> ---
>  drivers/net/pcs/pcs-xpcs.c | 17 +++++++++++++----
>  1 file changed, 13 insertions(+), 4 deletions(-)
> 
> diff --git a/drivers/net/pcs/pcs-xpcs.c b/drivers/net/pcs/pcs-xpcs.c
> index 0337e2bcc0125..28e98449e8ac2 100644
> --- a/drivers/net/pcs/pcs-xpcs.c
> +++ b/drivers/net/pcs/pcs-xpcs.c
> @@ -816,9 +816,14 @@ static int xpcs_config_aneg_c37_sgmii(struct dw_xpcs *xpcs,
>  	if (ret < 0)
>  		return ret;
>  
> +	/* Clear CL37 AN complete status */
> +	ret = xpcs_write(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_INTR_STS, 0);
> +	if (ret < 0)
> +		return ret;
> +
>  	if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
>  		ret = xpcs_write(xpcs, MDIO_MMD_VEND2, MII_BMCR,
> -				 mdio_ctrl | BMCR_ANENABLE);
> +				 mdio_ctrl | BMCR_ANENABLE | BMCR_ANRESTART);
>  
>  	return ret;
>  }
> @@ -1089,14 +1094,18 @@ static int xpcs_get_state_c37_sgmii(struct dw_xpcs *xpcs,
>  			state->duplex = DUPLEX_FULL;
>  		else
>  			state->duplex = DUPLEX_HALF;
> -
>  		return 0;
>  	}
>  
> -	/* Clear AN complete status or interrupt */
> -	if (state->an_complete)
> +	if (state->an_complete) {
> +		/* Clear AN complete status or interrupt */
>  		xpcs_write(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_INTR_STS, 0);
>  
> +		/* Initiate the next round of AN */
> +		xpcs_modify(xpcs, MDIO_MMD_VEND2, MII_BMCR, BMCR_ANRESTART,
> +				    BMCR_ANRESTART);
> +	}
> +
>  	return 0;
>  }
>  



From sashiko-bot at kernel.org  Tue Aug  4 00:38:09 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Tue, 04 Aug 2026 07:38:09 +0000
Subject: [PATCH v6 7/7] drm/rockchip: cdn-dp: Add multiple bridges to
 support PHY port selection
In-Reply-To: <20260804070730.68-8-kernel@airkyi.com>
References: <20260804070730.68-1-kernel@airkyi.com>
 <20260804070730.68-8-kernel@airkyi.com>
Message-ID: <20260804073810.38E081F000E9@smtp.kernel.org>

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

New issues:
- [High] `cdn_dp_connected_port` unconditionally returns `dp->port[0]` for extcon setups, completely breaking Hot-Plug Detection.
- [High] A mismatch between the number of PHYs (`dp->ports`) and DT endpoints (`dp->bridge_count`) causes uninitialized stack reads and NULL pointer dereferences.
- [High] Out-of-bounds array write in `cdn_dp_parse_next_bridge_dt` when a device tree has more than two endpoints.
- [High] The error path in `cdn_dp_bind()` and successful path in `cdn_dp_unbind()` do not clear `dp->next_bridge_list` pointers after dropping references, leading to a Double-Put / Use-After-Free.
- [High] Double-free of EDID cache pointers across component unbind/rebind cycles.
- [High] Missing mutual exclusion in `atomic_check` allows simultaneous enablement of multiple bridges sharing a single DP controller.
- [High] Using `devm_drm_bridge_alloc()` and `devm_drm_bridge_add()` inside the component `bind` callback leads to memory leaks and dangling global DRM objects on unbind/rebind cycles.
- [High] `cdn_dp_switch_port` unconditionally programs video timings during EDID reads, causing failures when no valid mode is set.
- [High] Failure to clean up a successfully initialized `drm_encoder` in `cdn_bridge_add` error paths leads to Use-After-Free.
- [Medium] `INIT_WORK` is called after exposing the bridge to external subsystems, allowing an early HPD notification to schedule uninitialized work.

Pre-existing issues:
- [High] Using `devm_extcon_register_notifier` inside a component `bind` function causes notifier chain corruption and UAF on rebind.
--

--- Patch [7]: [PATCH v6 7/7] drm/rockchip: cdn-dp: Add multiple bridges to support PHY port selection ---
commit 47d7fbb7737dc45e037477d5f8fc38f597fe33c9
Author: Chaoyi Chen <chaoyi.chen at rock-chips.com>

drm/rockchip: cdn-dp: Add multiple bridges to support PHY port selection

This patch adds support for multiple bridges in the Rockchip CDN-DP controller 
to allow users to flexibly select the output port between two USB/DP combo 
PHYs. It introduces independent encoders and bridges for each PHY port based 
on the DRM AUX HPD bridge, requiring proper DT configuration.

[ ... ]

> diff --git a/drivers/gpu/drm/rockchip/cdn-dp-core.c b/drivers/gpu/drm/rockchip/cdn-dp-core.c
> --- a/drivers/gpu/drm/rockchip/cdn-dp-core.c
> +++ b/drivers/gpu/drm/rockchip/cdn-dp-core.c

[ ... ]

> @@ -192,14 +193,27 @@ static int cdn_dp_get_sink_count(struct cdn_dp_device *dp, u8 *sink_count)
>  static struct cdn_dp_port *cdn_dp_connected_port(struct cdn_dp_device *dp)
>  {
>  	struct cdn_dp_port *port;
> -	int i, lanes;
> +	int i, lanes[MAX_PHY];
>  
>  	for (i = 0; i < dp->ports; i++) {
>  		port = dp->port[i];
> -		lanes = cdn_dp_get_port_lanes(port);
> -		if (lanes)
> +		lanes[i] = cdn_dp_get_port_lanes(port);
> +		if (!dp->next_bridge_valid)
>  			return port;

[Severity: High]
Does this early return unconditionally bypass the connection state evaluation
for extcon setups?

If !dp->next_bridge_valid evaluates to true, it looks like this will return
dp->port[0] on the first iteration without verifying whether the lanes
indicate an active connection, which would break hot-plug detection for
extcon-based devices.

>  	}
> +
> +	if (dp->next_bridge_valid) {
> +		/* If more than one port is available, pick the last active port */
> +		if (dp->active_port >= 0 && lanes[dp->active_port])
> +			return dp->port[dp->active_port];
> +
> +		/* If the last active port is not available, pick an available port in order */
> +		for (i = 0; i < dp->bridge_count; i++) {
> +			if (lanes[i])
> +				return dp->port[i];
> +		}

[Severity: High]
Could this loop read uninitialized stack memory?

If the device tree defines more bridge endpoints in dp->bridge_count than the
number of physical PHY ports in dp->ports, this loop iterates past the
initialized bounds of the lanes array.

> +	}
> +
>  	return NULL;
>  }

[ ... ]

> @@ -254,12 +268,45 @@ static const struct drm_edid *
>  cdn_dp_bridge_edid_read(struct drm_bridge *bridge, struct drm_connector *connector)
>  {
>  	struct cdn_dp_device *dp = bridge_to_dp(bridge);
> -	const struct drm_edid *drm_edid;
> +	struct cdn_dp_bridge *dp_bridge = bridge_to_dp_bridge(bridge);
> +	struct cdn_dp_port *port = dp->port[dp_bridge->id];

[Severity: High]
Could this also lead to a NULL pointer dereference?

If dp_bridge->id exceeds the configured PHY ports, dp->port will be NULL.
The subsequent check for port->phy_enabled below will then dereference this
NULL pointer.

> +	struct cdn_dp_port *prev_port = NULL;
> +	const struct drm_edid *drm_edid = NULL;

[ ... ]

> +static int cdn_dp_switch_port(struct cdn_dp_device *dp, struct cdn_dp_port *prev_port,
> +			      struct cdn_dp_port *port)
> +{
> +	int ret;

[ ... ]

> +	dp->lanes = port->lanes;
> +
> +	if (!cdn_dp_check_link_status(dp)) {
> +		dev_info(dp->dev, "Connected with sink; re-train link\n");
> +
> +		ret = cdn_dp_train_link(dp);
> +		if (ret) {
> +			dev_err(dp->dev, "Training link failed: %d\n", ret);
> +			goto out;
> +		}
> +
> +		ret = cdn_dp_set_video_status(dp, CONTROL_VIDEO_IDLE);
> +		if (ret) {
> +			dev_err(dp->dev, "Failed to idle video %d\n", ret);
> +			goto out;
> +		}
> +
> +		ret = cdn_dp_config_video(dp);

[Severity: High]
Will this video configuration fail when called from the EDID read path?

cdn_dp_switch_port() is invoked from cdn_dp_bridge_edid_read() where no
valid display mode has been established yet. Since dp->mode.clock is likely
zero at this point, cdn_dp_config_video() might fail with an invalid argument
or divide-by-zero, which would break the port switch entirely.

> +		if (ret)
> +			dev_err(dp->dev, "Failed to configure video: %d\n", ret);
> +	}
> +
> +out:
> +	return ret;
> +}

[ ... ]

> @@ -600,6 +693,9 @@ static void cdn_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_at
>  
>  	mutex_lock(&dp->lock);
>  
> +	if (dp->next_bridge_valid)
> +		dp->active_port = dp_bridge->id;
> +
>  	ret = cdn_dp_enable(dp);

[Severity: High]
Is there missing mutual exclusion to prevent simultaneous enablement of
multiple bridges sharing this single DP controller?

Since the driver creates multiple DRM connectors (one per PHY port) that
share a single physical DP hardware block, without an atomic_check to ensure
only one connector is active at a time, userspace might try to simultaneously
enable CRTCs on multiple connectors. Both could race to program the shared
dp->regs and GRF registers.

>  	if (ret) {
>  		DRM_DEV_ERROR(dp->dev, "Failed to enable bridge %d\n",

[ ... ]

> -static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
> +static int cdn_bridge_add(struct device *dev,
> +			  struct drm_bridge *bridge,
> +			  struct drm_bridge *next_bridge,
> +			  struct drm_encoder *encoder)
>  {
>  	struct cdn_dp_device *dp = dev_get_drvdata(dev);
> -	struct drm_encoder *encoder;
> +	struct drm_device *drm_dev = dp->drm_dev;
> +	struct drm_bridge *last_bridge __free(drm_bridge_put) = NULL;
>  	struct drm_connector *connector;
> -	struct cdn_dp_port *port;
> -	struct drm_device *drm_dev = data;
> -	int ret, i;
> -
> -	ret = cdn_dp_parse_dt(dp);
> -	if (ret < 0)
> -		return ret;
> -
> -	dp->drm_dev = drm_dev;
> -	dp->connected = false;
> -	dp->active = false;
> -	dp->active_port = -1;
> -	dp->fw_loaded = false;
> -
> -	INIT_WORK(&dp->event_work, cdn_dp_pd_event_work);
> -
> -	encoder = &dp->encoder.encoder;
> +	int ret;
>  
>  	encoder->possible_crtcs = drm_of_find_possible_crtcs(drm_dev,
>  							     dev->of_node);
> @@ -1003,26 +1106,35 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
>  
>  	drm_encoder_helper_add(encoder, &cdn_dp_encoder_helper_funcs);
>  
> -	dp->bridge.ops =
> -			DRM_BRIDGE_OP_DETECT |
> -			DRM_BRIDGE_OP_EDID |
> -			DRM_BRIDGE_OP_HPD |
> -			DRM_BRIDGE_OP_DP_AUDIO;
> -	dp->bridge.of_node = dp->dev->of_node;
> -	dp->bridge.type = DRM_MODE_CONNECTOR_DisplayPort;
> -	dp->bridge.hdmi_audio_dev = dp->dev;
> -	dp->bridge.hdmi_audio_max_i2s_playback_channels = 8;
> -	dp->bridge.hdmi_audio_spdif_playback = 1;
> -	dp->bridge.hdmi_audio_dai_port = -1;
> -
> -	ret = devm_drm_bridge_add(dev, &dp->bridge);
> +	bridge->ops =
> +		DRM_BRIDGE_OP_DETECT |
> +		DRM_BRIDGE_OP_EDID |
> +		DRM_BRIDGE_OP_HPD |
> +		DRM_BRIDGE_OP_DP_AUDIO;
> +	bridge->of_node = dp->dev->of_node;
> +	bridge->type = DRM_MODE_CONNECTOR_DisplayPort;
> +	bridge->hdmi_audio_dev = dp->dev;
> +	bridge->hdmi_audio_max_i2s_playback_channels = 8;
> +	bridge->hdmi_audio_spdif_playback = 1;
> +	bridge->hdmi_audio_dai_port = -1;
> +
> +	ret = devm_drm_bridge_add(dev, bridge);

[Severity: High]
Will using devres-managed memory allocation and registration inside a
component bind callback cause memory leaks?

Because cdn_dp_bind() is a component bind callback, the devm lifecycle
is tied to the parent device which was bound during probe. When the
component is unbound, the devm cleanup does not fire. Upon rebinding,
new memory is allocated and new bridges are added to the global list,
leaking the old ones and leaving dangling objects.

This applies to devm_drm_bridge_alloc() usage below as well.

>  	if (ret)
>  		return ret;

[ ... ]

> +static int cdn_dp_parse_next_bridge_dt(struct cdn_dp_device *dp)
> +{
> +	struct device_node *np = dp->dev->of_node;
> +	struct device_node *port __free(device_node) = of_graph_get_port_by_id(np, 1);
> +	struct drm_bridge *bridge;
> +	int count = 0;
> +	int ret = 0;
> +	int i;

[ ... ]

> +	/* One endpoint may correspond to one next bridge. */
> +	for_each_of_graph_port_endpoint(port, dp_ep) {
> +		struct device_node *next_bridge_node __free(device_node) =
> +			of_graph_get_remote_port_parent(dp_ep);
> +
> +		bridge = of_drm_find_bridge(next_bridge_node);
> +		if (!bridge) {
> +			ret = -EPROBE_DEFER;
> +			goto out;
> +		}
> +
> +		dp->next_bridge_valid = true;
> +		dp->next_bridge_list[count] = drm_bridge_get(bridge);
> +		count++;

[Severity: High]
Can this loop overflow the next_bridge_list array?

If the device tree specifies more than MAX_PHY (2) endpoints, this
unconditionally increments count and writes past the bounds of the array,
which would corrupt the cdn_dp_device structure.

> +	}
> +
> +out:
> +	dp->bridge_count = count ? count : 1;
> +	return ret;
> +}
> +
> +static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
> +{
> +	struct cdn_dp_device *dp = dev_get_drvdata(dev);

[ ... ]

> +	for (i = 0; i < dp->bridge_count; i++) {
> +		encoder = &dp->bridge_list[i]->encoder.encoder;
> +		bridge = &dp->bridge_list[i]->bridge;
> +		next_bridge = dp->next_bridge_list[i];
> +		ret = cdn_bridge_add(dev, bridge, next_bridge, encoder);

[Severity: High]
Does this error path leave an initialized encoder hanging without cleanup?

If cdn_bridge_add() succeeds at drm_simple_encoder_init() but fails on a
subsequent step like drm_bridge_connector_init(), it returns an error but
leaves the encoder initialized and attached to the drm_dev. 

Additionally, if the loop here fails on the second iteration and jumps to
out, it frees resources but leaves the first successfully initialized encoder
still attached to the drm_dev. When the DRM core later traverses its encoder
lists, this could result in a use-after-free.

> +		if (ret)
> +			goto out;
> +	}
> +
> +	INIT_WORK(&dp->event_work, cdn_dp_pd_event_work);

[Severity: Medium]
Is it safe to initialize this work struct after the bridge has been exposed
to external subsystems?

The bridges are created and attached to the global chain just prior to this,
making them visible. If an external bridge or connected sink fires an HPD
notification immediately, cdn_dp_bridge_hpd_notify() could execute and call
schedule_work() before INIT_WORK() has run, potentially causing a crash.

>  	for (i = 0; i < dp->ports; i++) {
>  		port = dp->port[i];
>  
> @@ -1043,7 +1250,7 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
>  		if (ret) {
>  			DRM_DEV_ERROR(dev,
>  				      "register EXTCON_DISP_DP notifier err\n");
> -			return ret;
> +			goto out;

[Severity: High]
This is a pre-existing issue, but does using devm_extcon_register_notifier()
in a component bind callback cause notifier chain corruption?

Similar to the bridge devm issue, since the device isn't torn down on
component unbind, the devres cleanup never fires. On a subsequent re-bind,
it registers the same notifier block again, which corrupts the linked list
of the extcon notifier chain.

>  		}
>  	}
>  
> @@ -1052,15 +1259,29 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
>  	schedule_work(&dp->event_work);
>  
>  	return 0;
> +
> +out:
> +	for (i = 0; i < MAX_PHY; i++)
> +		drm_bridge_put(dp->next_bridge_list[i]);

[Severity: High]
Could this error path cause a double-put or use-after-free on bind retries?

The dp structure is allocated in probe, so it persists across component
bind/unbind cycles. When dropping references here, the pointers in
dp->next_bridge_list are not set to NULL. If a subsequent bind retry fails,
this error path might execute again and call drm_bridge_put() on the already
freed pointers.

> +
> +	return ret;
>  }
>  
>  static void cdn_dp_unbind(struct device *dev, struct device *master, void *data)
>  {
>  	struct cdn_dp_device *dp = dev_get_drvdata(dev);
> -	struct drm_encoder *encoder = &dp->encoder.encoder;
> +	struct drm_encoder *encoder;
> +	int i;
>  
>  	cancel_work_sync(&dp->event_work);
> -	encoder->funcs->destroy(encoder);
> +	for (i = 0; i < dp->bridge_count; i++) {
> +		encoder = &dp->bridge_list[i]->encoder.encoder;
> +		encoder->funcs->destroy(encoder);
> +		drm_bridge_put(dp->next_bridge_list[i]);
> +	}
> +
> +	for (i = 0; i < MAX_PHY; i++)
> +		drm_edid_free(dp->edid_cache[i]);

[Severity: High]
Does this leave stale EDID pointers in the cache?

This frees the cached EDIDs but fails to set the pointers to NULL. Because
the dp context persists across component unbind/rebind, a subsequent EDID
read via cdn_dp_bridge_edid_read() might attempt to free the same stale
pointer again before replacing it, corrupting memory.

>  
>  	pm_runtime_disable(dev);
>  	if (dp->fw_loaded)

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/20260804070730.68-1-kernel at airkyi.com?part=7


From loic.poulain at oss.qualcomm.com  Tue Aug  4 00:55:12 2026
From: loic.poulain at oss.qualcomm.com (Loic Poulain)
Date: Tue, 4 Aug 2026 09:55:12 +0200
Subject: [PATCH v15 2/2] phy: qcom-mipi-csi2: Add a CSI2 MIPI DPHY driver
In-Reply-To: <20260730-x1e-csi2-phy-v15-2-f284131de1fa@linaro.org>
References: <20260730-x1e-csi2-phy-v15-0-f284131de1fa@linaro.org> <20260730-x1e-csi2-phy-v15-2-f284131de1fa@linaro.org>
Message-ID: <CAFEp6-0S2eKAeYrAOUnsE-aXTkOdEJm7BGqKTprmVEQL24urOw@mail.gmail.com>

On Thu, Jul 30, 2026 at 2:14?PM Bryan O'Donoghue
<bryan.odonoghue at linaro.org> wrote:
>
> Add a new MIPI CSI2 driver in DPHY mode initially. The entire set of
> existing CAMSS CSI PHY init sequences are imported in order to save time
> and effort in later patches.
>
> The following devices are supported in this drop:
> "qcom,x1e80100-csi2-phy"
>
> In-line with other PHY drivers the process node is included in the name.
> Data-lane and clock lane positioning and polarity selection via newly
> amended struct phy_configure_opts_mipi_dphy{} is supported.
>
> The Qualcomm 3PH class of PHYs can do both DPHY and CPHY mode. For now only
> DPHY is supported.
>
> In porting some of the logic over from camss-csiphy*.c to here its also
> possible to rationalise some of the code.
>
> In particular use of regulator_bulk and clk_bulk as well as dropping the
> seemingly useless and unused interrupt handler.
>
> The PHY sequences and a lot of the logic that goes with them are well
> proven in CAMSS and mature so the main thing to watch out for here is how
> to get the right sequencing of regulators, clocks and register-writes.
>
> The register init sequence table is imported verbatim from the existing
> CAMSS csiphy driver. A follow-up series will rework the table to extract
> the repetitive per-lane pattern into a loop.
>
> Signed-off-by: Bryan O'Donoghue <bryan.odonoghue at linaro.org>

Reviewed-by: Loic Poulain <loic.poulain at oss.qualcomm.com>

> ---
>  MAINTAINERS                                        |  10 +
>  drivers/phy/qualcomm/Kconfig                       |  15 +
>  drivers/phy/qualcomm/Makefile                      |   5 +
>  drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c | 385 +++++++++++++++++
>  drivers/phy/qualcomm/phy-qcom-mipi-csi2-core.c     | 460 +++++++++++++++++++++
>  drivers/phy/qualcomm/phy-qcom-mipi-csi2.h          |  97 +++++
>  6 files changed, 972 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 15011f5752a99..a203b41475ea4 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -22296,6 +22296,16 @@ S:     Maintained
>  F:     Documentation/devicetree/bindings/media/qcom,*-iris.yaml
>  F:     drivers/media/platform/qcom/iris/
>
> +QUALCOMM MIPI CSI2 PHY DRIVER
> +M:     Bryan O'Donoghue <bod at kernel.org>
> +L:     linux-phy at lists.infradead.org
> +L:     linux-media at vger.kernel.org
> +L:     linux-arm-msm at vger.kernel.org
> +S:     Maintained
> +F:     Documentation/devicetree/bindings/phy/qcom,*-csi2-phy.yaml
> +F:     drivers/phy/qualcomm/phy-qcom-mipi-csi2*.c
> +F:     drivers/phy/qualcomm/phy-qcom-mipi-csi2*.h
> +
>  QUALCOMM NAND CONTROLLER DRIVER
>  M:     Manivannan Sadhasivam <mani at kernel.org>
>  L:     linux-mtd at lists.infradead.org
> diff --git a/drivers/phy/qualcomm/Kconfig b/drivers/phy/qualcomm/Kconfig
> index 60a0ead127fa9..cd6959e69bef7 100644
> --- a/drivers/phy/qualcomm/Kconfig
> +++ b/drivers/phy/qualcomm/Kconfig
> @@ -28,6 +28,21 @@ config PHY_QCOM_EDP
>           Enable this driver to support the Qualcomm eDP PHY found in various
>           Qualcomm chipsets.
>
> +config PHY_QCOM_MIPI_CSI2
> +       tristate "Qualcomm MIPI CSI2 PHY driver"
> +       depends on ARCH_QCOM || COMPILE_TEST
> +       depends on OF
> +       depends on PM
> +       depends on PM_OPP
> +       depends on COMMON_CLK
> +       select GENERIC_PHY
> +       select GENERIC_PHY_MIPI_DPHY
> +       help
> +         Enable this to support the MIPI CSI2 PHY driver found in various
> +         Qualcomm chipsets. This PHY is used to connect MIPI CSI2
> +         camera sensors to the CSI Decoder in the Qualcomm Camera Subsystem
> +         CAMSS.
> +
>  config PHY_QCOM_IPQ4019_USB
>         tristate "Qualcomm IPQ4019 USB PHY driver"
>         depends on OF && (ARCH_QCOM || COMPILE_TEST)
> diff --git a/drivers/phy/qualcomm/Makefile b/drivers/phy/qualcomm/Makefile
> index b71a6a0bed3f1..382cb594b06b6 100644
> --- a/drivers/phy/qualcomm/Makefile
> +++ b/drivers/phy/qualcomm/Makefile
> @@ -6,6 +6,11 @@ obj-$(CONFIG_PHY_QCOM_IPQ4019_USB)     += phy-qcom-ipq4019-usb.o
>  obj-$(CONFIG_PHY_QCOM_IPQ806X_SATA)    += phy-qcom-ipq806x-sata.o
>  obj-$(CONFIG_PHY_QCOM_M31_USB)         += phy-qcom-m31.o
>  obj-$(CONFIG_PHY_QCOM_M31_EUSB)                += phy-qcom-m31-eusb2.o
> +
> +phy-qcom-mipi-csi2-objs                        += phy-qcom-mipi-csi2-core.o \
> +                                          phy-qcom-mipi-csi2-3ph-dphy.o
> +obj-$(CONFIG_PHY_QCOM_MIPI_CSI2)       += phy-qcom-mipi-csi2.o
> +
>  obj-$(CONFIG_PHY_QCOM_PCIE2)           += phy-qcom-pcie2.o
>
>  obj-$(CONFIG_PHY_QCOM_QMP_COMBO)       += phy-qcom-qmp-combo.o phy-qcom-qmp-usbc.o
> diff --git a/drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c b/drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c
> new file mode 100644
> index 0000000000000..966d79c98f9b2
> --- /dev/null
> +++ b/drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c
> @@ -0,0 +1,385 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Qualcomm MSM Camera Subsystem - CSIPHY Module 3phase v1.0
> + *
> + * Copyright (c) 2011-2015, The Linux Foundation. All rights reserved.
> + * Copyright (C) 2016-2026 Linaro Ltd.
> + */
> +
> +#include <linux/delay.h>
> +#include <linux/interrupt.h>
> +#include <linux/io.h>
> +#include <linux/time64.h>
> +
> +#include "phy-qcom-mipi-csi2.h"
> +
> +#define CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(offset, n)     ((offset) + 0x4 * (n))
> +#define CSIPHY_3PH_CMN_CSI_COMMON_CTRL0_PHY_SW_RESET   BIT(0)
> +#define CSIPHY_3PH_CMN_CSI_COMMON_CTRL5_CLK_ENABLE     BIT(7)
> +#define CSIPHY_3PH_CMN_CSI_COMMON_CTRL6_COMMON_PWRDN_B BIT(0)
> +#define CSIPHY_3PH_CMN_CSI_COMMON_CTRL6_SHOW_REV_ID    BIT(1)
> +#define CSIPHY_3PH_CMN_CSI_COMMON_CTRL10_IRQ_CLEAR_CMD BIT(0)
> +#define CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(offset, n)   ((offset) + 0xb0 + 0x4 * (n))
> +
> +#define CSIPHY_2PH_LN_CSI_2PHASE_CTRL9n(n)             ((0x200 * (n)) + 0x24)
> +
> +/*
> + * 3 phase CSI has 19 common status regs with only 0-10 being used
> + * and 11-18 being reserved.
> + */
> +#define CSI_COMMON_STATUS_NUM                          11
> +/*
> + * There are a number of common control registers
> + * The offset to clear the CSIPHY IRQ status starts @ 22
> + * So to clear CSI_COMMON_STATUS0 this is CSI_COMMON_CONTROL22, STATUS1 is
> + * CONTROL23 and so on
> + */
> +#define CSI_CTRL_STATUS_INDEX                          22
> +
> +/*
> + * There are 43 COMMON_CTRL registers with regs after # 33 being reserved
> + */
> +#define CSI_CTRL_MAX                                   33
> +
> +#define CSIPHY_DEFAULT_PARAMS                          0
> +#define CSIPHY_SETTLE_CNT_LOWER_BYTE                   2
> +#define CSIPHY_SKEW_CAL                                        7
> +
> +/* 4nm 2PH v 2.1.2 2p5Gbps 4 lane DPHY mode */
> +static const struct
> +mipi_csi2phy_lane_regs lane_regs_x1e80100[] = {
> +       /* Power up lanes 2ph mode */
> +       {.reg_addr = 0x101c, .reg_data = 0x7a, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x1018, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +
> +       {.reg_addr = 0x0094, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x00a0, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0090, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0098, .reg_data = 0x08, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0094, .reg_data = 0x07, .delay_us = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0030, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0000, .reg_data = 0x8e, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0038, .reg_data = 0xfe, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x002c, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0034, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x001c, .reg_data = 0x0a, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0014, .reg_data = 0x60, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x003c, .reg_data = 0xb8, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0004, .reg_data = 0x0c, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0020, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0008, .reg_data = 0x10, .param_type = CSIPHY_SETTLE_CNT_LOWER_BYTE},
> +       {.reg_addr = 0x0010, .reg_data = 0x52, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0094, .reg_data = 0xd7, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x005c, .reg_data = 0x00, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x0060, .reg_data = 0xbd, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x0064, .reg_data = 0x7f, .param_type = CSIPHY_SKEW_CAL},
> +
> +       {.reg_addr = 0x0e94, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0ea0, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e90, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e98, .reg_data = 0x08, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e94, .reg_data = 0x07, .delay_us =  0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e30, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e28, .reg_data = 0x04, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e00, .reg_data = 0x80, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e0c, .reg_data = 0xff, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e38, .reg_data = 0x1f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e2c, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e34, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e1c, .reg_data = 0x0a, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e14, .reg_data = 0x60, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e3c, .reg_data = 0xb8, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e04, .reg_data = 0x0c, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e20, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0e08, .reg_data = 0x10, .param_type = CSIPHY_SETTLE_CNT_LOWER_BYTE},
> +       {.reg_addr = 0x0e10, .reg_data = 0x52, .param_type = CSIPHY_DEFAULT_PARAMS},
> +
> +       {.reg_addr = 0x0494, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x04a0, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0490, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0498, .reg_data = 0x08, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0494, .reg_data = 0x07, .delay_us =  0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0430, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0400, .reg_data = 0x8e, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0438, .reg_data = 0xfe, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x042c, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0434, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x041c, .reg_data = 0x0a, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0414, .reg_data = 0x60, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x043c, .reg_data = 0xb8, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0404, .reg_data = 0x0c, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0420, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0408, .reg_data = 0x10, .param_type = CSIPHY_SETTLE_CNT_LOWER_BYTE},
> +       {.reg_addr = 0x0410, .reg_data = 0x52, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0494, .reg_data = 0xd7, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x045c, .reg_data = 0x00, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x0460, .reg_data = 0xbd, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x0464, .reg_data = 0x7f, .param_type = CSIPHY_SKEW_CAL},
> +
> +       {.reg_addr = 0x0894, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x08a0, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0890, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0898, .reg_data = 0x08, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0894, .reg_data = 0x07, .delay_us =  0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0830, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0800, .reg_data = 0x8e, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0838, .reg_data = 0xfe, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x082c, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0834, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x081c, .reg_data = 0x0a, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0814, .reg_data = 0x60, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x083c, .reg_data = 0xb8, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0804, .reg_data = 0x0c, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0820, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0808, .reg_data = 0x10, .param_type = CSIPHY_SETTLE_CNT_LOWER_BYTE},
> +       {.reg_addr = 0x0810, .reg_data = 0x52, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0894, .reg_data = 0xd7, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x085c, .reg_data = 0x00, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x0860, .reg_data = 0xbd, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x0864, .reg_data = 0x7f, .param_type = CSIPHY_SKEW_CAL},
> +
> +       {.reg_addr = 0x0c94, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0ca0, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c90, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c98, .reg_data = 0x08, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c94, .reg_data = 0x07, .delay_us =  0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c30, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c00, .reg_data = 0x8e, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c38, .reg_data = 0xfe, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c2c, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c34, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c1c, .reg_data = 0x0a, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c14, .reg_data = 0x60, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c3c, .reg_data = 0xb8, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c04, .reg_data = 0x0c, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c20, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c08, .reg_data = 0x10, .param_type = CSIPHY_SETTLE_CNT_LOWER_BYTE},
> +       {.reg_addr = 0x0c10, .reg_data = 0x52, .param_type = CSIPHY_DEFAULT_PARAMS},
> +       {.reg_addr = 0x0c94, .reg_data = 0xd7, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x0c5c, .reg_data = 0x00, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x0c60, .reg_data = 0xbd, .param_type = CSIPHY_SKEW_CAL},
> +       {.reg_addr = 0x0c64, .reg_data = 0x7f, .param_type = CSIPHY_SKEW_CAL},
> +};
> +
> +static inline const struct mipi_csi2phy_device_regs *
> +csi2phy_dev_to_regs(struct mipi_csi2phy_device *csi2phy)
> +{
> +       return &csi2phy->soc_cfg->reg_info;
> +}
> +
> +static void phy_qcom_mipi_csi2_hw_version_read(struct mipi_csi2phy_device *csi2phy)
> +{
> +       const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
> +       u32 tmp;
> +
> +       writel(CSIPHY_3PH_CMN_CSI_COMMON_CTRL6_SHOW_REV_ID, csi2phy->base +
> +              CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 6));
> +
> +       tmp = readl_relaxed(csi2phy->base +
> +                           CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(regs->common_regs_offset, 12));
> +       csi2phy->hw_version = tmp;
> +
> +       tmp = readl_relaxed(csi2phy->base +
> +                           CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(regs->common_regs_offset, 13));
> +       csi2phy->hw_version |= (tmp << 8) & 0xFF00;
> +
> +       tmp = readl_relaxed(csi2phy->base +
> +                           CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(regs->common_regs_offset, 14));
> +       csi2phy->hw_version |= (tmp << 16) & 0xFF0000;
> +
> +       tmp = readl_relaxed(csi2phy->base +
> +                           CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(regs->common_regs_offset, 15));
> +       csi2phy->hw_version |= (tmp << 24) & 0xFF000000;
> +
> +       dev_dbg_once(csi2phy->dev, "CSIPHY 3PH HW Version = 0x%08x\n", csi2phy->hw_version);
> +}
> +
> +/*
> + * phy_qcom_mipi_csi2_reset - Perform software reset on CSIPHY module
> + * @phy_qcom_mipi_csi2: CSIPHY device
> + */
> +static void phy_qcom_mipi_csi2_reset(struct mipi_csi2phy_device *csi2phy)
> +{
> +       const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
> +
> +       writel(CSIPHY_3PH_CMN_CSI_COMMON_CTRL0_PHY_SW_RESET,
> +              csi2phy->base + CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 0));
> +       usleep_range(5000, 8000);
> +       writel(0x0, csi2phy->base +
> +              CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 0));
> +}
> +
> +/*
> + * phy_qcom_mipi_csi2_settle_cnt_calc - Calculate settle count value
> + *
> + * Helper function to calculate settle count value. This is
> + * based on the CSI2 T_hs_settle parameter which in turn
> + * is calculated based on the CSI2 transmitter link frequency.
> + *
> + * Return settle count.
> + */
> +static u8 phy_qcom_mipi_csi2_settle_cnt_calc(s64 link_freq, u32 timer_clk_rate)
> +{
> +       u32 t_hs_prepare_max_ps;
> +       u32 timer_period_ps;
> +       u32 t_hs_settle_ps;
> +       u8 settle_cnt;
> +       u32 ui_ps;
> +
> +       ui_ps = div64_u64(PSEC_PER_SEC, link_freq);
> +       ui_ps /= 2;
> +       t_hs_prepare_max_ps = 85000 + 6 * ui_ps;
> +       t_hs_settle_ps = t_hs_prepare_max_ps;
> +
> +       timer_period_ps = div_u64(PSEC_PER_SEC, timer_clk_rate);
> +
> +       if ((t_hs_settle_ps / timer_period_ps) < 6)
> +               return 0;
> +
> +       settle_cnt = t_hs_settle_ps / timer_period_ps - 6;
> +
> +       return settle_cnt;
> +}
> +
> +static void
> +phy_qcom_mipi_csi2_gen2_config_lanes(struct mipi_csi2phy_device *csi2phy,
> +                                    u8 settle_cnt)
> +{
> +       const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
> +       const struct mipi_csi2phy_lane_regs *r = regs->init_seq;
> +       int i, array_size = regs->lane_array_size;
> +       u32 val;
> +
> +       for (i = 0; i < array_size; i++, r++) {
> +               switch (r->param_type) {
> +               case CSIPHY_SETTLE_CNT_LOWER_BYTE:
> +                       val = settle_cnt & 0xff;
> +                       break;
> +               case CSIPHY_SKEW_CAL:
> +                       /* TODO: support application of skew from dt flag */
> +                       continue;
> +               default:
> +                       val = r->reg_data;
> +                       break;
> +               }
> +               writel(val, csi2phy->base + r->reg_addr);
> +               if (r->delay_us)
> +                       udelay(r->delay_us);
> +       }
> +}
> +
> +static int phy_qcom_mipi_csi2_lanes_enable(struct mipi_csi2phy_device *csi2phy,
> +                                          struct mipi_csi2phy_stream_cfg *cfg)
> +{
> +       const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
> +       struct mipi_csi2phy_lanes_cfg *lane_cfg = &cfg->lane_cfg;
> +       u8 settle_cnt;
> +       u8 val;
> +       int i;
> +
> +       if (cfg->link_freq <= 0)
> +               return -EINVAL;
> +
> +       settle_cnt = phy_qcom_mipi_csi2_settle_cnt_calc(cfg->link_freq, csi2phy->timer_clk_rate);
> +       if (!settle_cnt)
> +               return -ENODEV;
> +
> +       /*
> +        * CSI_COMMON_CTRL5 is a physical lane power-up bitmap:
> +        * - Bits [0,2,4,6] ? D-PHY data lanes(LN0, LN2, LN4, LN6)
> +        * - Bits [1,3,5] ? C-PHY trio lanes(LN1, LN3, LN5)
> +        * - Bit [7] ? D-PHY clock lane(LNCK) dedicated clock enable
> +        */
> +       val = BIT(lane_cfg->clk.pos);
> +       for (i = 0; i < cfg->num_data_lanes; i++)
> +               val |= BIT(lane_cfg->data[i].pos * 2);
> +
> +       writel(val, csi2phy->base +
> +              CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 5));
> +
> +       /* Lane configuration for polarity @ CSIPHY-base + CTRL9 */
> +       for (i = 0; i < cfg->num_data_lanes; i++) {
> +               if (lane_cfg->data[i].pol) {
> +                       u8 pos = lane_cfg->data[i].pos;
> +
> +                       writel(BIT(2), csi2phy->base + CSIPHY_2PH_LN_CSI_2PHASE_CTRL9n(pos * 2));
> +               }
> +       }
> +
> +       if (lane_cfg->clk.pol)
> +               writel(BIT(2), csi2phy->base + CSIPHY_2PH_LN_CSI_2PHASE_CTRL9n(lane_cfg->clk.pos));
> +
> +       val = CSIPHY_3PH_CMN_CSI_COMMON_CTRL6_COMMON_PWRDN_B;
> +       writel(val, csi2phy->base +
> +              CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 6));
> +
> +       val = 0x02;
> +       writel(val, csi2phy->base +
> +              CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 7));
> +
> +       val = 0x00;
> +       writel(val, csi2phy->base +
> +              CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 0));
> +
> +       phy_qcom_mipi_csi2_gen2_config_lanes(csi2phy, settle_cnt);
> +
> +       /* IRQ_MASK registers - disable all interrupts */
> +       for (i = CSI_COMMON_STATUS_NUM; i < CSI_CTRL_STATUS_INDEX; i++) {
> +               writel(0, csi2phy->base +
> +                      CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, i));
> +       }
> +
> +       return 0;
> +}
> +
> +static void
> +phy_qcom_mipi_csi2_lanes_disable(struct mipi_csi2phy_device *csi2phy,
> +                                struct mipi_csi2phy_stream_cfg *cfg)
> +{
> +       const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
> +
> +       writel(0, csi2phy->base +
> +              CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 5));
> +
> +       writel(0, csi2phy->base +
> +              CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 6));
> +}
> +
> +static const struct mipi_csi2phy_hw_ops phy_qcom_mipi_csi2_ops_3ph_1_0 = {
> +       .hw_version_read = phy_qcom_mipi_csi2_hw_version_read,
> +       .reset = phy_qcom_mipi_csi2_reset,
> +       .lanes_enable = phy_qcom_mipi_csi2_lanes_enable,
> +       .lanes_disable = phy_qcom_mipi_csi2_lanes_disable,
> +};
> +
> +static const char * const x1e_clks[] = {
> +       "core",
> +       "timer",
> +       "ahb"
> +};
> +
> +static const char * const x1e_supplies[] = {
> +       "vdda-0p9",
> +       "vdda-1p2"
> +};
> +
> +static struct mipi_csi2_genpd x1e_genpds[] = {
> +       { .name = "top", .scaled = false },
> +       { .name = "mmcx", .scaled = true },
> +       { .name = "mx", .scaled = true },
> +};
> +
> +const struct mipi_csi2phy_soc_cfg mipi_csi2_dphy_4nm_x1e = {
> +       .ops = &phy_qcom_mipi_csi2_ops_3ph_1_0,
> +       .reg_info = {
> +               .init_seq = lane_regs_x1e80100,
> +               .lane_array_size = ARRAY_SIZE(lane_regs_x1e80100),
> +               .common_regs_offset = 0x1000,
> +       },
> +       .supply_names = (const char **)x1e_supplies,
> +       .num_supplies = ARRAY_SIZE(x1e_supplies),
> +       .clk_names = (const char **)x1e_clks,
> +       .num_clk = ARRAY_SIZE(x1e_clks),
> +       .genpds = x1e_genpds,
> +       .num_genpds = ARRAY_SIZE(x1e_genpds),
> +};
> diff --git a/drivers/phy/qualcomm/phy-qcom-mipi-csi2-core.c b/drivers/phy/qualcomm/phy-qcom-mipi-csi2-core.c
> new file mode 100644
> index 0000000000000..02f34b7de05f5
> --- /dev/null
> +++ b/drivers/phy/qualcomm/phy-qcom-mipi-csi2-core.c
> @@ -0,0 +1,460 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright (c) 2026, Linaro Ltd.
> + */
> +
> +#include <dt-bindings/media/video-interfaces.h>
> +#include <linux/clk.h>
> +#include <linux/delay.h>
> +#include <linux/err.h>
> +#include <linux/io.h>
> +#include <linux/kernel.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/pm_opp.h>
> +#include <linux/phy/phy.h>
> +#include <linux/phy/phy-mipi-dphy.h>
> +#include <linux/platform_device.h>
> +#include <linux/pm_domain.h>
> +#include <linux/pm_runtime.h>
> +#include <linux/regmap.h>
> +#include <linux/regulator/consumer.h>
> +#include <linux/reset.h>
> +#include <linux/slab.h>
> +
> +#include "phy-qcom-mipi-csi2.h"
> +
> +static int
> +phy_qcom_mipi_csi2_set_clock_rates(struct mipi_csi2phy_device *csi2phy,
> +                                  s64 link_freq)
> +{
> +       struct device *dev = csi2phy->dev;
> +       unsigned long opp_rate = link_freq / 4;
> +       struct dev_pm_opp *opp;
> +       long timer_rate;
> +       int i, pstate;
> +       int ret;
> +
> +       opp = dev_pm_opp_find_freq_ceil(dev, &opp_rate);
> +       if (IS_ERR(opp)) {
> +               dev_err(csi2phy->dev, "Couldn't find ceiling for %lld Hz\n",
> +                       link_freq);
> +               return PTR_ERR(opp);
> +       }
> +
> +       pstate = 0;
> +       for (i = 0; i < csi2phy->pd_list->num_pds; i++) {
> +               unsigned int perf;
> +
> +               if (!csi2phy->soc_cfg->genpds[i].scaled)
> +                       continue;
> +
> +               perf = dev_pm_opp_get_required_pstate(opp, pstate);
> +               pstate += 1;
> +
> +               ret = dev_pm_genpd_set_performance_state(csi2phy->pd_list->pd_devs[i], perf);
> +               if (ret) {
> +                       dev_err(csi2phy->dev, "Couldn't set perf state %u\n",
> +                               perf);
> +                       dev_pm_opp_put(opp);
> +                       goto unset_pstate;
> +               }
> +       }
> +       dev_pm_opp_put(opp);
> +
> +       ret = dev_pm_opp_set_rate(dev, opp_rate);
> +       if (ret) {
> +               dev_err(csi2phy->dev, "dev_pm_opp_set_rate() fail\n");
> +               goto unset_opp_rate;
> +       }
> +
> +       timer_rate = clk_round_rate(csi2phy->timer_clk, link_freq / 4);
> +       if (timer_rate <= 0) {
> +               ret = -ENODEV;
> +               goto unset_opp_rate;
> +       }
> +
> +       ret = clk_set_rate(csi2phy->timer_clk, timer_rate);
> +       if (ret)
> +               goto unset_opp_rate;
> +
> +       csi2phy->timer_clk_rate = timer_rate;
> +
> +       return 0;
> +
> +unset_opp_rate:
> +       dev_pm_opp_set_rate(dev, 0);
> +
> +unset_pstate:
> +       while (i--) {
> +               if (!csi2phy->soc_cfg->genpds[i].scaled)
> +                       continue;
> +
> +               dev_pm_genpd_set_performance_state(csi2phy->pd_list->pd_devs[i], 0);
> +       }
> +
> +       return ret;
> +}
> +
> +static int phy_qcom_mipi_csi2_configure(struct phy *phy,
> +                                       union phy_configure_opts *opts)
> +{
> +       struct mipi_csi2phy_device *csi2phy = phy_get_drvdata(phy);
> +       struct phy_configure_opts_mipi_dphy *dphy_cfg = &opts->mipi_dphy;
> +       struct mipi_csi2phy_stream_cfg *stream_cfg = &csi2phy->stream_cfg;
> +       int ret;
> +
> +       ret = phy_mipi_dphy_config_validate(dphy_cfg);
> +       if (ret)
> +               return ret;
> +
> +       if (dphy_cfg->lanes < 1 || dphy_cfg->lanes > CSI2_MAX_DATA_LANES)
> +               return -EINVAL;
> +
> +       stream_cfg->link_freq = dphy_cfg->hs_clk_rate;
> +
> +       return 0;
> +}
> +
> +static int phy_qcom_mipi_csi2_set_mode(struct phy *phy,
> +                                      enum phy_mode mode, int submode)
> +{
> +       struct mipi_csi2phy_device *csi2phy = phy_get_drvdata(phy);
> +
> +       if (mode != PHY_MODE_MIPI_DPHY)
> +               return -EOPNOTSUPP;
> +
> +       csi2phy->phy_mode = mode;
> +       return 0;
> +}
> +
> +static int phy_qcom_mipi_csi2_power_on(struct phy *phy)
> +{
> +       struct mipi_csi2phy_device *csi2phy = phy_get_drvdata(phy);
> +       const struct mipi_csi2phy_hw_ops *ops = csi2phy->soc_cfg->ops;
> +       int i, ret;
> +
> +       ret = regulator_bulk_enable(csi2phy->soc_cfg->num_supplies,
> +                                   csi2phy->supplies);
> +       if (ret)
> +               return ret;
> +
> +       ret = pm_runtime_resume_and_get(csi2phy->dev);
> +       if (ret < 0)
> +               goto disable_regulators;
> +
> +       ret = phy_qcom_mipi_csi2_set_clock_rates(csi2phy, csi2phy->stream_cfg.link_freq);
> +       if (ret)
> +               goto poweroff_phy;
> +
> +       ret = clk_bulk_prepare_enable(csi2phy->soc_cfg->num_clk,
> +                                     csi2phy->clks);
> +       if (ret) {
> +               dev_err(csi2phy->dev, "failed to enable clocks, %d\n", ret);
> +               goto unset_rate;
> +       }
> +
> +       ops->reset(csi2phy);
> +
> +       ops->hw_version_read(csi2phy);
> +
> +       ret = ops->lanes_enable(csi2phy, &csi2phy->stream_cfg);
> +       if (ret)
> +               goto unset_clocks;
> +
> +       return 0;
> +
> +unset_clocks:
> +       clk_bulk_disable_unprepare(csi2phy->soc_cfg->num_clk,
> +                                  csi2phy->clks);
> +
> +unset_rate:
> +       dev_pm_opp_set_rate(csi2phy->dev, 0);
> +
> +       for (i = 0; i < csi2phy->pd_list->num_pds; i++) {
> +               if (!csi2phy->soc_cfg->genpds[i].scaled)
> +                       continue;
> +
> +               dev_pm_genpd_set_performance_state(csi2phy->pd_list->pd_devs[i], 0);
> +       }
> +
> +poweroff_phy:
> +       pm_runtime_put_sync(csi2phy->dev);
> +
> +disable_regulators:
> +       regulator_bulk_disable(csi2phy->soc_cfg->num_supplies,
> +                              csi2phy->supplies);
> +
> +       return ret;
> +}
> +
> +static int phy_qcom_mipi_csi2_power_off(struct phy *phy)
> +{
> +       struct mipi_csi2phy_device *csi2phy = phy_get_drvdata(phy);
> +       const struct mipi_csi2phy_hw_ops *ops = csi2phy->soc_cfg->ops;
> +       int i;
> +
> +       ops->lanes_disable(csi2phy, &csi2phy->stream_cfg);
> +
> +       clk_bulk_disable_unprepare(csi2phy->soc_cfg->num_clk,
> +                                  csi2phy->clks);
> +
> +       dev_pm_opp_set_rate(csi2phy->dev, 0);
> +
> +       for (i = 0; i < csi2phy->pd_list->num_pds; i++) {
> +               if (!csi2phy->soc_cfg->genpds[i].scaled)
> +                       continue;
> +
> +               dev_pm_genpd_set_performance_state(csi2phy->pd_list->pd_devs[i], 0);
> +       }
> +
> +       pm_runtime_put_sync(csi2phy->dev);
> +
> +       regulator_bulk_disable(csi2phy->soc_cfg->num_supplies,
> +                              csi2phy->supplies);
> +
> +       return 0;
> +}
> +
> +static const struct phy_ops phy_qcom_mipi_csi2_ops = {
> +       .configure      = phy_qcom_mipi_csi2_configure,
> +       .set_mode       = phy_qcom_mipi_csi2_set_mode,
> +       .power_on       = phy_qcom_mipi_csi2_power_on,
> +       .power_off      = phy_qcom_mipi_csi2_power_off,
> +       .owner          = THIS_MODULE,
> +};
> +
> +static int phy_qcom_mipi_csi2_attach_pm_domains(struct mipi_csi2phy_device *csi2phy)
> +{
> +       struct dev_pm_domain_attach_data pd_data = { 0 };
> +       const char **pd_names;
> +       int i;
> +
> +       pd_names = devm_kzalloc(csi2phy->dev,
> +                               sizeof(char *) * csi2phy->soc_cfg->num_genpds,
> +                               GFP_KERNEL);
> +       if (!pd_names)
> +               return -ENOMEM;
> +
> +       for (i = 0; i < csi2phy->soc_cfg->num_genpds; i++)
> +               pd_names[i] = csi2phy->soc_cfg->genpds[i].name;
> +
> +       pd_data.pd_names = pd_names;
> +       pd_data.num_pd_names = csi2phy->soc_cfg->num_genpds;
> +
> +       return devm_pm_domain_attach_list(csi2phy->dev, &pd_data,
> +                                         &csi2phy->pd_list);
> +}
> +
> +static int phy_qcom_mipi_csi2_parse_routing(struct mipi_csi2phy_device *csi2phy)
> +{
> +       struct mipi_csi2phy_stream_cfg *stream_cfg = &csi2phy->stream_cfg;
> +       u32 lane_polarities[CSI2_MAX_DATA_LANES + 1];
> +       u32 data_lanes[CSI2_MAX_DATA_LANES];
> +       struct device *dev = csi2phy->dev;
> +       struct fwnode_handle *ep;
> +       int num_polarities;
> +       int num_data_lanes;
> +       u32 bus_type;
> +       int i, ret;
> +
> +       ep = fwnode_graph_get_endpoint_by_id(dev_fwnode(dev), 0, 1, 0);
> +       if (ep) {
> +               fwnode_handle_put(ep);
> +               dev_err(dev, "DPHY split mode is not supported\n");
> +               return -EOPNOTSUPP;
> +       }
> +
> +       ep = fwnode_graph_get_endpoint_by_id(dev_fwnode(dev), 0, 0, 0);
> +       if (!ep) {
> +               dev_err(dev, "Missing port at 0\n");
> +               return -ENODEV;
> +       }
> +
> +       ret = fwnode_property_read_u32(ep, "bus-type", &bus_type);
> +       if (ret) {
> +               dev_err(dev, "Failed to read bus-type: %d\n", ret);
> +               goto out_put;
> +       }
> +
> +       if (bus_type != MEDIA_BUS_TYPE_CSI2_DPHY) {
> +               ret = -EOPNOTSUPP;
> +               dev_err(dev, "Unsupported bus-type %u\n", bus_type);
> +               goto out_put;
> +       }
> +
> +       num_data_lanes = fwnode_property_count_u32(ep, "data-lanes");
> +       if (num_data_lanes < 1 || num_data_lanes > CSI2_MAX_DATA_LANES) {
> +               ret = -EINVAL;
> +               dev_err(dev, "Invalid data-lanes count: %d\n", num_data_lanes);
> +               goto out_put;
> +       }
> +       stream_cfg->num_data_lanes = num_data_lanes;
> +
> +       ret = fwnode_property_read_u32_array(ep, "data-lanes", data_lanes,
> +                                            stream_cfg->num_data_lanes);
> +       if (ret) {
> +               dev_err(dev, "Failed to read data-lanes: %d\n", ret);
> +               goto out_put;
> +       }
> +
> +       /* lane-polarities: optional, up to num_data_lanes + 1 entries */
> +       memset(lane_polarities, 0x00, sizeof(lane_polarities));
> +       num_polarities = fwnode_property_count_u32(ep, "lane-polarities");
> +       if (num_polarities > 0) {
> +               if (num_polarities != stream_cfg->num_data_lanes + 1) {
> +                       ret = -EINVAL;
> +                       dev_err(dev, "clock+data-lane %d/polarities %d mismatch\n",
> +                               stream_cfg->num_data_lanes + 1, num_polarities);
> +                       goto out_put;
> +               }
> +
> +               ret = fwnode_property_read_u32_array(ep, "lane-polarities", lane_polarities,
> +                                                    num_polarities);
> +               if (ret) {
> +                       dev_err(dev, "Failed to read lane-polarities: %d\n", ret);
> +                       goto out_put;
> +               }
> +       }
> +
> +       csi2phy->stream_cfg.lane_cfg.clk.pos = CSI2_DEFAULT_CLK_LANE;
> +       csi2phy->stream_cfg.lane_cfg.clk.pol = lane_polarities[0];
> +
> +       for (i = 0; i < csi2phy->stream_cfg.num_data_lanes; i++) {
> +               if (data_lanes[i] >= CSI2_MAX_DATA_LANES) {
> +                       dev_err(dev, "Invalid lane %d\n", data_lanes[i]);
> +                       ret = -EINVAL;
> +                       goto out_put;
> +               }
> +               csi2phy->stream_cfg.lane_cfg.data[i].pos = data_lanes[i];
> +               csi2phy->stream_cfg.lane_cfg.data[i].pol = lane_polarities[i + 1];
> +       }
> +
> +       ret = 0;
> +
> +out_put:
> +       fwnode_handle_put(ep);
> +
> +       return ret;
> +}
> +
> +static int phy_qcom_mipi_csi2_probe(struct platform_device *pdev)
> +{
> +       unsigned int i, num_clk, num_supplies;
> +       struct mipi_csi2phy_device *csi2phy;
> +       struct phy_provider *phy_provider;
> +       struct device *dev = &pdev->dev;
> +       struct phy *generic_phy;
> +       int ret;
> +
> +       csi2phy = devm_kzalloc(dev, sizeof(*csi2phy), GFP_KERNEL);
> +       if (!csi2phy)
> +               return -ENOMEM;
> +
> +       csi2phy->dev = dev;
> +       dev_set_drvdata(dev, csi2phy);
> +
> +       csi2phy->soc_cfg = device_get_match_data(&pdev->dev);
> +
> +       if (!csi2phy->soc_cfg)
> +               return -EINVAL;
> +
> +       num_clk = csi2phy->soc_cfg->num_clk;
> +
> +       ret = phy_qcom_mipi_csi2_parse_routing(csi2phy);
> +       if (ret)
> +               return ret;
> +
> +       ret = phy_qcom_mipi_csi2_attach_pm_domains(csi2phy);
> +       if (ret < 0 || csi2phy->pd_list == NULL) {
> +               if (ret == 0)
> +                       ret = -ENODEV;
> +               return dev_err_probe(dev, ret, "Failed to attach power-domain list\n");
> +       }
> +
> +       ret = devm_pm_runtime_enable(dev);
> +       if (ret < 0)
> +               return dev_err_probe(dev, ret, "Failed to enable pm runtime\n");
> +
> +       ret = devm_clk_bulk_get_all(dev, &csi2phy->clks);
> +       if (ret < 0)
> +               return dev_err_probe(dev, ret, "Failed to get clocks\n");
> +
> +       if (num_clk != ret) {
> +               return dev_err_probe(dev, -ENODEV, "clock count %d expected %d\n",
> +                                    ret, num_clk);
> +       }
> +
> +       for (i = 0; i < num_clk; i++) {
> +               if (!csi2phy->clks[i].id)
> +                       return dev_err_probe(dev, -EINVAL, "Missing clock-names\n");
> +
> +               if (!strcmp(csi2phy->clks[i].id, "timer")) {
> +                       csi2phy->timer_clk = csi2phy->clks[i].clk;
> +                       break;
> +               }
> +       }
> +       if (!csi2phy->timer_clk)
> +               return dev_err_probe(dev, -ENODEV, "no timer clock\n");
> +
> +       ret = devm_pm_opp_set_clkname(dev, "core");
> +       if (ret)
> +               return dev_err_probe(dev, ret, "Failed to set opp clkname\n");
> +
> +       ret = devm_pm_opp_of_add_table(dev);
> +       if (ret)
> +               return dev_err_probe(dev, ret, "invalid OPP table in device tree\n");
> +
> +       num_supplies = csi2phy->soc_cfg->num_supplies;
> +       csi2phy->supplies = devm_kzalloc(dev, sizeof(*csi2phy->supplies) * num_supplies,
> +                                        GFP_KERNEL);
> +       if (!csi2phy->supplies)
> +               return -ENOMEM;
> +
> +       for (i = 0; i < num_supplies; i++)
> +               csi2phy->supplies[i].supply = csi2phy->soc_cfg->supply_names[i];
> +
> +       ret = devm_regulator_bulk_get(dev, num_supplies, csi2phy->supplies);
> +       if (ret)
> +               return dev_err_probe(dev, ret,
> +                                    "failed to get regulator supplies\n");
> +
> +       csi2phy->base = devm_platform_ioremap_resource(pdev, 0);
> +       if (IS_ERR(csi2phy->base))
> +               return PTR_ERR(csi2phy->base);
> +
> +       generic_phy = devm_phy_create(dev, NULL, &phy_qcom_mipi_csi2_ops);
> +       if (IS_ERR(generic_phy)) {
> +               ret = PTR_ERR(generic_phy);
> +               return dev_err_probe(dev, ret, "failed to create phy\n");
> +       }
> +       csi2phy->phy = generic_phy;
> +
> +       phy_set_drvdata(generic_phy, csi2phy);
> +
> +       phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
> +       if (!IS_ERR(phy_provider))
> +               dev_dbg(dev, "Registered MIPI CSI2 PHY device\n");
> +
> +       return PTR_ERR_OR_ZERO(phy_provider);
> +}
> +
> +static const struct of_device_id phy_qcom_mipi_csi2_of_match_table[] = {
> +       { .compatible   = "qcom,x1e80100-csi2-phy", .data = &mipi_csi2_dphy_4nm_x1e },
> +       { }
> +};
> +MODULE_DEVICE_TABLE(of, phy_qcom_mipi_csi2_of_match_table);
> +
> +static struct platform_driver phy_qcom_mipi_csi2_driver = {
> +       .probe          = phy_qcom_mipi_csi2_probe,
> +       .driver = {
> +               .name   = "qcom-mipi-csi2-phy",
> +               .of_match_table = phy_qcom_mipi_csi2_of_match_table,
> +       },
> +};
> +
> +module_platform_driver(phy_qcom_mipi_csi2_driver);
> +
> +MODULE_DESCRIPTION("Qualcomm MIPI CSI2 PHY driver");
> +MODULE_AUTHOR("Bryan O'Donoghue <bod at kernel.org>");
> +MODULE_LICENSE("GPL");
> diff --git a/drivers/phy/qualcomm/phy-qcom-mipi-csi2.h b/drivers/phy/qualcomm/phy-qcom-mipi-csi2.h
> new file mode 100644
> index 0000000000000..7e55ae0073704
> --- /dev/null
> +++ b/drivers/phy/qualcomm/phy-qcom-mipi-csi2.h
> @@ -0,0 +1,97 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + *
> + * Qualcomm MIPI CSI2 CPHY/DPHY driver
> + *
> + * Copyright (C) 2025 Linaro Ltd.
> + */
> +#ifndef __PHY_QCOM_MIPI_CSI2_H__
> +#define __PHY_QCOM_MIPI_CSI2_H__
> +
> +#include <linux/phy/phy.h>
> +
> +#define CSI2_MAX_DATA_LANES    4
> +#define CSI2_DEFAULT_CLK_LANE  7
> +
> +struct mipi_csi2phy_lane {
> +       u8 pos;
> +       u8 pol;
> +};
> +
> +struct mipi_csi2phy_lanes_cfg {
> +       struct mipi_csi2phy_lane data[CSI2_MAX_DATA_LANES];
> +       struct mipi_csi2phy_lane clk;
> +};
> +
> +struct mipi_csi2phy_stream_cfg {
> +       s64 link_freq;
> +       u8 num_data_lanes;
> +       struct mipi_csi2phy_lanes_cfg lane_cfg;
> +};
> +
> +struct mipi_csi2phy_device;
> +
> +struct mipi_csi2phy_hw_ops {
> +       void (*hw_version_read)(struct mipi_csi2phy_device *csi2phy_dev);
> +       void (*reset)(struct mipi_csi2phy_device *csi2phy_dev);
> +       int (*lanes_enable)(struct mipi_csi2phy_device *csi2phy_dev,
> +                           struct mipi_csi2phy_stream_cfg *cfg);
> +       void (*lanes_disable)(struct mipi_csi2phy_device *csi2phy_dev,
> +                             struct mipi_csi2phy_stream_cfg *cfg);
> +};
> +
> +struct mipi_csi2phy_lane_regs {
> +       const s32 reg_addr;
> +       const s32 reg_data;
> +       const u32 delay_us;
> +       const u32 param_type;
> +};
> +
> +struct mipi_csi2phy_device_regs {
> +       const struct mipi_csi2phy_lane_regs *init_seq;
> +       const int lane_array_size;
> +       const u32 common_regs_offset;
> +};
> +
> +struct mipi_csi2_genpd {
> +       const char *name;
> +       bool scaled;
> +};
> +
> +struct mipi_csi2phy_soc_cfg {
> +       const struct mipi_csi2phy_hw_ops *ops;
> +       const struct mipi_csi2phy_device_regs reg_info;
> +
> +       const char ** const supply_names;
> +       const unsigned int num_supplies;
> +
> +       const char ** const clk_names;
> +       const unsigned int num_clk;
> +
> +       const struct mipi_csi2_genpd *genpds;
> +       const unsigned int num_genpds;
> +};
> +
> +struct mipi_csi2phy_device {
> +       struct device *dev;
> +       u8 phy_mode;
> +
> +       struct phy *phy;
> +       void __iomem *base;
> +
> +       struct clk_bulk_data *clks;
> +       struct clk *timer_clk;
> +       u32 timer_clk_rate;
> +
> +       struct regulator_bulk_data *supplies;
> +       struct dev_pm_domain_list *pd_list;
> +
> +       const struct mipi_csi2phy_soc_cfg *soc_cfg;
> +       struct mipi_csi2phy_stream_cfg stream_cfg;
> +
> +       u32 hw_version;
> +};
> +
> +extern const struct mipi_csi2phy_soc_cfg mipi_csi2_dphy_4nm_x1e;
> +
> +#endif /* __PHY_QCOM_MIPI_CSI2_H__ */
>
> --
> 2.54.0
>
>


From xiaopei01 at kylinos.cn  Tue Aug  4 01:01:36 2026
From: xiaopei01 at kylinos.cn (Pei Xiao)
Date: Tue,  4 Aug 2026 16:01:36 +0800
Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in rcar_gen3_phy_usb2_remove due to race condition
Message-ID: <d265021fb7fe432e32eedbe9e075fb041c15cbe6.1785830417.git.xiaopei01@kylinos.cn>

In rcar_gen3_phy_usb2_probe, &channel->work is bound with
rcar_gen3_phy_usb2_work. rcar_gen3_phy_usb2_irq can schedule this work
on system_wq via rcar_gen3_device_recognition(), and the role sysfs
store can also schedule it via rcar_gen3_init_for_host() /
rcar_gen3_init_for_peri().

If we remove the device, rcar_gen3_phy_usb2_remove makes cleanup and
the memory allocated for channel with devm_kzalloc() is released by
the devm cleanup after the remove callback returns, while the work
mentioned above may still be pending or running. The sequence of
operations that may lead to a UAF bug is as follows:

CPU0                                      CPU1

                                          | rcar_gen3_phy_usb2_irq
                                          | rcar_gen3_device_recognition
                                          | rcar_gen3_init_for_host
                                          | schedule_work(&ch->work)
rcar_gen3_phy_usb2_remove                 |
device_remove_file(&pdev->dev,            |
                   &dev_attr_role)        |
// remove returns                         |
// devm cleanup: free_irq,                |
// kfree(channel)                         |
                                          | rcar_gen3_phy_usb2_work
                                          | // use ch (use-after-free)

Fix it by disabling the OTG interrupts, so the IRQ handler cannot
schedule new work, and canceling the work before the remaining cleanup
in rcar_gen3_phy_usb2_remove and the devm release of channel.

Fixes: c14f8a4032ef ("phy: rcar-gen3-usb2: fix mutex_lock calling in interrupt")
Assisted-by: Codex:deepseek-v4-flash
Signed-off-by: Pei Xiao <xiaopei01 at kylinos.cn>
---
 drivers/phy/renesas/phy-rcar-gen3-usb2.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/drivers/phy/renesas/phy-rcar-gen3-usb2.c b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
index 9a45d840efeb..fa0e680a4b91 100644
--- a/drivers/phy/renesas/phy-rcar-gen3-usb2.c
+++ b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
@@ -1067,8 +1067,16 @@ static void rcar_gen3_phy_usb2_remove(struct platform_device *pdev)
 {
 	struct rcar_gen3_chan *channel = platform_get_drvdata(pdev);
 
-	if (channel->is_otg_channel)
+	if (channel->is_otg_channel) {
+		/* Disable OTG interrupts so the IRQ handler cannot
+		 * schedule new work.
+		 */
+		rcar_gen3_control_otg_irq(channel, 0);
+
 		device_remove_file(&pdev->dev, &dev_attr_role);
+
+		cancel_work_sync(&channel->work);
+	}
 }
 
 static int rcar_gen3_phy_usb2_suspend(struct device *dev)
-- 
2.25.1



From krzk at kernel.org  Tue Aug  4 01:07:48 2026
From: krzk at kernel.org (Krzysztof Kozlowski)
Date: Tue, 4 Aug 2026 10:07:48 +0200
Subject: [PATCH 1/2] dt-bindings: phy: Add sa8255p high-speed USB PHY
In-Reply-To: <20260730-phy-qcom-snps-femto-v2-8255-v1-1-8b8bf5105a68@kernel.org>
References: <20260730-phy-qcom-snps-femto-v2-8255-v1-0-8b8bf5105a68@kernel.org>
 <20260730-phy-qcom-snps-femto-v2-8255-v1-1-8b8bf5105a68@kernel.org>
Message-ID: <20260804-active-sensible-reindeer-f459a3@quoll>

On Thu, Jul 30, 2026 at 03:58:14PM +0200, Mattijs Korpershoek wrote:
> SA8255p platform abstracts resources such as clocks, interconnect and
> GPIO pins configuration in Firmware. SCMI power protocol is used to
> request resource configurations.
> 
> Two power domains can be requested: transfer and core.
> transfer controls clocks and interconnects and core controls
> the GDSC, resets and TLMM (GPIOs).
> 
> Add a new binding for the Qualcomm SA8255p Synopsys Femto High-Speed
> USB PHY V2 found in SA8255P.
> 
> Signed-off-by: Mattijs Korpershoek <mkorpershoek at kernel.org>
> ---
>  .../bindings/phy/qcom,sa8255p-usb-hs-phy.yaml      | 52 ++++++++++++++++++++++
>  1 file changed, 52 insertions(+)
> 
> diff --git a/Documentation/devicetree/bindings/phy/qcom,sa8255p-usb-hs-phy.yaml b/Documentation/devicetree/bindings/phy/qcom,sa8255p-usb-hs-phy.yaml
> new file mode 100644
> index 000000000000..09f7ca6e7f72
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/phy/qcom,sa8255p-usb-hs-phy.yaml
> @@ -0,0 +1,52 @@
> +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/phy/qcom,sa8255p-usb-hs-phy.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: Qualcomm SA8255p Synopsys Femto High-Speed USB PHY V2
> +
> +maintainers:
> +  - Mattijs Korpershoek <mkorpershoek at kernel.org>
> +
> +properties:
> +  compatible:
> +    const: qcom,sa8255p-usb-hs-phy
> +
> +  reg:
> +    maxItems: 1
> +
> +  "#phy-cells":
> +    const: 0
> +
> +  power-domains:
> +    maxItems: 2
> +
> +  power-domain-names:
> +    items:
> +      - const: transfer
> +      - const: core

Here and in your other binding you call these 'transfer' and 'core' but
all other bindings call them 'power' and 'perf'. Why this difference?
What are these domains responsible for?

Commit msg is pretty useless here - repeats the diff. I can read the
diff but I still do not understand why such naming.

And why core is not the first one? It clearly feels like the power
domain thus the main one.

Best regards,
Krzysztof



From krzk at kernel.org  Tue Aug  4 01:08:18 2026
From: krzk at kernel.org (Krzysztof Kozlowski)
Date: Tue, 4 Aug 2026 10:08:18 +0200
Subject: [PATCH v3 1/2] dt-bindings: phy: Add sa8255p USB PHY
In-Reply-To: <20260722-phy-qcom-qmp-usb-8255-v3-1-e789070b48f7@kernel.org>
References: <20260722-phy-qcom-qmp-usb-8255-v3-0-e789070b48f7@kernel.org>
 <20260722-phy-qcom-qmp-usb-8255-v3-1-e789070b48f7@kernel.org>
Message-ID: <f9320dae-6436-45e1-b99e-f49115b6279b@kernel.org>

On 22/07/2026 15:45, Mattijs Korpershoek wrote:
> +  power-domains:
> +    maxItems: 2
> +
> +  power-domain-names:
> +    items:
> +      - const: transfer
> +      - const: core
> +

Same comments, why isn't this following established naming practice for
sa8xxx bindings?

Best regards,
Krzysztof


From sashiko-bot at kernel.org  Tue Aug  4 01:12:12 2026
From: sashiko-bot at kernel.org (sashiko-bot at kernel.org)
Date: Tue, 04 Aug 2026 08:12:12 +0000
Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in
 rcar_gen3_phy_usb2_remove due to race condition
In-Reply-To: <d265021fb7fe432e32eedbe9e075fb041c15cbe6.1785830417.git.xiaopei01@kylinos.cn>
References: <d265021fb7fe432e32eedbe9e075fb041c15cbe6.1785830417.git.xiaopei01@kylinos.cn>
Message-ID: <20260804081213.485401F000E9@smtp.kernel.org>

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

New issues:
- [Critical] Accessing hardware registers in `remove()` while the device may be runtime suspended causes a synchronous external abort (kernel panic).
- [High] Unlocked hardware register access in `remove()` races with the `role_store()` sysfs operation, allowing interrupts to be re-enabled and completely bypassing the fix.

Pre-existing issues:
- [High] The patch fails to resolve the use-after-free race condition because it does not synchronize with an already-executing IRQ handler before calling `cancel_work_sync()`.
- [High] Missing initialization of `work_struct` when an IRQ is absent causes a kernel panic on sysfs writes.
- [High] Missing `cancel_work_sync()` on probe error paths leads to use-after-free if probe fails.
--

--- Patch [1]: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in rcar_gen3_phy_usb2_remove due to race condition ---
commit c1e0683754e769afd34c8d1183cbf96e6d4a4b81
Author: Pei Xiao <xiaopei01 at kylinos.cn>

phy: renesas: rcar-gen3-usb2: Fix use-after-free in rcar_gen3_phy_usb2_remove due to race condition

This commit attempts to fix a use-after-free in the driver remove path by
disabling OTG interrupts and canceling the work queue before the devm cleanup
happens.

> diff --git a/drivers/phy/renesas/phy-rcar-gen3-usb2.c b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> index 9a45d840efeb2..fa0e680a4b915 100644
> --- a/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> +++ b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> @@ -1067,8 +1067,16 @@ static void rcar_gen3_phy_usb2_remove(struct platform_device *pdev)
>  {
>  	struct rcar_gen3_chan *channel = platform_get_drvdata(pdev);
>  
> -	if (channel->is_otg_channel)
> +	if (channel->is_otg_channel) {
> +		/* Disable OTG interrupts so the IRQ handler cannot
> +		 * schedule new work.
> +		 */
> +		rcar_gen3_control_otg_irq(channel, 0);

[Severity: Critical]
Will this trigger a synchronous external abort?

If there are no active PHY consumers when rcar_gen3_phy_usb2_remove() is
called, the device will be runtime suspended and its clocks gated.

Calling rcar_gen3_control_otg_irq() here reads and writes to USB2_OBINTEN
without first waking the device via pm_runtime_resume_and_get().

[Severity: High]
Does this unlocked register access race with the sysfs role_store() function?

If CPU1 concurrently writes to the sysfs role attribute, role_store() takes
channel->lock and eventually executes rcar_gen3_control_otg_irq(channel, 1).

Because the interrupt is disabled here without channel->lock, CPU1's write
can re-enable the interrupt right before device_remove_file() is called.

This would leave the interrupt fully enabled, bypassing the intended fix.

> +
>  		device_remove_file(&pdev->dev, &dev_attr_role);
> +
> +		cancel_work_sync(&channel->work);

[Severity: High]
This is a pre-existing issue, but does this actually prevent the
use-after-free?

Since the IRQ handler is shared and managed by devres, it isn't freed until
after rcar_gen3_phy_usb2_remove() returns.

If CPU1 is already executing rcar_gen3_phy_usb2_irq(), it might have already
read the status register. CPU0 can disable the IRQ and call cancel_work_sync(),
but CPU1 can subsequently call schedule_work().

When the remove function returns and devres frees the channel, the queued work
will execute and cause a use-after-free. To fix this, the IRQ must be
explicitly freed or synchronized before cancel_work_sync().

[Severity: High]
This isn't a bug introduced by this patch, but is it possible for sysfs writes
to trigger a NULL pointer dereference on the work queue?

In rcar_gen3_phy_usb2_probe():

    if (irq < 0 && irq != -ENXIO) {
            return irq;
    } else if (irq > 0) {
            INIT_WORK(&channel->work, rcar_gen3_phy_usb2_work);
    }

If irq <= 0, INIT_WORK is skipped. However, if dr_mode is configured,
the role sysfs attribute is still created. Writing to it will call
schedule_work(&channel->work) on an uninitialized work structure.

[Severity: High]
This is a pre-existing issue, but does the probe error path leak pending work?

If devm_request_irq() succeeds in rcar_gen3_phy_usb2_probe(), the shared IRQ
becomes live.

If a subsequent probe step fails (like devm_of_phy_provider_register), the
probe function returns an error and devres frees the IRQ and channel memory.

If the IRQ fired and scheduled channel->work, there is no explicit
cancel_work_sync() on the error path to stop it before the memory is freed,
potentially leading to a use-after-free.

> +	}
>  }
>  
>  static int rcar_gen3_phy_usb2_suspend(struct device *dev)

-- 
Sashiko AI review ? https://sashiko.dev/#/patchset/d265021fb7fe432e32eedbe9e075fb041c15cbe6.1785830417.git.xiaopei01 at kylinos.cn?part=1


From biju.das.jz at bp.renesas.com  Tue Aug  4 01:34:18 2026
From: biju.das.jz at bp.renesas.com (Biju Das)
Date: Tue, 4 Aug 2026 08:34:18 +0000
Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in
 rcar_gen3_phy_usb2_remove due to race condition
In-Reply-To: <d265021fb7fe432e32eedbe9e075fb041c15cbe6.1785830417.git.xiaopei01@kylinos.cn>
References: <d265021fb7fe432e32eedbe9e075fb041c15cbe6.1785830417.git.xiaopei01@kylinos.cn>
Message-ID: <TY3PR01MB1134664317D6A75D8230DB51B86D42@TY3PR01MB11346.jpnprd01.prod.outlook.com>

Hi Pei Xiao,

Thanks for the patch.

> -----Original Message-----
> From: Pei Xiao <xiaopei01 at kylinos.cn>
> Sent: 04 August 2026 09:02
> Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in rcar_gen3_phy_usb2_remove due to
> race condition
> 
> In rcar_gen3_phy_usb2_probe, &channel->work is bound with rcar_gen3_phy_usb2_work. rcar_gen3_phy_usb2_irq
> can schedule this work on system_wq via rcar_gen3_device_recognition(), and the role sysfs store can also
> schedule it via rcar_gen3_init_for_host() / rcar_gen3_init_for_peri().
> 
> If we remove the device, rcar_gen3_phy_usb2_remove makes cleanup and the memory allocated for channel
> with devm_kzalloc() is released by the devm cleanup after the remove callback returns, while the work
> mentioned above may still be pending or running. The sequence of operations that may lead to a UAF bug is
> as follows:
> 
> CPU0                                      CPU1
> 
>                                           | rcar_gen3_phy_usb2_irq
>                                           | rcar_gen3_device_recognition
>                                           | rcar_gen3_init_for_host
>                                           | schedule_work(&ch->work)
> rcar_gen3_phy_usb2_remove                 |
> device_remove_file(&pdev->dev,            |
>                    &dev_attr_role)        |
> // remove returns                         |
> // devm cleanup: free_irq,                |
> // kfree(channel)                         |
>                                           | rcar_gen3_phy_usb2_work
>                                           | // use ch (use-after-free)
> 
> Fix it by disabling the OTG interrupts, so the IRQ handler cannot schedule new work, and canceling the
> work before the remaining cleanup in rcar_gen3_phy_usb2_remove and the devm release of channel.
> 
> Fixes: c14f8a4032ef ("phy: rcar-gen3-usb2: fix mutex_lock calling in interrupt")
> Assisted-by: Codex:deepseek-v4-flash
> Signed-off-by: Pei Xiao <xiaopei01 at kylinos.cn>
> ---
>  drivers/phy/renesas/phy-rcar-gen3-usb2.c | 10 +++++++++-
>  1 file changed, 9 insertions(+), 1 deletion(-)
> 
> diff --git a/drivers/phy/renesas/phy-rcar-gen3-usb2.c b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> index 9a45d840efeb..fa0e680a4b91 100644
> --- a/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> +++ b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> @@ -1067,8 +1067,16 @@ static void rcar_gen3_phy_usb2_remove(struct platform_device *pdev)  {
>  	struct rcar_gen3_chan *channel = platform_get_drvdata(pdev);
> 
> -	if (channel->is_otg_channel)
> +	if (channel->is_otg_channel) {
> +		/* Disable OTG interrupts so the IRQ handler cannot
> +		 * schedule new work.
> +		 */
> +		rcar_gen3_control_otg_irq(channel, 0);
> +
>  		device_remove_file(&pdev->dev, &dev_attr_role);
> +
> +		cancel_work_sync(&channel->work);

What about pending wq that is still about execute after
"device_remove_file(&pdev->dev, &dev_attr_role);" ?

Cheers,
Biju

> +	}
>  }
> 
>  static int rcar_gen3_phy_usb2_suspend(struct device *dev)
> --
> 2.25.1
> 



From mohd.anwar at oss.qualcomm.com  Tue Aug  4 01:36:30 2026
From: mohd.anwar at oss.qualcomm.com (Mohd Ayaan Anwar)
Date: Tue, 4 Aug 2026 14:06:30 +0530
Subject: [PATCH v2 0/6] Fix regulator voting for the Qualcomm SGMII
 SerDes PHY
In-Reply-To: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
References: <20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd@oss.qualcomm.com>
Message-ID: <anGkjjDJRRMVT7ds@oss.qualcomm.com>

On Tue, Aug 04, 2026 at 12:57:56AM +0530, Mohd Ayaan Anwar wrote:
> This series corrects this by:
> 
>   1. Fixing the DT binding to describe both named supplies.
>   2. Fixing all affected board DTS to use the correct vdda-0p9-supply
>      and vdda-1p2-supply properties.
>   3. Adding bulk regulator support to the PHY driver so it enables both
>      supplies and sets the proper current loads.
> 
> The patches are sequenced to maintain bisectability. With the driver
> change, the PHY framework will continue to vote for the regulator
> described by 'phy-supply'. The DT changes which follow then remove
> 'phy-supply' in favour of 'vdda-0p9' and 'vdda-1p2'.
> 

Sashiko raises some valid concerns [0]:
--
1. This isn't a bug introduced by this patch, but I noticed that
another file in the tree, monaco-monza-som.dtsi, still has the exact
same configuration you are fixing here. It retains the deprecated
phy-supply property and lacks the required 0.9V rail.

Should monaco-monza-som.dtsi be updated similarly in this patch or as a
follow-up in this series?

2. This is a pre-existing issue, but does registering the PHY provider
before setting the driver data create a race condition here?

Looking at the end of qcom_dwmac_sgmii_phy_probe():
	provider = devm_of_phy_provider_register(dev,
	of_phy_simple_xlate);
	if (IS_ERR(provider))
		return PTR_ERR(provider);

	phy_set_drvdata(phy, data);

By calling devm_of_phy_provider_register() first, the PHY is made
available to consumers before phy_set_drvdata() initializes the private
data. If a consumer (such as a deferred probe workqueue) concurrently
looks up the PHY and immediately invokes an operation like
phy_power_on(), could qcom_dwmac_sgmii_phy_power_on() read a NULL
driver data pointer and cause a NULL pointer dereference when
attempting to access data->vregs?
--

For 1., I can either post a follow-up patch or add it as part of v3.

For 2., this is a genuine pre-existing bug. I will raise a separate
patch to move phy_set_drvdata() before devm_of_phy_provider_register()
to close the race window.

	Ayaan

[0] https://sashiko.dev/#/patchset/20260804-b4-sgmiieth_serdes_regulator-v2-0-c4bc688177dd at oss.qualcomm.com



From xiaopei01 at kylinos.cn  Tue Aug  4 02:05:12 2026
From: xiaopei01 at kylinos.cn (Pei Xiao)
Date: Tue, 4 Aug 2026 17:05:12 +0800
Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in
 rcar_gen3_phy_usb2_remove due to race condition
In-Reply-To: <TY3PR01MB1134664317D6A75D8230DB51B86D42@TY3PR01MB11346.jpnprd01.prod.outlook.com>
References: <d265021fb7fe432e32eedbe9e075fb041c15cbe6.1785830417.git.xiaopei01@kylinos.cn>
 <TY3PR01MB1134664317D6A75D8230DB51B86D42@TY3PR01MB11346.jpnprd01.prod.outlook.com>
Message-ID: <63160e2f-8566-4a2f-b0fe-ba97603df78b@kylinos.cn>



? 2026/8/4 16:34, Biju Das ??:
> Hi Pei Xiao,
> 
> Thanks for the patch.
> 
>> -----Original Message-----
>> From: Pei Xiao <xiaopei01 at kylinos.cn>
>> Sent: 04 August 2026 09:02
>> Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in rcar_gen3_phy_usb2_remove due to
>> race condition
>>
>> In rcar_gen3_phy_usb2_probe, &channel->work is bound with rcar_gen3_phy_usb2_work. rcar_gen3_phy_usb2_irq
>> can schedule this work on system_wq via rcar_gen3_device_recognition(), and the role sysfs store can also
>> schedule it via rcar_gen3_init_for_host() / rcar_gen3_init_for_peri().
>>
>> If we remove the device, rcar_gen3_phy_usb2_remove makes cleanup and the memory allocated for channel
>> with devm_kzalloc() is released by the devm cleanup after the remove callback returns, while the work
>> mentioned above may still be pending or running. The sequence of operations that may lead to a UAF bug is
>> as follows:
>>
>> CPU0                                      CPU1
>>
>>                                           | rcar_gen3_phy_usb2_irq
>>                                           | rcar_gen3_device_recognition
>>                                           | rcar_gen3_init_for_host
>>                                           | schedule_work(&ch->work)
>> rcar_gen3_phy_usb2_remove                 |
>> device_remove_file(&pdev->dev,            |
>>                    &dev_attr_role)        |
>> // remove returns                         |
>> // devm cleanup: free_irq,                |
>> // kfree(channel)                         |
>>                                           | rcar_gen3_phy_usb2_work
>>                                           | // use ch (use-after-free)
>>
>> Fix it by disabling the OTG interrupts, so the IRQ handler cannot schedule new work, and canceling the
>> work before the remaining cleanup in rcar_gen3_phy_usb2_remove and the devm release of channel.
>>
>> Fixes: c14f8a4032ef ("phy: rcar-gen3-usb2: fix mutex_lock calling in interrupt")
>> Assisted-by: Codex:deepseek-v4-flash
>> Signed-off-by: Pei Xiao <xiaopei01 at kylinos.cn>
>> ---
>>  drivers/phy/renesas/phy-rcar-gen3-usb2.c | 10 +++++++++-
>>  1 file changed, 9 insertions(+), 1 deletion(-)
>>
>> diff --git a/drivers/phy/renesas/phy-rcar-gen3-usb2.c b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
>> index 9a45d840efeb..fa0e680a4b91 100644
>> --- a/drivers/phy/renesas/phy-rcar-gen3-usb2.c
>> +++ b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
>> @@ -1067,8 +1067,16 @@ static void rcar_gen3_phy_usb2_remove(struct platform_device *pdev)  {
>>  	struct rcar_gen3_chan *channel = platform_get_drvdata(pdev);
>>
>> -	if (channel->is_otg_channel)
>> +	if (channel->is_otg_channel) {
>> +		/* Disable OTG interrupts so the IRQ handler cannot
>> +		 * schedule new work.
>> +		 */
>> +		rcar_gen3_control_otg_irq(channel, 0);
>> +
>>  		device_remove_file(&pdev->dev, &dev_attr_role);
>> +
>> +		cancel_work_sync(&channel->work);
> 
> What about pending wq that is still about execute after
> "device_remove_file(&pdev->dev, &dev_attr_role);" ?
Hi Biju,
  I don't understand what you mean. cancel_work_sync is exactly what
catches this kind of pending work ? it either cancels the work that
hasn't run yet, or waits for the one that is currently running to
finish, and it is placed right.
 Could you explain it in more detail?

Thanks?
Pei.

  > after device_remove_file.

> Cheers,
> Biju
> 
>> +	}
>>  }
>>
>>  static int rcar_gen3_phy_usb2_suspend(struct device *dev)
>> --
>> 2.25.1
>>



From biju.das.jz at bp.renesas.com  Tue Aug  4 02:08:44 2026
From: biju.das.jz at bp.renesas.com (Biju Das)
Date: Tue, 4 Aug 2026 09:08:44 +0000
Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in
 rcar_gen3_phy_usb2_remove due to race condition
In-Reply-To: <63160e2f-8566-4a2f-b0fe-ba97603df78b@kylinos.cn>
References: <d265021fb7fe432e32eedbe9e075fb041c15cbe6.1785830417.git.xiaopei01@kylinos.cn>
 <TY3PR01MB1134664317D6A75D8230DB51B86D42@TY3PR01MB11346.jpnprd01.prod.outlook.com>
 <63160e2f-8566-4a2f-b0fe-ba97603df78b@kylinos.cn>
Message-ID: <TY3PR01MB1134613500284ECC47AE1B1D686D42@TY3PR01MB11346.jpnprd01.prod.outlook.com>



> -----Original Message-----
> From: Pei Xiao <xiaopei01 at kylinos.cn>
> Sent: 04 August 2026 10:05
> To: Biju Das <biju.das.jz at bp.renesas.com>; Yoshihiro Shimoda <yoshihiro.shimoda.uh at renesas.com>;
> vkoul at kernel.org; neil.armstrong at linaro.org; geert+renesas at glider.be; magnus.damm
> <magnus.damm at gmail.com>; linux-renesas-soc at vger.kernel.org; linux-phy at lists.infradead.org; linux-
> kernel at vger.kernel.org
> Subject: Re: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in rcar_gen3_phy_usb2_remove due to
> race condition
> 
> 
> 
> ? 2026/8/4 16:34, Biju Das ??:
> > Hi Pei Xiao,
> >
> > Thanks for the patch.
> >
> >> -----Original Message-----
> >> From: Pei Xiao <xiaopei01 at kylinos.cn>
> >> Sent: 04 August 2026 09:02
> >> Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in
> >> rcar_gen3_phy_usb2_remove due to race condition
> >>
> >> In rcar_gen3_phy_usb2_probe, &channel->work is bound with
> >> rcar_gen3_phy_usb2_work. rcar_gen3_phy_usb2_irq can schedule this
> >> work on system_wq via rcar_gen3_device_recognition(), and the role sysfs store can also schedule it
> via rcar_gen3_init_for_host() / rcar_gen3_init_for_peri().
> >>
> >> If we remove the device, rcar_gen3_phy_usb2_remove makes cleanup and
> >> the memory allocated for channel with devm_kzalloc() is released by
> >> the devm cleanup after the remove callback returns, while the work
> >> mentioned above may still be pending or running. The sequence of operations that may lead to a UAF bug
> is as follows:
> >>
> >> CPU0                                      CPU1
> >>
> >>                                           | rcar_gen3_phy_usb2_irq
> >>                                           | rcar_gen3_device_recognition
> >>                                           | rcar_gen3_init_for_host
> >>                                           | schedule_work(&ch->work)
> >> rcar_gen3_phy_usb2_remove                 |
> >> device_remove_file(&pdev->dev,            |
> >>                    &dev_attr_role)        |
> >> // remove returns                         |
> >> // devm cleanup: free_irq,                |
> >> // kfree(channel)                         |
> >>                                           | rcar_gen3_phy_usb2_work
> >>                                           | // use ch
> >> (use-after-free)
> >>
> >> Fix it by disabling the OTG interrupts, so the IRQ handler cannot
> >> schedule new work, and canceling the work before the remaining cleanup in rcar_gen3_phy_usb2_remove
> and the devm release of channel.
> >>
> >> Fixes: c14f8a4032ef ("phy: rcar-gen3-usb2: fix mutex_lock calling in
> >> interrupt")
> >> Assisted-by: Codex:deepseek-v4-flash
> >> Signed-off-by: Pei Xiao <xiaopei01 at kylinos.cn>
> >> ---
> >>  drivers/phy/renesas/phy-rcar-gen3-usb2.c | 10 +++++++++-
> >>  1 file changed, 9 insertions(+), 1 deletion(-)
> >>
> >> diff --git a/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> >> b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> >> index 9a45d840efeb..fa0e680a4b91 100644
> >> --- a/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> >> +++ b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> >> @@ -1067,8 +1067,16 @@ static void rcar_gen3_phy_usb2_remove(struct platform_device *pdev)  {
> >>  	struct rcar_gen3_chan *channel = platform_get_drvdata(pdev);
> >>
> >> -	if (channel->is_otg_channel)
> >> +	if (channel->is_otg_channel) {
> >> +		/* Disable OTG interrupts so the IRQ handler cannot
> >> +		 * schedule new work.
> >> +		 */
> >> +		rcar_gen3_control_otg_irq(channel, 0);
> >> +
> >>  		device_remove_file(&pdev->dev, &dev_attr_role);
> >> +
> >> +		cancel_work_sync(&channel->work);
> >
> > What about pending wq that is still about execute after
> > "device_remove_file(&pdev->dev, &dev_attr_role);" ?
> Hi Biju,
>   I don't understand what you mean. cancel_work_sync is exactly what catches this kind of pending work ?
> it either cancels the work that hasn't run yet, or waits for the one that is currently running to finish,
> and it is placed right.
>  Could you explain it in more detail?

Assume you removed the file, and before cancel_work_sync(), the WQ get scheduled
will it result in UAF bug mentioned in the commit message.

Maybe??

rcar_gen3_control_otg_irq(channel, 0);
cancel_work_sync(&channel->work);
device_remove_file(&pdev->dev, &dev_attr_role);

Cheers,
Biju




> 
> Thanks?
> Pei.
> 
>   > after device_remove_file.
> 
> > Cheers,
> > Biju
> >
> >> +	}
> >>  }
> >>
> >>  static int rcar_gen3_phy_usb2_suspend(struct device *dev)
> >> --
> >> 2.25.1
> >>


From xiaopei01 at kylinos.cn  Tue Aug  4 02:30:35 2026
From: xiaopei01 at kylinos.cn (Pei Xiao)
Date: Tue, 4 Aug 2026 17:30:35 +0800
Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in
 rcar_gen3_phy_usb2_remove due to race condition
In-Reply-To: <TY3PR01MB1134613500284ECC47AE1B1D686D42@TY3PR01MB11346.jpnprd01.prod.outlook.com>
References: <d265021fb7fe432e32eedbe9e075fb041c15cbe6.1785830417.git.xiaopei01@kylinos.cn>
 <TY3PR01MB1134664317D6A75D8230DB51B86D42@TY3PR01MB11346.jpnprd01.prod.outlook.com>
 <63160e2f-8566-4a2f-b0fe-ba97603df78b@kylinos.cn>
 <TY3PR01MB1134613500284ECC47AE1B1D686D42@TY3PR01MB11346.jpnprd01.prod.outlook.com>
Message-ID: <c1449f71-b516-4184-b224-052d2e0a46e6@kylinos.cn>



? 2026/8/4 17:08, Biju Das ??:
> 
> 
>> -----Original Message-----
>> From: Pei Xiao <xiaopei01 at kylinos.cn>
>> Sent: 04 August 2026 10:05
>> To: Biju Das <biju.das.jz at bp.renesas.com>; Yoshihiro Shimoda <yoshihiro.shimoda.uh at renesas.com>;
>> vkoul at kernel.org; neil.armstrong at linaro.org; geert+renesas at glider.be; magnus.damm
>> <magnus.damm at gmail.com>; linux-renesas-soc at vger.kernel.org; linux-phy at lists.infradead.org; linux-
>> kernel at vger.kernel.org
>> Subject: Re: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in rcar_gen3_phy_usb2_remove due to
>> race condition
>>
>>
>>
>> ? 2026/8/4 16:34, Biju Das ??:
>>> Hi Pei Xiao,
>>>
>>> Thanks for the patch.
>>>
>>>> -----Original Message-----
>>>> From: Pei Xiao <xiaopei01 at kylinos.cn>
>>>> Sent: 04 August 2026 09:02
>>>> Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in
>>>> rcar_gen3_phy_usb2_remove due to race condition
>>>>
>>>> In rcar_gen3_phy_usb2_probe, &channel->work is bound with
>>>> rcar_gen3_phy_usb2_work. rcar_gen3_phy_usb2_irq can schedule this
>>>> work on system_wq via rcar_gen3_device_recognition(), and the role sysfs store can also schedule it
>> via rcar_gen3_init_for_host() / rcar_gen3_init_for_peri().
>>>>
>>>> If we remove the device, rcar_gen3_phy_usb2_remove makes cleanup and
>>>> the memory allocated for channel with devm_kzalloc() is released by
>>>> the devm cleanup after the remove callback returns, while the work
>>>> mentioned above may still be pending or running. The sequence of operations that may lead to a UAF bug
>> is as follows:
>>>>
>>>> CPU0                                      CPU1
>>>>
>>>>                                           | rcar_gen3_phy_usb2_irq
>>>>                                           | rcar_gen3_device_recognition
>>>>                                           | rcar_gen3_init_for_host
>>>>                                           | schedule_work(&ch->work)
>>>> rcar_gen3_phy_usb2_remove                 |
>>>> device_remove_file(&pdev->dev,            |
>>>>                    &dev_attr_role)        |
>>>> // remove returns                         |
>>>> // devm cleanup: free_irq,                |
>>>> // kfree(channel)                         |
>>>>                                           | rcar_gen3_phy_usb2_work
>>>>                                           | // use ch
>>>> (use-after-free)
>>>>
>>>> Fix it by disabling the OTG interrupts, so the IRQ handler cannot
>>>> schedule new work, and canceling the work before the remaining cleanup in rcar_gen3_phy_usb2_remove
>> and the devm release of channel.
>>>>
>>>> Fixes: c14f8a4032ef ("phy: rcar-gen3-usb2: fix mutex_lock calling in
>>>> interrupt")
>>>> Assisted-by: Codex:deepseek-v4-flash
>>>> Signed-off-by: Pei Xiao <xiaopei01 at kylinos.cn>
>>>> ---
>>>>  drivers/phy/renesas/phy-rcar-gen3-usb2.c | 10 +++++++++-
>>>>  1 file changed, 9 insertions(+), 1 deletion(-)
>>>>
>>>> diff --git a/drivers/phy/renesas/phy-rcar-gen3-usb2.c
>>>> b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
>>>> index 9a45d840efeb..fa0e680a4b91 100644
>>>> --- a/drivers/phy/renesas/phy-rcar-gen3-usb2.c
>>>> +++ b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
>>>> @@ -1067,8 +1067,16 @@ static void rcar_gen3_phy_usb2_remove(struct platform_device *pdev)  {
>>>>  	struct rcar_gen3_chan *channel = platform_get_drvdata(pdev);
>>>>
>>>> -	if (channel->is_otg_channel)
>>>> +	if (channel->is_otg_channel) {
>>>> +		/* Disable OTG interrupts so the IRQ handler cannot
>>>> +		 * schedule new work.
>>>> +		 */
>>>> +		rcar_gen3_control_otg_irq(channel, 0);
>>>> +
>>>>  		device_remove_file(&pdev->dev, &dev_attr_role);
>>>> +
>>>> +		cancel_work_sync(&channel->work);
>>>
>>> What about pending wq that is still about execute after
>>> "device_remove_file(&pdev->dev, &dev_attr_role);" ?
>> Hi Biju,
>>   I don't understand what you mean. cancel_work_sync is exactly what catches this kind of pending work ?
>> it either cancels the work that hasn't run yet, or waits for the one that is currently running to finish,
>> and it is placed right.
>>  Could you explain it in more detail?
> 
> Assume you removed the file, and before cancel_work_sync(), the WQ get scheduled
> will it result in UAF bug mentioned in the commit message.
> 
The rcar_gen3_phy_usb2_work only uses chan. If you call
device_remove_file(&pdev->dev, &dev_attr_role) before
cancel_work_sync(&channel->work), it does not lead to a UAF.

On the contrary, if you call cancel_work_sync(&channel->work) first, the
sysfs node has not been removed yet. When the sysfs node is written to
(via store) again, it will schedule the work once more, rendering the
cancel_work_sync call ineffective.
> Maybe??
> 
> rcar_gen3_control_otg_irq(channel, 0);
> cancel_work_sync(&channel->work);
> device_remove_file(&pdev->dev, &dev_attr_role);
> 
> Cheers,
> Biju
> 
> 
> 
> 
>>
>> Thanks?
>> Pei.
>>
>>   > after device_remove_file.
>>
>>> Cheers,
>>> Biju
>>>
>>>> +	}
>>>>  }
>>>>
>>>>  static int rcar_gen3_phy_usb2_suspend(struct device *dev)
>>>> --
>>>> 2.25.1
>>>>
> 



From biju.das.jz at bp.renesas.com  Tue Aug  4 02:38:36 2026
From: biju.das.jz at bp.renesas.com (Biju Das)
Date: Tue, 4 Aug 2026 09:38:36 +0000
Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in
 rcar_gen3_phy_usb2_remove due to race condition
In-Reply-To: <c1449f71-b516-4184-b224-052d2e0a46e6@kylinos.cn>
References: <d265021fb7fe432e32eedbe9e075fb041c15cbe6.1785830417.git.xiaopei01@kylinos.cn>
 <TY3PR01MB1134664317D6A75D8230DB51B86D42@TY3PR01MB11346.jpnprd01.prod.outlook.com>
 <63160e2f-8566-4a2f-b0fe-ba97603df78b@kylinos.cn>
 <TY3PR01MB1134613500284ECC47AE1B1D686D42@TY3PR01MB11346.jpnprd01.prod.outlook.com>
 <c1449f71-b516-4184-b224-052d2e0a46e6@kylinos.cn>
Message-ID: <TYCPR01MB11332566338565BC938D459D686D42@TYCPR01MB11332.jpnprd01.prod.outlook.com>



> -----Original Message-----
> From: Pei Xiao <xiaopei01 at kylinos.cn>
> Sent: 04 August 2026 10:31
> Subject: Re: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free in rcar_gen3_phy_usb2_remove due to
> race condition
> 
> 
> 
> ? 2026/8/4 17:08, Biju Das ??:
> >
> >
> >> -----Original Message-----
> >> From: Pei Xiao <xiaopei01 at kylinos.cn>
> >> Sent: 04 August 2026 10:05
> >> To: Biju Das <biju.das.jz at bp.renesas.com>; Yoshihiro Shimoda
> >> <yoshihiro.shimoda.uh at renesas.com>;
> >> vkoul at kernel.org; neil.armstrong at linaro.org; geert+renesas at glider.be;
> >> magnus.damm <magnus.damm at gmail.com>;
> >> linux-renesas-soc at vger.kernel.org; linux-phy at lists.infradead.org;
> >> linux- kernel at vger.kernel.org
> >> Subject: Re: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free
> >> in rcar_gen3_phy_usb2_remove due to race condition
> >>
> >>
> >>
> >> ? 2026/8/4 16:34, Biju Das ??:
> >>> Hi Pei Xiao,
> >>>
> >>> Thanks for the patch.
> >>>
> >>>> -----Original Message-----
> >>>> From: Pei Xiao <xiaopei01 at kylinos.cn>
> >>>> Sent: 04 August 2026 09:02
> >>>> Subject: [PATCH] phy: renesas: rcar-gen3-usb2: Fix use-after-free
> >>>> in rcar_gen3_phy_usb2_remove due to race condition
> >>>>
> >>>> In rcar_gen3_phy_usb2_probe, &channel->work is bound with
> >>>> rcar_gen3_phy_usb2_work. rcar_gen3_phy_usb2_irq can schedule this
> >>>> work on system_wq via rcar_gen3_device_recognition(), and the role
> >>>> sysfs store can also schedule it
> >> via rcar_gen3_init_for_host() / rcar_gen3_init_for_peri().
> >>>>
> >>>> If we remove the device, rcar_gen3_phy_usb2_remove makes cleanup
> >>>> and the memory allocated for channel with devm_kzalloc() is
> >>>> released by the devm cleanup after the remove callback returns,
> >>>> while the work mentioned above may still be pending or running. The
> >>>> sequence of operations that may lead to a UAF bug
> >> is as follows:
> >>>>
> >>>> CPU0                                      CPU1
> >>>>
> >>>>                                           | rcar_gen3_phy_usb2_irq
> >>>>                                           | rcar_gen3_device_recognition
> >>>>                                           | rcar_gen3_init_for_host
> >>>>                                           | schedule_work(&ch->work)
> >>>> rcar_gen3_phy_usb2_remove                 |
> >>>> device_remove_file(&pdev->dev,            |
> >>>>                    &dev_attr_role)        |
> >>>> // remove returns                         |
> >>>> // devm cleanup: free_irq,                |
> >>>> // kfree(channel)                         |
> >>>>                                           | rcar_gen3_phy_usb2_work
> >>>>                                           | // use ch
> >>>> (use-after-free)
> >>>>
> >>>> Fix it by disabling the OTG interrupts, so the IRQ handler cannot
> >>>> schedule new work, and canceling the work before the remaining
> >>>> cleanup in rcar_gen3_phy_usb2_remove
> >> and the devm release of channel.
> >>>>
> >>>> Fixes: c14f8a4032ef ("phy: rcar-gen3-usb2: fix mutex_lock calling
> >>>> in
> >>>> interrupt")
> >>>> Assisted-by: Codex:deepseek-v4-flash
> >>>> Signed-off-by: Pei Xiao <xiaopei01 at kylinos.cn>
> >>>> ---
> >>>>  drivers/phy/renesas/phy-rcar-gen3-usb2.c | 10 +++++++++-
> >>>>  1 file changed, 9 insertions(+), 1 deletion(-)
> >>>>
> >>>> diff --git a/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> >>>> b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> >>>> index 9a45d840efeb..fa0e680a4b91 100644
> >>>> --- a/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> >>>> +++ b/drivers/phy/renesas/phy-rcar-gen3-usb2.c
> >>>> @@ -1067,8 +1067,16 @@ static void rcar_gen3_phy_usb2_remove(struct platform_device *pdev)  {
> >>>>  	struct rcar_gen3_chan *channel = platform_get_drvdata(pdev);
> >>>>
> >>>> -	if (channel->is_otg_channel)
> >>>> +	if (channel->is_otg_channel) {
> >>>> +		/* Disable OTG interrupts so the IRQ handler cannot
> >>>> +		 * schedule new work.
> >>>> +		 */
> >>>> +		rcar_gen3_control_otg_irq(channel, 0);
> >>>> +
> >>>>  		device_remove_file(&pdev->dev, &dev_attr_role);
> >>>> +
> >>>> +		cancel_work_sync(&channel->work);
> >>>
> >>> What about pending wq that is still about execute after
> >>> "device_remove_file(&pdev->dev, &dev_attr_role);" ?
> >> Hi Biju,
> >>   I don't understand what you mean. cancel_work_sync is exactly what
> >> catches this kind of pending work ? it either cancels the work that
> >> hasn't run yet, or waits for the one that is currently running to finish, and it is placed right.
> >>  Could you explain it in more detail?
> >
> > Assume you removed the file, and before cancel_work_sync(), the WQ get
> > scheduled will it result in UAF bug mentioned in the commit message.
> >
> The rcar_gen3_phy_usb2_work only uses chan. If you call device_remove_file(&pdev->dev, &dev_attr_role)
> before cancel_work_sync(&channel->work), it does not lead to a UAF.
> 
> On the contrary, if you call cancel_work_sync(&channel->work) first, the sysfs node has not been removed
> yet. When the sysfs node is written to (via store) again, it will schedule the work once more, rendering
> the cancel_work_sync call ineffective.

Ok, I missed this.

Thanks for explanation.

Cheers,
Biju

> > Maybe??
> >
> > rcar_gen3_control_otg_irq(channel, 0);
> > cancel_work_sync(&channel->work); device_remove_file(&pdev->dev,
> > &dev_attr_role);
> >
> > Cheers,
> > Biju
> >
> >
> >
> >
> >>
> >> Thanks?
> >> Pei.
> >>
> >>   > after device_remove_file.
> >>
> >>> Cheers,
> >>> Biju
> >>>
> >>>> +	}
> >>>>  }
> >>>>
> >>>>  static int rcar_gen3_phy_usb2_suspend(struct device *dev)
> >>>> --
> >>>> 2.25.1
> >>>>
> >


