mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem
@ 2026-09-19  6:54 Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 01/12] dt-bindings: phy: Add " Jan Petrous via B4 Relay
                   ` (11 more replies)
  0 siblings, 12 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS),
	Alexandru-Catalin Ionita, Ionut Vicovan, Bogdan Roman

This series continues the S32G SerDes/XPCS upstreaming started by Vincent
Guittot. The effort has moved in-house at NXP and I am carrying it
forward from his v2 [v2] below, rather than restarting from scratch.
Authorship of the patches originating from his series is preserved
through Co-developed-by and his Signed-off-by; the rest builds on top.
Thanks to Vincent for the v1 -> v2 groundwork.

The S32G SoC family integrates two SerDes subsystems. Each one is built
from a DesignWare PCIe controller, two DesignWare Ethernet XPCS
instances and a shared two-lane combo PHY (PMA). The two lanes are
multiplexed between the PCIe controller and the XPCS instances, so a
given SerDes instance can run PCIe only, SGMII only, or a combination of
both, depending on the selected subsystem working mode.

This series adds the device tree bindings, the SerDes subsystem PHY
driver that owns the shared PMA and sequences the bring-up, a
transport-agnostic XPCS PCS core shared with i.MX, the S32G XPCS glue,
the SGMII support in the dwmac-s32 MAC glue, and the device tree nodes
for S32G2 and S32G3 including two routing variants for the
S32G3-VNP-RDB3 board.

The lane multiplexing is the reason the XPCS is not a self-contained PCS
platform device: in the mixed PCIe+SGMII modes both XPCS instances share
the PMA with the PCIe lane, so the reset and bring-up ordering has to be
driven by the SerDes PHY driver, which owns the shared resources. The
MAC obtains its phylink PCS from the SerDes subsystem through the
standard pcs-handle property.

Only the 1.25 Gbit/s (1000BASE-X / SGMII) working modes are wired up
here. The 2500BASE-X register paths are present in the PCS but are not
enabled, as there is no in-tree consumer yet; they are added by a
follow-up series together with the per-lane capability the binding would
need to describe them.

Tested on an S32G3-VNP-RDB3 board, in SerDes mode 1 (SGMII alongside two
active PCIe root complexes) and in SerDes mode 3.

Compared to v2 this series is rebased onto current mainline master
(see base-commit at the end of this cover letter) instead of the tree
the previous revision was generated against.

Changes since v2:
- Restructured the series: the single "serdes subsystem" driver has been
  split into the SerDes PHY driver (drivers/phy/freescale) and the PCS
  side (drivers/net/pcs), and the PCS side is further split into a
  transport-agnostic XPCS core shared with i.MX plus the S32G-specific
  glue. The core operates on regmaps supplied by the platform glue, so
  the S32G indirect memory-mapped transport and the i.MX Clause 45 MDIO
  transport can share it.
- Binding: dropped the vendor nxp,sys-mode property. The
  SS_RW_REG_0[SUBSYS_MODE] value is now derived from the lane child
  nodes and the nxp,xpcs-instance routing, since each described working
  mode has a unique lane mux. The reference-clock rate is validated
  against the derived mode instead of selecting it.
- Binding: S32G2 and S32G3 now use distinct compatibles without
  fallback, because the reference-manual mode tables differ per SoC and
  per SerDes instance.
- Binding: only the PCIe lane child carries '#phy-cells'; an XPCS lane
  is referenced by the MAC through pcs-handle rather than as a generic
  PHY provider.
- Documented pcs-handle for nxp,s32-dwmac and fixed the SerDes PHY
  phandle in the nxp,s32g-pcie example.
- Added SGMII support to the dwmac-s32 glue (pcs_init / select_pcs) and
  fixed the PHY interface selector to handle SGMII, RGMII, RMII and MII.
- Converted the vendor driver busy-waits to read_poll_timeout().
- Added the S32G2/S32G3 SerDes, PCIe and SCMI reset controller DT nodes
  and the S32G3-RDB3 SerDes routing overlays.

Link: https://lore.kernel.org/all/20260203161917.1666696-1-vincent.guittot@linaro.org/ [v2]

Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
Clark Wang (1):
      net: pcs: add NXP SerDes XPCS shared core

Jan Petrous (OSS) (8):
      dt-bindings: phy: Add NXP S32G SerDes subsystem
      dt-bindings: net: nxp,s32-dwmac: Document pcs-handle
      dt-bindings: PCI: nxp,s32g-pcie: Fix SerDes PHY phandle in example
      net: stmmac: dwmac-s32: Add SGMII support
      arm64: dts: s32g: Add SCMI reset controller
      arm64: dts: s32g: Add SerDes controller nodes
      arm64: dts: s32g: Add PCIe controller nodes
      arm64: dts: s32g: Add S32G3-RDB3 SerDes routing variants

Vincent Guittot (3):
      net: pcs: Add NXP S32G XPCS driver
      phy: freescale: s32g: Add SerDes subsystem PHY
      MAINTAINERS: Add NXP S32G SerDes and SerDes xPCS core entries

 .../devicetree/bindings/net/nxp,s32-dwmac.yaml     |   11 +-
 .../devicetree/bindings/pci/nxp,s32g-pcie.yaml     |    3 +-
 .../devicetree/bindings/phy/nxp,s32g-serdes.yaml   |  258 +++++
 MAINTAINERS                                        |   23 +
 arch/arm64/boot/dts/freescale/Makefile             |    6 +
 arch/arm64/boot/dts/freescale/s32g2.dtsi           |  118 +++
 arch/arm64/boot/dts/freescale/s32g3.dtsi           |  118 +++
 .../dts/freescale/s32g399a-rdb3-serdes-mode1.dtso  |  111 ++
 .../dts/freescale/s32g399a-rdb3-serdes-mode3.dtso  |   60 ++
 drivers/net/ethernet/stmicro/stmmac/Kconfig        |    6 +
 drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c    |   74 +-
 drivers/net/pcs/Kconfig                            |   25 +
 drivers/net/pcs/Makefile                           |    2 +
 drivers/net/pcs/pcs-nxp-s32g-xpcs.c                |  947 ++++++++++++++++++
 drivers/net/pcs/pcs-nxp-serdes-xpcs.c              |  853 ++++++++++++++++
 drivers/phy/freescale/Kconfig                      |    9 +
 drivers/phy/freescale/Makefile                     |    1 +
 drivers/phy/freescale/phy-nxp-s32g-serdes.c        | 1057 ++++++++++++++++++++
 include/linux/pcs/pcs-nxp-s32g-xpcs.h              |   48 +
 include/linux/pcs/pcs-nxp-serdes-xpcs.h            |  100 ++
 include/linux/phy/nxp-s32g-serdes.h                |   25 +
 21 files changed, 3850 insertions(+), 5 deletions(-)
---
base-commit: 5dd1818b15d98d4a20806cd00b1b40320b06004f
change-id: 20260918-s32g_serdes-7807070de807

Best regards,
-- 
Jan Petrous (OSS) <jan.petrous@oss.nxp.com>



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

* [PATCH RFC v3 01/12] dt-bindings: phy: Add NXP S32G SerDes subsystem
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 02/12] dt-bindings: net: nxp,s32-dwmac: Document pcs-handle Jan Petrous via B4 Relay
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS)

From: "Jan Petrous (OSS)" <jan.petrous@oss.nxp.com>

The S32G SerDes subsystem multiplexes two lanes between a PCIe PHY and
two DesignWare XPCS instances. Describe it with one node per SerDes and
one child node per lane.

Compared to the previous revision, the vendor nxp,sys-mode property is
removed: the SS_RW_REG_0[SUBSYS_MODE] value is fully derivable from the
child-node lane mux and the XPCS instance routing (nxp,xpcs-instance).
Each working mode described here has a unique lane mux, so the mode is
a pure function of the child nodes. The reference-clock rate is
validated against the derived mode; it is not used to select it.

The 3.125 Gbit/s working modes are deliberately not described yet.
Distinguishing them from the 1.25 Gbit/s dual-XPCS mode requires a
per-lane 2500BASE-X capability that this binding does not express, and
the reference clock does not distinguish them either - both accept 100
or 125 MHz. They are added by the 2500BASE-X follow-up.

Only the PCIe lane child gets '#phy-cells'. An XPCS lane is not a
generic PHY provider; the Ethernet controller references it through the
standard pcs-handle property instead.

S32G2 and S32G3 use distinct compatibles without fallback because the
full reference-manual mode tables differ per SoC and per SerDes
instance.

Co-developed-by: Vincent Guittot <vincent.guittot@linaro.org>
Signed-off-by: Vincent Guittot <vincent.guittot@linaro.org>
Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 .../devicetree/bindings/phy/nxp,s32g-serdes.yaml   | 258 +++++++++++++++++++++
 1 file changed, 258 insertions(+)

diff --git a/Documentation/devicetree/bindings/phy/nxp,s32g-serdes.yaml b/Documentation/devicetree/bindings/phy/nxp,s32g-serdes.yaml
new file mode 100644
index 000000000000..6343d01bfde4
--- /dev/null
+++ b/Documentation/devicetree/bindings/phy/nxp,s32g-serdes.yaml
@@ -0,0 +1,258 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/phy/nxp,s32g-serdes.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: NXP S32G2xxx/S32G3xxx SerDes PHY subsystem
+
+maintainers:
+  - Ghennadi Procopciuc <ghennadi.procopciuc@nxp.com>
+  - Jan Petrous <jan.petrous@oss.nxp.com>
+
+description: |
+  The SerDes subsystem multiplexes two SerDes lanes between one PCIe
+  controller PHY and two Synopsys DesignWare XPCS (Ethernet PCS) instances,
+  behind a shared 2-lane combo PHY. The active routing is selected by the
+  SS_RW_REG_0[SUBSYS_MODE] field.
+
+  Reference-manual working modes described by this binding:
+
+  Mode  Lane0   Lane1   PHY refclk (MHz)  Description
+  ------------------------------------------------------------
+  0     PCIe    PCIe    100               PCIe x2
+  1     PCIe    XPCS0   100               PCIe x1 + SGMII
+  2     PCIe    XPCS1   100               PCIe x1 + SGMII
+  3     XPCS0   XPCS1   100 or 125        dual SGMII, 1.25 Gbit/s
+
+  Which Ethernet MAC an XPCS instance feeds is fixed by the SoC integration
+  and differs per SoC and per SerDes instance - on S32G3 SerDes_0, XPCS0
+  feeds GMAC0 and XPCS1 feeds PFE_MAC2, while on SerDes_1 the same two
+  instances feed PFE_MAC0 and PFE_MAC1. That mapping is a property of the
+  SoC, not of this binding, and is resolved by the driver.
+
+  The 3.125 Gbit/s working modes are not described here yet. Distinguishing
+  them from mode 3 requires a per-lane 2500BASE-X capability, which this
+  binding does not express; the reference clock does not distinguish them
+  either, as both accept 100 or 125 MHz. They are added by a follow-up.
+
+  SUBSYS_MODE is not encoded in the devicetree. It is derived at probe from
+  information already present in standard form:
+    - the lane mux, from the per-lane child nodes below;
+    - the XPCS instance a lane feeds, from nxp,xpcs-instance (this is what
+      distinguishes modes 1 and 2).
+  Each mode above has a unique lane mux, so the mode is a pure function of
+  the child nodes. The reference-clock rate is validated against the
+  derived mode; it is never used to select it.
+
+  Both lanes must always be described, even a lane the board does not wire
+  out to a connector. The lane mux is a property of the hardware
+  SUBSYS_MODE, not of the board routing: in mode 1, for example, the
+  subsystem internally routes lane 1 to XPCS0 whether or not the board
+  connects that lane to anything. The child nodes describe that fixed
+  hardware mux, so both must be present; omitting a lane leaves the mode
+  underivable and the probe fails with -EINVAL. Which of those lanes is
+  actually used by a given board is expressed elsewhere (the consumer's
+  phys / pcs-handle reference), not by leaving the lane out here.
+
+  The full reference-manual mode tables differ per SoC, which is why S32G2
+  and S32G3 use distinct compatibles without a fallback between them.
+
+  They also differ between the two SerDes instances of one SoC, so each
+  instance additionally carries a compatible naming it, with the per-SoC
+  string as fallback:
+
+    serdes0: compatible = "nxp,s32g3-serdes0", "nxp,s32g3-serdes";
+    serdes1: compatible = "nxp,s32g3-serdes1", "nxp,s32g3-serdes";
+
+  For the modes described here the two instances behave identically and a
+  driver need only match the fallback. They diverge outside this scope:
+  on S32G3 the 3.125 Gbit/s dual-XPCS mode 4 exists on SerDes_1 only, and
+  on S32G2 only SerDes_1 reaches 3.125 Gbit/s at all. Recording the
+  instance now means the follow-up that adds those modes is purely
+  additive - it matches the specific strings and needs no devicetree
+  change.
+
+  The same mechanism covers SoCs whose instances differ in kind rather
+  than in degree: on S32R47 one SerDes has no PCIe controller at all, so
+  its compatible must be distinguishable in order to reject a PCIe lane
+  child in schema rather than at probe time.
+
+properties:
+  compatible:
+    oneOf:
+      - items:
+          - enum:
+              - nxp,s32g2-serdes0
+              - nxp,s32g2-serdes1
+          - const: nxp,s32g2-serdes
+      - items:
+          - enum:
+              - nxp,s32g3-serdes0
+              - nxp,s32g3-serdes1
+          - const: nxp,s32g3-serdes
+
+  reg:
+    maxItems: 4
+
+  reg-names:
+    items:
+      - const: ss-pcie
+      - const: pcie-phy
+      - const: xpcs0
+      - const: xpcs1
+
+  clocks:
+    minItems: 4
+    maxItems: 5
+
+  clock-names:
+    minItems: 4
+    items:
+      - const: axi
+      - const: aux
+      - const: apb
+      - const: ref
+      - const: ext
+    description:
+      The combo PHY reference can be taken from the internal reference
+      clock ("ref") or from the external reference pad ("ext"). A board
+      that routes the external pad lists both; the external reference is
+      then the one used.
+
+  resets:
+    maxItems: 2
+
+  reset-names:
+    items:
+      - const: serdes
+      - const: pcie
+
+  '#address-cells':
+    const: 1
+
+  '#size-cells':
+    const: 0
+
+patternProperties:
+  '^phy@[01]$':
+    description: One SerDes lane. The unit address is the physical lane index.
+    type: object
+    additionalProperties: false
+
+    properties:
+      reg:
+        description: Physical lane index.
+        maximum: 1
+
+      compatible:
+        enum:
+          - nxp,s32g-serdes-pcie-phy
+          - nxp,s32g-serdes-xpcs
+
+      '#phy-cells':
+        const: 0
+
+      nxp,xpcs-instance:
+        $ref: /schemas/types.yaml#/definitions/uint32
+        enum: [0, 1]
+        description:
+          DesignWare XPCS instance this lane is routed to. Required for, and
+          only valid on, XPCS lanes. Distinguishes modes 1 and 2; for the
+          dual-XPCS mode the instance equals the lane index.
+
+    required:
+      - reg
+      - compatible
+
+    allOf:
+      - if:
+          properties:
+            compatible:
+              const: nxp,s32g-serdes-xpcs
+          required:
+            - compatible
+        then:
+          # An XPCS lane is not a generic PHY provider. It is referenced by
+          # the Ethernet controller through pcs-handle, not through phys.
+          required:
+            - nxp,xpcs-instance
+          properties:
+            '#phy-cells': false
+        else:
+          required:
+            - '#phy-cells'
+          properties:
+            nxp,xpcs-instance: false
+
+required:
+  - compatible
+  - reg
+  - reg-names
+  - clocks
+  - clock-names
+  - resets
+  - reset-names
+  - '#address-cells'
+  - '#size-cells'
+
+additionalProperties: false
+
+examples:
+  # PCIe x1 on lane 0 + 1G SGMII on lane 1 via XPCS0 (derived mode 1).
+  - |
+    serdes@40480000 {
+        compatible = "nxp,s32g3-serdes0", "nxp,s32g3-serdes";
+        reg = <0x40480000 0x108>,
+              <0x40483008 0x10>,
+              <0x40482000 0x800>,
+              <0x40482800 0x800>;
+        reg-names = "ss-pcie", "pcie-phy", "xpcs0", "xpcs1";
+        clocks = <&clks 1>, <&clks 2>, <&clks 3>, <&clks 4>;
+        clock-names = "axi", "aux", "apb", "ref";
+        resets = <&scmi_reset 1>, <&scmi_reset 0>;
+        reset-names = "serdes", "pcie";
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        phy@0 {
+            reg = <0>;
+            compatible = "nxp,s32g-serdes-pcie-phy";
+            #phy-cells = <0>;
+        };
+
+        phy@1 {
+            reg = <1>;
+            compatible = "nxp,s32g-serdes-xpcs";
+            nxp,xpcs-instance = <0>;
+        };
+    };
+
+  # Dual 1G SGMII (derived mode 3).
+  - |
+    serdes@44180000 {
+        compatible = "nxp,s32g3-serdes1", "nxp,s32g3-serdes";
+        reg = <0x44180000 0x108>,
+              <0x44183008 0x10>,
+              <0x44182000 0x800>,
+              <0x44182800 0x800>;
+        reg-names = "ss-pcie", "pcie-phy", "xpcs0", "xpcs1";
+        clocks = <&clks 1>, <&clks 2>, <&clks 3>, <&clks 4>;
+        clock-names = "axi", "aux", "apb", "ref";
+        resets = <&scmi_reset 11>, <&scmi_reset 10>;
+        reset-names = "serdes", "pcie";
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        phy@0 {
+            reg = <0>;
+            compatible = "nxp,s32g-serdes-xpcs";
+            nxp,xpcs-instance = <0>;
+        };
+
+        phy@1 {
+            reg = <1>;
+            compatible = "nxp,s32g-serdes-xpcs";
+            nxp,xpcs-instance = <1>;
+        };
+    };

-- 
2.55.0



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

* [PATCH RFC v3 02/12] dt-bindings: net: nxp,s32-dwmac: Document pcs-handle
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 01/12] dt-bindings: phy: Add " Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 03/12] dt-bindings: PCI: nxp,s32g-pcie: Fix SerDes PHY phandle in example Jan Petrous via B4 Relay
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS)

From: "Jan Petrous (OSS)" <jan.petrous@oss.nxp.com>

When the MAC output is routed to the embedded SerDes for SGMII, the PCS
is one of the SerDes XPCS instances. Document that it is referenced
through the standard pcs-handle property defined by
ethernet-controller.yaml, pointing at the SerDes XPCS lane child node.

The type already comes from the ethernet-controller.yaml reference in
the schema chain; this only records the intent, the same way
renesas,rzn1-gmac.yaml does.

Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 Documentation/devicetree/bindings/net/nxp,s32-dwmac.yaml | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/Documentation/devicetree/bindings/net/nxp,s32-dwmac.yaml b/Documentation/devicetree/bindings/net/nxp,s32-dwmac.yaml
index 753a04941659..b168decb540c 100644
--- a/Documentation/devicetree/bindings/net/nxp,s32-dwmac.yaml
+++ b/Documentation/devicetree/bindings/net/nxp,s32-dwmac.yaml
@@ -15,7 +15,8 @@ description:
   The SoC series S32G2xx and S32G3xx feature one DWMAC instance,
   the SoC S32R45 has two instances. The devices can use RGMII/RMII/MII
   interface over Pinctrl device or the output can be routed
-  to the embedded SerDes for SGMII connectivity.
+  to the embedded SerDes for SGMII connectivity, in which case the
+  SerDes XPCS lane acting as the PCS is referenced via pcs-handle.
   The DWMAC instances have connected all RX/TX queues interrupts,
   enabling load balancing of data traffic across all CPU cores.
 
@@ -81,6 +82,14 @@ properties:
       - const: rx
       - const: ptp_ref
 
+  pcs-handle:
+    $ref: /schemas/types.yaml#/definitions/phandle-array
+    maxItems: 1
+    description:
+      phandle pointing to the SerDes XPCS lane child node that provides
+      the SGMII PCS for this MAC. See
+      Documentation/devicetree/bindings/phy/nxp,s32g-serdes.yaml.
+
 required:
   - clocks
   - clock-names

-- 
2.55.0



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

* [PATCH RFC v3 03/12] dt-bindings: PCI: nxp,s32g-pcie: Fix SerDes PHY phandle in example
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 01/12] dt-bindings: phy: Add " Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 02/12] dt-bindings: net: nxp,s32-dwmac: Document pcs-handle Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 04/12] net: pcs: add NXP SerDes XPCS shared core Jan Petrous via B4 Relay
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS)

From: "Jan Petrous (OSS)" <jan.petrous@oss.nxp.com>

The example references the SerDes PHY as

  phys = <&serdes0 PHY_TYPE_PCIE 0 0>;

i.e. the SerDes node itself with three PHY cells. That does not match
the SerDes binding: the per-lane child node is the generic PHY
provider and carries '#phy-cells = <0>', while the SerDes node itself
has no '#phy-cells' at all.

Point the example at the PCIe lane child instead, which is also what
the S32G3 board device trees use. The dt-bindings/phy/phy.h include is
dropped along with the now-unused PHY_TYPE_PCIE constant.

The schema itself is unchanged; 'phys: maxItems: 1' already describes
the corrected form.

Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 Documentation/devicetree/bindings/pci/nxp,s32g-pcie.yaml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/Documentation/devicetree/bindings/pci/nxp,s32g-pcie.yaml b/Documentation/devicetree/bindings/pci/nxp,s32g-pcie.yaml
index 66a050028278..29323e50bd8b 100644
--- a/Documentation/devicetree/bindings/pci/nxp,s32g-pcie.yaml
+++ b/Documentation/devicetree/bindings/pci/nxp,s32g-pcie.yaml
@@ -82,7 +82,6 @@ unevaluatedProperties: false
 examples:
   - |
     #include <dt-bindings/interrupt-controller/arm-gic.h>
-    #include <dt-bindings/phy/phy.h>
 
     bus {
         #address-cells = <2>;
@@ -124,7 +123,7 @@ examples:
                 ranges;
 
                 device_type = "pci";
-                phys = <&serdes0 PHY_TYPE_PCIE 0 0>;
+                phys = <&serdes0_pcie0>;
             };
         };
     };

-- 
2.55.0



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

* [PATCH RFC v3 04/12] net: pcs: add NXP SerDes XPCS shared core
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
                   ` (2 preceding siblings ...)
  2026-09-19  6:54 ` [PATCH RFC v3 03/12] dt-bindings: PCI: nxp,s32g-pcie: Fix SerDes PHY phandle in example Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19 15:31   ` Maxime Chevallier
  2026-09-19  6:54 ` [PATCH RFC v3 05/12] net: pcs: Add NXP S32G XPCS driver Jan Petrous via B4 Relay
                   ` (7 subsequent siblings)
  11 siblings, 1 reply; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS)

From: Clark Wang <xiaoning.wang@nxp.com>

The SerDes subsystems found on NXP S32G and i.MX SoCs integrate a
Synopsys DesignWare Ethernet XPCS and an Ethernet PHY (PMA) with an
identical register layout. Only the register transport differs: indirect
memory mapped access on S32G, Clause 45 MDIO on i.MX.

Add a transport agnostic phylink PCS core which operates on regmaps
provided by the platform glue. It implements the phylink PCS operations,
the Clause 37 SGMII / 2500BASE-X / USXGMII / 10GBASE-R configuration and
link state handling, soft reset and polling helpers, and the common
feature sets. The platform specific register sequences, compatibility
tables and quirks are supplied through a per revision descriptor by the
platform drivers selecting this core.

The register sequences and the feature lists are derived from the
Synopsys pcs-xpcs driver, so its copyright is retained. A separate driver
is needed because struct dw_xpcs is built around an mdio_device and owns
the transport, the reset and the identification of a standalone PCS,
while here the PMA can be shared with a PCIe controller, the bring-up is
sequenced across both XPCS instances by the SerDes PHY driver, and the
transport is not always MDIO.

Tested on an S32G3-VNP-RDB3 board through the S32G platform glue added
by the next patch, in both SerDes working modes this series describes:
mode 1 (PCIe x1 + 1G SGMII) and mode 3 (dual SGMII). GMAC0 links at 1G
over XPCS0 and passes traffic in both.

Signed-off-by: Clark Wang <xiaoning.wang@nxp.com>
[jan.petrous: report in-band SGMII link down until BMSR_ANEGCOMPLETE]
Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 drivers/net/pcs/Kconfig                 |  11 +
 drivers/net/pcs/Makefile                |   1 +
 drivers/net/pcs/pcs-nxp-serdes-xpcs.c   | 853 ++++++++++++++++++++++++++++++++
 include/linux/pcs/pcs-nxp-serdes-xpcs.h | 100 ++++
 4 files changed, 965 insertions(+)

diff --git a/drivers/net/pcs/Kconfig b/drivers/net/pcs/Kconfig
index e417fd66f660..ec4af294003f 100644
--- a/drivers/net/pcs/Kconfig
+++ b/drivers/net/pcs/Kconfig
@@ -12,6 +12,17 @@ config PCS_XPCS
 	  This module provides a driver and helper functions for Synopsys
 	  DesignWare XPCS controllers.
 
+config PCS_NXP_SERDES_XPCS
+	tristate "NXP SerDes XPCS shared core"
+	select PHYLINK
+	select REGMAP
+	help
+	  Transport agnostic phylink PCS core for the Synopsys DesignWare
+	  Ethernet XPCS and Ethernet PHY of the SerDes subsystems found on
+	  NXP S32G and i.MX SoCs. The platform specific register transport
+	  (indirect MMIO on S32G, Clause 45 MDIO on i.MX) is implemented as
+	  regmap callbacks by the platform drivers selecting this core.
+
 config PCS_LYNX
 	tristate
 	help
diff --git a/drivers/net/pcs/Makefile b/drivers/net/pcs/Makefile
index 4f7920618b90..bbf063b18f1f 100644
--- a/drivers/net/pcs/Makefile
+++ b/drivers/net/pcs/Makefile
@@ -5,6 +5,7 @@ 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_NXP_SERDES_XPCS) += pcs-nxp-serdes-xpcs.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-nxp-serdes-xpcs.c b/drivers/net/pcs/pcs-nxp-serdes-xpcs.c
new file mode 100644
index 000000000000..cc739914496e
--- /dev/null
+++ b/drivers/net/pcs/pcs-nxp-serdes-xpcs.c
@@ -0,0 +1,853 @@
+// SPDX-License-Identifier: GPL-2.0
+/* NXP SerDes XPCS / XPCS-PHY shared core
+ *
+ * Transport agnostic phylink PCS driver for the Ethernet XPCS and Ethernet
+ * PHY (XPCS-PHY) of the SerDes subsystems found on NXP S32G and i.MX SoCs.
+ * The platform glue provides the regmaps and a per revision descriptor
+ * with the compatibility table, quirks and PMA programming sequences.
+ *
+ * The XPCS is a Synopsys DesignWare core, so the Clause 37 / 2500BASE-X /
+ * USXGMII / 10GBASE-R register sequences below are shared with the Synopsys
+ * pcs-xpcs driver and were derived from it. This is a separate driver rather
+ * than an extension of pcs-xpcs because struct dw_xpcs is built around an
+ * mdio_device: it owns the transport, the reset and the identification of a
+ * standalone PCS. That model does not fit these SerDes subsystems, where the
+ * PMA belongs to a combo PHY shared with a PCIe controller, the bring-up is
+ * sequenced across both XPCS instances by the PHY driver, and the register
+ * transport is either indirect MMIO or Clause 45 MDIO. What is left here is
+ * the phylink PCS layer on top of a regmap.
+ *
+ * Keep the register sequences in sync with drivers/net/pcs/pcs-xpcs.c when
+ * fixes apply to both.
+ *
+ * Copyright 2023-2026 NXP
+ * Copyright (c) 2020 Synopsys, Inc. and/or its affiliates.
+ */
+
+#include <linux/device.h>
+#include <linux/ethtool.h>
+#include <linux/iopoll.h>
+#include <linux/mdio.h>
+#include <linux/mii.h>
+#include <linux/module.h>
+#include <linux/pcs/pcs-nxp-serdes-xpcs.h>
+#include <linux/phylink.h>
+
+/* SR_MII USXGMII speed selection */
+#define DW_USXGMII_RST			BIT(10)
+#define DW_USXGMII_EN			BIT(9)
+#define DW_USXGMII_FULL			BIT(8)
+#define DW_USXGMII_SS_MASK		(BIT(13) | BIT(6) | BIT(5))
+#define DW_USXGMII_10000		(BIT(13) | BIT(6))
+#define DW_USXGMII_5000			(BIT(13) | BIT(5))
+#define DW_USXGMII_2500			(BIT(5))
+#define DW_USXGMII_1000			(BIT(6))
+#define DW_USXGMII_100			(BIT(13))
+#define DW_USXGMII_10			(0)
+
+/* VR MII MMD registers offsets */
+#define DW_VR_MII_DIG_CTRL1		0x8000
+#define DW_VR_MII_DIG_CTRL1_MAC_AUTO_SW		BIT(9)
+#define DW_VR_MII_DIG_CTRL1_2G5_EN		BIT(2)
+
+#define DW_VR_MII_AN_CTRL		0x8001
+#define DW_VR_MII_TX_CONFIG_MASK		BIT(3)
+#define DW_VR_MII_TX_CONFIG_MAC_SIDE_SGMII	0x0
+#define DW_VR_MII_PCS_MODE_MASK			GENMASK(2, 1)
+#define DW_VR_MII_PCS_MODE_C37_SGMII		0x2
+#define DW_VR_MII_AN_INTR_EN			BIT(0)
+
+#define DW_VR_MII_AN_INTR_STS		0x8002
+#define DW_VR_MII_AN_STS_C37_ANCMPLT_INTR	BIT(0)
+#define DW_VR_MII_AN_STS_C37_ANSGM_FD		BIT(1)
+#define DW_VR_MII_AN_STS_C37_ANSGM_SP		GENMASK(3, 2)
+#define DW_VR_MII_C37_ANSGM_SP_10		0x0
+#define DW_VR_MII_C37_ANSGM_SP_100		0x1
+#define DW_VR_MII_C37_ANSGM_SP_1000		0x2
+#define DW_VR_MII_C37_ANSGM_SP_LNKSTS		BIT(4)
+#define DW_VR_MII_USX_AN_STS_MASK		GENMASK(14, 8)
+
+#define DW_VR_MII_EEE_MCTRL0		0x8006
+#define DW_VR_MII_EEE_LTX_EN			BIT(0)  /* LPI Tx Enable */
+#define DW_VR_MII_EEE_LRX_EN			BIT(1)  /* LPI Rx Enable */
+#define DW_VR_MII_EEE_TX_QUIET_EN		BIT(2)  /* Tx Quiet Enable */
+#define DW_VR_MII_EEE_RX_QUIET_EN		BIT(3)  /* Rx Quiet Enable */
+#define DW_VR_MII_EEE_TX_EN_CTRL		BIT(4)  /* Tx Control Enable */
+#define DW_VR_MII_EEE_RX_EN_CTRL		BIT(7)  /* Rx Control Enable */
+#define DW_VR_MII_EEE_MULT_FACT_100NS		GENMASK(11, 8)
+
+#define DW_VR_MII_EEE_MCTRL1		0x800b
+#define DW_VR_MII_EEE_TRN_LPI		BIT(0)	/* Transparent Mode Enable */
+
+const int nxp_serdes_xpcs_usxgmii_features[] = {
+	ETHTOOL_LINK_MODE_Pause_BIT,
+	ETHTOOL_LINK_MODE_Asym_Pause_BIT,
+	ETHTOOL_LINK_MODE_Autoneg_BIT,
+	ETHTOOL_LINK_MODE_1000baseKX_Full_BIT,
+	ETHTOOL_LINK_MODE_10000baseKX4_Full_BIT,
+	ETHTOOL_LINK_MODE_10000baseKR_Full_BIT,
+	ETHTOOL_LINK_MODE_2500baseX_Full_BIT,
+	__ETHTOOL_LINK_MODE_MASK_NBITS,
+};
+EXPORT_SYMBOL_GPL(nxp_serdes_xpcs_usxgmii_features);
+
+const int nxp_serdes_xpcs_2500basex_features[] = {
+	ETHTOOL_LINK_MODE_Pause_BIT,
+	ETHTOOL_LINK_MODE_Asym_Pause_BIT,
+	ETHTOOL_LINK_MODE_Autoneg_BIT,
+	ETHTOOL_LINK_MODE_2500baseX_Full_BIT,
+	ETHTOOL_LINK_MODE_2500baseT_Full_BIT,
+	__ETHTOOL_LINK_MODE_MASK_NBITS,
+};
+EXPORT_SYMBOL_GPL(nxp_serdes_xpcs_2500basex_features);
+
+const int nxp_serdes_xpcs_sgmii_features[] = {
+	ETHTOOL_LINK_MODE_Pause_BIT,
+	ETHTOOL_LINK_MODE_Asym_Pause_BIT,
+	ETHTOOL_LINK_MODE_Autoneg_BIT,
+	ETHTOOL_LINK_MODE_10baseT_Half_BIT,
+	ETHTOOL_LINK_MODE_10baseT_Full_BIT,
+	ETHTOOL_LINK_MODE_100baseT_Half_BIT,
+	ETHTOOL_LINK_MODE_100baseT_Full_BIT,
+	ETHTOOL_LINK_MODE_1000baseT_Half_BIT,
+	ETHTOOL_LINK_MODE_1000baseT_Full_BIT,
+	__ETHTOOL_LINK_MODE_MASK_NBITS,
+};
+EXPORT_SYMBOL_GPL(nxp_serdes_xpcs_sgmii_features);
+
+static const struct nxp_serdes_xpcs_compat *
+nxp_serdes_xpcs_find_compat(struct nxp_serdes_xpcs *xpcs,
+			    phy_interface_t interface)
+{
+	const struct nxp_serdes_xpcs_compat *compat;
+
+	for (compat = xpcs->desc->compat; compat->supported; compat++)
+		if (compat->interface == interface)
+			return compat;
+
+	return NULL;
+}
+
+static void nxp_serdes_xpcs_get_interfaces(struct nxp_serdes_xpcs *xpcs,
+					   unsigned long *interfaces)
+{
+	const struct nxp_serdes_xpcs_compat *compat;
+
+	for (compat = xpcs->desc->compat; compat->supported; compat++)
+		__set_bit(compat->interface, interfaces);
+}
+
+int nxp_serdes_xpcs_read(struct nxp_serdes_xpcs *xpcs, u8 devad, u32 reg)
+{
+	unsigned int val;
+	int ret;
+
+	ret = regmap_read(xpcs->xpcs_regmap, (devad << 16) | (reg & 0xffff),
+			  &val);
+	if (ret < 0)
+		return ret;
+
+	return val;
+}
+EXPORT_SYMBOL_GPL(nxp_serdes_xpcs_read);
+
+int nxp_serdes_xpcs_write(struct nxp_serdes_xpcs *xpcs, u8 devad, u32 reg,
+			  u16 val)
+{
+	return regmap_write(xpcs->xpcs_regmap, (devad << 16) | (reg & 0xffff),
+			    val);
+}
+EXPORT_SYMBOL_GPL(nxp_serdes_xpcs_write);
+
+int nxp_serdes_xpcs_modify(struct nxp_serdes_xpcs *xpcs, u8 devad, u32 reg,
+			   u16 mask, u16 set)
+{
+	return regmap_update_bits(xpcs->xpcs_regmap,
+				  (devad << 16) | (reg & 0xffff), mask, set);
+}
+EXPORT_SYMBOL_GPL(nxp_serdes_xpcs_modify);
+
+static int nxp_serdes_xpcs_poll_reset(struct nxp_serdes_xpcs *xpcs, int dev)
+{
+	int ret, val;
+
+	ret = read_poll_timeout(nxp_serdes_xpcs_read, val,
+				val < 0 || !(val & BMCR_RESET),
+				50000, 600000, true, xpcs, dev, MII_BMCR);
+	if (val < 0)
+		ret = val;
+
+	return ret;
+}
+
+static int nxp_serdes_xpcs_soft_reset(struct nxp_serdes_xpcs *xpcs,
+				      const struct nxp_serdes_xpcs_compat *compat)
+{
+	int ret, dev;
+
+	switch (compat->an_mode) {
+	case NXP_SERDES_10GBASER:
+		dev = MDIO_MMD_PCS;
+		break;
+	case NXP_SERDES_AN_C37_SGMII:
+	case NXP_SERDES_AN_C37_USXGMII:
+	case NXP_SERDES_2500BASEX:
+		dev = MDIO_MMD_VEND2;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	ret = nxp_serdes_xpcs_write(xpcs, dev, MII_BMCR, BMCR_RESET);
+	if (ret < 0)
+		return ret;
+
+	return nxp_serdes_xpcs_poll_reset(xpcs, dev);
+}
+
+static int nxp_serdes_xpcs_validate(struct phylink_pcs *pcs,
+				    unsigned long *supported,
+				    const struct phylink_link_state *state)
+{
+	__ETHTOOL_DECLARE_LINK_MODE_MASK(xpcs_supported) = { 0, };
+	const struct nxp_serdes_xpcs_compat *compat;
+	struct nxp_serdes_xpcs *xpcs;
+	int i;
+
+	xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+	compat = nxp_serdes_xpcs_find_compat(xpcs, state->interface);
+	if (!compat)
+		return -EINVAL;
+
+	for (i = 0; compat->supported[i] != __ETHTOOL_LINK_MODE_MASK_NBITS; i++)
+		set_bit(compat->supported[i], xpcs_supported);
+
+	linkmode_and(supported, supported, xpcs_supported);
+
+	return 0;
+}
+
+static unsigned int nxp_serdes_xpcs_inband_caps(struct phylink_pcs *pcs,
+						phy_interface_t interface)
+{
+	struct nxp_serdes_xpcs *xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+	const struct nxp_serdes_xpcs_compat *compat;
+
+	compat = nxp_serdes_xpcs_find_compat(xpcs, interface);
+	if (!compat)
+		return 0;
+
+	switch (compat->an_mode) {
+	case NXP_SERDES_AN_C37_USXGMII:
+		return LINK_INBAND_ENABLE;
+
+	case NXP_SERDES_AN_C37_SGMII:
+		return LINK_INBAND_DISABLE | LINK_INBAND_ENABLE;
+
+	case NXP_SERDES_10GBASER:
+	case NXP_SERDES_2500BASEX:
+		return LINK_INBAND_DISABLE;
+
+	default:
+		return 0;
+	}
+}
+
+static void nxp_serdes_xpcs_disable(struct phylink_pcs *pcs)
+{
+	struct nxp_serdes_xpcs *xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+
+	if (xpcs->desc->reset)
+		xpcs->desc->reset(xpcs);
+}
+
+static void nxp_serdes_xpcs_pre_config(struct phylink_pcs *pcs,
+				       phy_interface_t interface)
+{
+	struct nxp_serdes_xpcs *xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+	const struct nxp_serdes_xpcs_compat *compat;
+	int ret;
+
+	if (xpcs->interface != interface) {
+		if (interface == PHY_INTERFACE_MODE_SGMII)
+			xpcs->need_reset = true;
+		xpcs->interface = interface;
+	}
+
+	if (xpcs->desc->quirks & NXP_SERDES_QUIRK_NO_POLL)
+		xpcs->pcs.poll = false;
+
+	if (xpcs->desc->quirks & NXP_SERDES_QUIRK_NO_SOFT_RESET)
+		xpcs->need_reset = false;
+
+	if (!xpcs->need_reset)
+		return;
+
+	compat = nxp_serdes_xpcs_find_compat(xpcs, interface);
+	if (!compat) {
+		dev_err(xpcs->dev, "unsupported interface %s\n",
+			phy_modes(interface));
+		return;
+	}
+
+	ret = nxp_serdes_xpcs_soft_reset(xpcs, compat);
+	if (ret)
+		dev_err(xpcs->dev, "soft reset failed: %pe\n", ERR_PTR(ret));
+
+	xpcs->need_reset = false;
+}
+
+static int nxp_serdes_xpcs_config_aneg_c37_sgmii(struct nxp_serdes_xpcs *xpcs,
+						 unsigned int neg_mode)
+{
+	int ret, mdio_ctrl;
+	u16 mask, val;
+
+	/* Disable AN while PCS_MODE and TX_CONFIG are switched to C37 SGMII on
+	 * the MAC side, then re-enable it for in-band mode. MAC_AUTO_SW lets
+	 * the hardware apply the negotiated speed and duplex.
+	 */
+	mdio_ctrl = nxp_serdes_xpcs_read(xpcs, MDIO_MMD_VEND2, MII_BMCR);
+	if (mdio_ctrl < 0)
+		return mdio_ctrl;
+
+	if (mdio_ctrl & BMCR_ANENABLE) {
+		ret = nxp_serdes_xpcs_write(xpcs, MDIO_MMD_VEND2, MII_BMCR,
+					    mdio_ctrl & ~BMCR_ANENABLE);
+		if (ret < 0)
+			return ret;
+	}
+
+	mask = DW_VR_MII_PCS_MODE_MASK | DW_VR_MII_TX_CONFIG_MASK;
+	val = FIELD_PREP(DW_VR_MII_PCS_MODE_MASK,
+			 DW_VR_MII_PCS_MODE_C37_SGMII);
+	val |= FIELD_PREP(DW_VR_MII_TX_CONFIG_MASK,
+			  DW_VR_MII_TX_CONFIG_MAC_SIDE_SGMII);
+
+	ret = nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_CTRL,
+				     mask, val);
+	if (ret < 0)
+		return ret;
+
+	val = 0;
+	mask = DW_VR_MII_DIG_CTRL1_2G5_EN | DW_VR_MII_DIG_CTRL1_MAC_AUTO_SW;
+
+	if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
+		val = DW_VR_MII_DIG_CTRL1_MAC_AUTO_SW;
+
+	ret = nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_VEND2, DW_VR_MII_DIG_CTRL1,
+				     mask, val);
+	if (ret < 0)
+		return ret;
+
+	if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
+		ret = nxp_serdes_xpcs_write(xpcs, MDIO_MMD_VEND2, MII_BMCR,
+					    mdio_ctrl | BMCR_ANENABLE);
+
+	return ret;
+}
+
+static int nxp_serdes_xpcs_config_2500basex(struct nxp_serdes_xpcs *xpcs)
+{
+	int ret;
+
+	ret = nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_VEND2, DW_VR_MII_DIG_CTRL1,
+				     DW_VR_MII_DIG_CTRL1_2G5_EN |
+				     DW_VR_MII_DIG_CTRL1_MAC_AUTO_SW,
+				     DW_VR_MII_DIG_CTRL1_2G5_EN);
+	if (ret < 0)
+		return ret;
+
+	return nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_VEND2, MII_BMCR,
+				      BMCR_ANENABLE | BMCR_SPEED1000 |
+				      BMCR_SPEED100, BMCR_SPEED1000);
+}
+
+static int nxp_serdes_xpcs_config_10gbaser(struct nxp_serdes_xpcs *xpcs)
+{
+	return nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_VEND2, DW_VR_MII_DIG_CTRL1,
+				      DW_VR_MII_DIG_CTRL1_MAC_AUTO_SW, 0);
+}
+
+static int nxp_serdes_xpcs_do_config(struct nxp_serdes_xpcs *xpcs,
+				     phy_interface_t interface,
+				     const unsigned long *advertising,
+				     unsigned int neg_mode)
+{
+	const struct nxp_serdes_xpcs_compat *compat;
+	int ret;
+
+	compat = nxp_serdes_xpcs_find_compat(xpcs, interface);
+	if (!compat)
+		return -ENODEV;
+
+	switch (compat->an_mode) {
+	case NXP_SERDES_10GBASER:
+		ret = nxp_serdes_xpcs_config_10gbaser(xpcs);
+		if (ret)
+			return ret;
+		break;
+	case NXP_SERDES_AN_C37_USXGMII:
+		break;
+	case NXP_SERDES_AN_C37_SGMII:
+		ret = nxp_serdes_xpcs_config_aneg_c37_sgmii(xpcs, neg_mode);
+		if (ret)
+			return ret;
+		break;
+	case NXP_SERDES_2500BASEX:
+		ret = nxp_serdes_xpcs_config_2500basex(xpcs);
+		if (ret)
+			return ret;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (compat->pma_config) {
+		ret = compat->pma_config(xpcs);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static int nxp_serdes_xpcs_config(struct phylink_pcs *pcs,
+				  unsigned int neg_mode,
+				  phy_interface_t interface,
+				  const unsigned long *advertising,
+				  bool permit_pause_to_mac)
+{
+	struct nxp_serdes_xpcs *xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+
+	return nxp_serdes_xpcs_do_config(xpcs, interface, advertising, neg_mode);
+}
+
+static int nxp_serdes_xpcs_get_state_c37_sgmii(struct nxp_serdes_xpcs *xpcs,
+					       unsigned int neg_mode,
+					       struct phylink_link_state *state)
+{
+	int ret;
+
+	state->link = false;
+	state->speed = SPEED_UNKNOWN;
+	state->duplex = DUPLEX_UNKNOWN;
+	state->pause = 0;
+
+	/* Until AN completes, the status still holds the previous SGMII word */
+	if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) {
+		ret = nxp_serdes_xpcs_read(xpcs, MDIO_MMD_VEND2, MII_BMSR);
+		if (ret < 0)
+			return ret;
+
+		if (!(ret & BMSR_ANEGCOMPLETE))
+			return 0;
+	}
+
+	ret = nxp_serdes_xpcs_read(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_INTR_STS);
+	if (ret < 0)
+		return ret;
+
+	state->an_complete = ret & DW_VR_MII_AN_STS_C37_ANCMPLT_INTR;
+
+	if (ret & DW_VR_MII_C37_ANSGM_SP_LNKSTS) {
+		int speed_value;
+
+		state->link = true;
+
+		speed_value = FIELD_GET(DW_VR_MII_AN_STS_C37_ANSGM_SP, ret);
+		if (speed_value == DW_VR_MII_C37_ANSGM_SP_1000)
+			state->speed = SPEED_1000;
+		else if (speed_value == DW_VR_MII_C37_ANSGM_SP_100)
+			state->speed = SPEED_100;
+		else
+			state->speed = SPEED_10;
+
+		if (ret & DW_VR_MII_AN_STS_C37_ANSGM_FD)
+			state->duplex = DUPLEX_FULL;
+		else
+			state->duplex = DUPLEX_HALF;
+	} else if (ret == DW_VR_MII_AN_STS_C37_ANCMPLT_INTR) {
+		int speed, duplex;
+
+		state->link = true;
+
+		speed = nxp_serdes_xpcs_read(xpcs, MDIO_MMD_VEND2, MII_BMCR);
+		if (speed < 0)
+			return speed;
+
+		speed &= BMCR_SPEED100 | BMCR_SPEED1000;
+		if (speed == BMCR_SPEED1000)
+			state->speed = SPEED_1000;
+		else if (speed == BMCR_SPEED100)
+			state->speed = SPEED_100;
+		else if (speed == 0)
+			state->speed = SPEED_10;
+
+		duplex = nxp_serdes_xpcs_read(xpcs, MDIO_MMD_VEND2,
+					      MII_ADVERTISE);
+		if (duplex < 0)
+			return duplex;
+
+		if (duplex & ADVERTISE_1000XFULL)
+			state->duplex = DUPLEX_FULL;
+		else if (duplex & ADVERTISE_1000XHALF)
+			state->duplex = DUPLEX_HALF;
+
+		nxp_serdes_xpcs_write(xpcs, MDIO_MMD_VEND2,
+				      DW_VR_MII_AN_INTR_STS, 0);
+	}
+
+	return 0;
+}
+
+static int nxp_serdes_xpcs_get_state_2500basex(struct nxp_serdes_xpcs *xpcs,
+					       struct phylink_link_state *state)
+{
+	int ret;
+
+	ret = nxp_serdes_xpcs_read(xpcs, MDIO_MMD_VEND2, MII_BMSR);
+	if (ret < 0) {
+		state->link = 0;
+		return ret;
+	}
+
+	state->link = !!(ret & BMSR_LSTATUS);
+	if (!state->link)
+		return 0;
+
+	state->speed = SPEED_2500;
+	state->pause |= MLO_PAUSE_TX | MLO_PAUSE_RX;
+	state->duplex = DUPLEX_FULL;
+
+	return 0;
+}
+
+static int nxp_serdes_xpcs_get_state_c37_usxgmii(struct nxp_serdes_xpcs *xpcs,
+						 struct phylink_link_state *state)
+{
+	u16 usxg_an_status;
+	int ret;
+
+	state->link = false;
+	state->speed = SPEED_UNKNOWN;
+	state->duplex = DUPLEX_UNKNOWN;
+	state->pause = 0;
+
+	ret = nxp_serdes_xpcs_read(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_INTR_STS);
+	if (ret < 0)
+		return ret;
+
+	usxg_an_status = FIELD_GET(DW_VR_MII_USX_AN_STS_MASK, ret);
+	if (!(usxg_an_status & BIT(6)))
+		return 0;
+
+	state->link = true;
+	state->an_complete = true;
+	phylink_decode_usxgmii_word(state, usxg_an_status << 7);
+
+	return 0;
+}
+
+static void nxp_serdes_xpcs_get_state(struct phylink_pcs *pcs,
+				      unsigned int neg_mode,
+				      struct phylink_link_state *state)
+{
+	struct nxp_serdes_xpcs *xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+	const struct nxp_serdes_xpcs_compat *compat;
+	int stat1;
+	int ret;
+
+	compat = nxp_serdes_xpcs_find_compat(xpcs, state->interface);
+	if (!compat)
+		return;
+
+	switch (compat->an_mode) {
+	case NXP_SERDES_10GBASER:
+		stat1 = nxp_serdes_xpcs_read(xpcs, MDIO_MMD_PCS, MDIO_STAT1);
+		if (stat1 < 0) {
+			state->link = false;
+			break;
+		}
+
+		if (stat1 & MDIO_STAT1_FAULT)
+			nxp_serdes_xpcs_do_config(xpcs, state->interface, NULL,
+						  PHYLINK_PCS_NEG_NONE);
+
+		state->link = !!(stat1 & MDIO_STAT1_LSTATUS);
+		if (state->link &&
+		    state->interface == PHY_INTERFACE_MODE_10GBASER) {
+			state->speed = SPEED_10000;
+			state->duplex = DUPLEX_FULL;
+		}
+		break;
+	case NXP_SERDES_AN_C37_USXGMII:
+		ret = nxp_serdes_xpcs_get_state_c37_usxgmii(xpcs, state);
+		if (ret)
+			dev_err(xpcs->dev, "%s returned %pe\n",
+				"nxp_serdes_xpcs_get_state_c37_usxgmii",
+				ERR_PTR(ret));
+		break;
+	case NXP_SERDES_AN_C37_SGMII:
+		ret = nxp_serdes_xpcs_get_state_c37_sgmii(xpcs, neg_mode, state);
+		if (ret)
+			dev_err(xpcs->dev, "%s returned %pe\n",
+				"nxp_serdes_xpcs_get_state_c37_sgmii",
+				ERR_PTR(ret));
+		break;
+	case NXP_SERDES_2500BASEX:
+		ret = nxp_serdes_xpcs_get_state_2500basex(xpcs, state);
+		if (ret)
+			dev_err(xpcs->dev, "%s returned %pe\n",
+				"nxp_serdes_xpcs_get_state_2500basex",
+				ERR_PTR(ret));
+		break;
+	default:
+		return;
+	}
+}
+
+static void nxp_serdes_xpcs_link_up_sgmii_1000basex(struct nxp_serdes_xpcs *xpcs,
+						    unsigned int neg_mode,
+						    phy_interface_t interface,
+						    int speed, int duplex)
+{
+	int ret;
+
+	if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
+		return;
+
+	if (interface == PHY_INTERFACE_MODE_1000BASEX) {
+		if (speed != SPEED_1000) {
+			dev_err(xpcs->dev, "%s: speed %dMbps not supported\n",
+				__func__, speed);
+			return;
+		}
+
+		if (duplex != DUPLEX_FULL)
+			dev_err(xpcs->dev, "%s: half duplex not supported\n",
+				__func__);
+	}
+
+	ret = nxp_serdes_xpcs_write(xpcs, MDIO_MMD_VEND2, MII_BMCR,
+				    mii_bmcr_encode_fixed(speed, duplex));
+	if (ret)
+		dev_err(xpcs->dev, "%s: write returned %pe\n",
+			__func__, ERR_PTR(ret));
+}
+
+static void nxp_serdes_xpcs_link_up_usxgmii(struct nxp_serdes_xpcs *xpcs,
+					    int speed)
+{
+	int ret, speed_sel;
+
+	switch (speed) {
+	case SPEED_10:
+		speed_sel = DW_USXGMII_10;
+		break;
+	case SPEED_100:
+		speed_sel = DW_USXGMII_100;
+		break;
+	case SPEED_1000:
+		speed_sel = DW_USXGMII_1000;
+		break;
+	case SPEED_2500:
+		speed_sel = DW_USXGMII_2500;
+		break;
+	case SPEED_5000:
+		speed_sel = DW_USXGMII_5000;
+		break;
+	case SPEED_10000:
+		speed_sel = DW_USXGMII_10000;
+		break;
+	default:
+		return;
+	}
+
+	ret = nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_PCS, BIT(15) | MDIO_CTRL1,
+				     DW_USXGMII_EN, DW_USXGMII_EN);
+	if (ret < 0)
+		goto out;
+
+	ret = nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_VEND2, MII_BMCR,
+				     DW_USXGMII_SS_MASK,
+				     speed_sel | DW_USXGMII_FULL);
+	if (ret < 0)
+		goto out;
+
+	ret = nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_PCS, BIT(15) | MDIO_CTRL1,
+				     DW_USXGMII_RST, DW_USXGMII_RST);
+	if (ret < 0)
+		goto out;
+
+	return;
+
+out:
+	dev_err(xpcs->dev, "%s: XPCS access returned %pe\n",
+		__func__, ERR_PTR(ret));
+}
+
+static void nxp_serdes_xpcs_link_up(struct phylink_pcs *pcs,
+				    unsigned int neg_mode,
+				    phy_interface_t interface, int speed,
+				    int duplex)
+{
+	struct nxp_serdes_xpcs *xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+	const struct nxp_serdes_xpcs_compat *compat;
+	int ret;
+
+	switch (interface) {
+	case PHY_INTERFACE_MODE_USXGMII:
+		nxp_serdes_xpcs_link_up_usxgmii(xpcs, speed);
+		break;
+
+	case PHY_INTERFACE_MODE_SGMII:
+	case PHY_INTERFACE_MODE_1000BASEX:
+		nxp_serdes_xpcs_link_up_sgmii_1000basex(xpcs, neg_mode,
+							interface, speed,
+							duplex);
+		break;
+
+	default:
+		break;
+	}
+
+	compat = nxp_serdes_xpcs_find_compat(xpcs, interface);
+	if (compat && compat->pma_link_up) {
+		ret = compat->pma_link_up(xpcs, speed, duplex);
+		if (ret)
+			dev_err(xpcs->dev, "%s: pma_link_up returned %pe\n",
+				__func__, ERR_PTR(ret));
+	}
+}
+
+static void nxp_serdes_xpcs_an_restart(struct phylink_pcs *pcs)
+{
+	struct nxp_serdes_xpcs *xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+
+	nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_VEND2, MII_BMCR, BMCR_ANRESTART,
+			       BMCR_ANRESTART);
+}
+
+static int nxp_serdes_xpcs_config_eee(struct nxp_serdes_xpcs *xpcs,
+				      bool enable)
+{
+	u16 mask, val;
+	int ret;
+
+	mask = DW_VR_MII_EEE_LTX_EN | DW_VR_MII_EEE_LRX_EN |
+	       DW_VR_MII_EEE_TX_QUIET_EN | DW_VR_MII_EEE_RX_QUIET_EN |
+	       DW_VR_MII_EEE_TX_EN_CTRL | DW_VR_MII_EEE_RX_EN_CTRL |
+	       DW_VR_MII_EEE_MULT_FACT_100NS;
+
+	if (enable)
+		val = DW_VR_MII_EEE_LTX_EN | DW_VR_MII_EEE_LRX_EN |
+		      DW_VR_MII_EEE_TX_QUIET_EN | DW_VR_MII_EEE_RX_QUIET_EN |
+		      DW_VR_MII_EEE_TX_EN_CTRL | DW_VR_MII_EEE_RX_EN_CTRL |
+		      /* No platform needs a non-default multiply factor */
+		      FIELD_PREP(DW_VR_MII_EEE_MULT_FACT_100NS, 0);
+	else
+		val = 0;
+
+	ret = nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_VEND2,
+				     DW_VR_MII_EEE_MCTRL0, mask, val);
+	if (ret < 0)
+		return ret;
+
+	return nxp_serdes_xpcs_modify(xpcs, MDIO_MMD_VEND2,
+				      DW_VR_MII_EEE_MCTRL1,
+				      DW_VR_MII_EEE_TRN_LPI,
+				      enable ? DW_VR_MII_EEE_TRN_LPI : 0);
+}
+
+static void nxp_serdes_xpcs_disable_eee(struct phylink_pcs *pcs)
+{
+	struct nxp_serdes_xpcs *xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+
+	if (xpcs->desc->quirks & NXP_SERDES_QUIRK_NO_EEE)
+		return;
+
+	nxp_serdes_xpcs_config_eee(xpcs, false);
+}
+
+static void nxp_serdes_xpcs_enable_eee(struct phylink_pcs *pcs)
+{
+	struct nxp_serdes_xpcs *xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+
+	if (xpcs->desc->quirks & NXP_SERDES_QUIRK_NO_EEE)
+		return;
+
+	nxp_serdes_xpcs_config_eee(xpcs, true);
+}
+
+static const struct phylink_pcs_ops nxp_serdes_xpcs_phylink_ops = {
+	.pcs_validate = nxp_serdes_xpcs_validate,
+	.pcs_inband_caps = nxp_serdes_xpcs_inband_caps,
+	.pcs_pre_config = nxp_serdes_xpcs_pre_config,
+	.pcs_disable = nxp_serdes_xpcs_disable,
+	.pcs_config = nxp_serdes_xpcs_config,
+	.pcs_get_state = nxp_serdes_xpcs_get_state,
+	.pcs_an_restart = nxp_serdes_xpcs_an_restart,
+	.pcs_link_up = nxp_serdes_xpcs_link_up,
+	.pcs_disable_eee = nxp_serdes_xpcs_disable_eee,
+	.pcs_enable_eee = nxp_serdes_xpcs_enable_eee,
+};
+
+/**
+ * nxp_serdes_xpcs_create() - create a NXP SerDes XPCS phylink PCS
+ * @dev: device the PCS belongs to, used for error reporting
+ * @xpcs_regmap: regmap of the XPCS registers
+ * @phy_regmap: regmap of the XPCS-PHY registers, may be NULL
+ * @desc: revision descriptor, see &struct nxp_serdes_xpcs_desc
+ * @portid: index of the XPCS port, used to select the shared MPLL
+ * @interface: PHY interface mode used for the link
+ *
+ * The regmaps must stay valid until nxp_serdes_xpcs_destroy() has been called.
+ *
+ * Return: a pointer to a &struct phylink_pcs, or an ERR_PTR() on failure.
+ */
+struct phylink_pcs *nxp_serdes_xpcs_create(struct device *dev,
+					   struct regmap *xpcs_regmap,
+					   struct regmap *phy_regmap,
+					   const struct nxp_serdes_xpcs_desc *desc,
+					   u8 portid, phy_interface_t interface)
+{
+	struct nxp_serdes_xpcs *xpcs;
+
+	if (!desc || !desc->compat)
+		return ERR_PTR(-EINVAL);
+
+	xpcs = kzalloc_obj(struct nxp_serdes_xpcs, GFP_KERNEL);
+	if (!xpcs)
+		return ERR_PTR(-ENOMEM);
+
+	xpcs->dev = dev;
+	xpcs->xpcs_regmap = xpcs_regmap;
+	xpcs->phy_regmap = phy_regmap;
+	xpcs->desc = desc;
+	xpcs->pma_id = desc->pma_id;
+	xpcs->portid = portid;
+	xpcs->interface = interface;
+	xpcs->pcs.ops = &nxp_serdes_xpcs_phylink_ops;
+	xpcs->pcs.poll = true;
+	nxp_serdes_xpcs_get_interfaces(xpcs, xpcs->pcs.supported_interfaces);
+	xpcs->need_reset = true;
+
+	return &xpcs->pcs;
+}
+EXPORT_SYMBOL_GPL(nxp_serdes_xpcs_create);
+
+void nxp_serdes_xpcs_destroy(struct phylink_pcs *pcs)
+{
+	struct nxp_serdes_xpcs *xpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+
+	if (!xpcs)
+		return;
+
+	kfree(xpcs);
+}
+EXPORT_SYMBOL_GPL(nxp_serdes_xpcs_destroy);
+
+MODULE_DESCRIPTION("NXP SerDes XPCS shared core");
+MODULE_AUTHOR("Clark Wang <xiaoning.wang@nxp.com>");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/pcs/pcs-nxp-serdes-xpcs.h b/include/linux/pcs/pcs-nxp-serdes-xpcs.h
new file mode 100644
index 000000000000..2579c6b14a61
--- /dev/null
+++ b/include/linux/pcs/pcs-nxp-serdes-xpcs.h
@@ -0,0 +1,100 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * NXP SerDes XPCS shared core
+ * Copyright 2026 NXP
+ */
+
+#ifndef __LINUX_PCS_NXP_SERDES_XPCS_H
+#define __LINUX_PCS_NXP_SERDES_XPCS_H
+
+#include <linux/phy.h>
+#include <linux/phylink.h>
+#include <linux/regmap.h>
+
+/* AN mode values as defined by the DesignWare XPCS */
+enum nxp_serdes_an_mode {
+	NXP_SERDES_AN_C37_SGMII = 2,
+	NXP_SERDES_2500BASEX = 3,
+	NXP_SERDES_10GBASER = 5,
+	NXP_SERDES_AN_C37_USXGMII = 6,
+};
+
+/* The revision runs its own reset sequence, skip the soft reset */
+#define NXP_SERDES_QUIRK_NO_SOFT_RESET	BIT(0)
+/* The revision reports the link state to the MAC, do not poll the PCS */
+#define NXP_SERDES_QUIRK_NO_POLL	BIT(1)
+/* The revision does not implement the VR_MII_EEE_MCTRL0/1 registers */
+#define NXP_SERDES_QUIRK_NO_EEE		BIT(2)
+
+struct nxp_serdes_xpcs;
+
+struct nxp_serdes_xpcs_compat {
+	phy_interface_t interface;
+	const int *supported;
+	enum nxp_serdes_an_mode an_mode;
+	int (*pma_config)(struct nxp_serdes_xpcs *xpcs);
+	int (*pma_link_up)(struct nxp_serdes_xpcs *xpcs, int speed,
+			   int duplex);
+};
+
+/* Description of one SerDes revision, provided by the platform glue */
+struct nxp_serdes_xpcs_desc {
+	const char *name;
+	u32 pma_id;
+	const struct nxp_serdes_xpcs_compat *compat;
+	unsigned int quirks;
+	void (*reset)(struct nxp_serdes_xpcs *xpcs);
+};
+
+struct nxp_serdes_xpcs {
+	struct device *dev;
+	struct regmap *xpcs_regmap;
+	struct regmap *phy_regmap;
+	const struct nxp_serdes_xpcs_desc *desc;
+	struct phylink_pcs pcs;
+	phy_interface_t interface;
+	u32 pma_id;
+	u8 portid;
+	bool need_reset;
+	bool sgmii_internal_ref_clk;
+	void *priv;
+};
+
+#define phylink_pcs_to_nxp_serdes_xpcs(pl_pcs) \
+	container_of((pl_pcs), struct nxp_serdes_xpcs, pcs)
+
+static inline void nxp_serdes_xpcs_set_drvdata(struct nxp_serdes_xpcs *xpcs,
+					       void *priv)
+{
+	xpcs->priv = priv;
+}
+
+static inline void *nxp_serdes_xpcs_get_drvdata(struct nxp_serdes_xpcs *xpcs)
+{
+	return xpcs->priv;
+}
+
+/* Feature sets for struct nxp_serdes_xpcs_compat::supported */
+extern const int nxp_serdes_xpcs_usxgmii_features[];
+extern const int nxp_serdes_xpcs_2500basex_features[];
+extern const int nxp_serdes_xpcs_sgmii_features[];
+
+struct phylink_pcs *nxp_serdes_xpcs_create(struct device *dev,
+					   struct regmap *xpcs_regmap,
+					   struct regmap *phy_regmap,
+					   const struct nxp_serdes_xpcs_desc *desc,
+					   u8 portid,
+					   phy_interface_t interface);
+void nxp_serdes_xpcs_destroy(struct phylink_pcs *pcs);
+
+/* Register accesses are encoded in the regmap as "(devad << 16) | reg", with
+ * "devad" the Clause 45 device address and "reg" the 16-bit register address.
+ * Decoding both fields is up to the regmap of the platform glue.
+ */
+int nxp_serdes_xpcs_read(struct nxp_serdes_xpcs *xpcs, u8 devad, u32 reg);
+int nxp_serdes_xpcs_write(struct nxp_serdes_xpcs *xpcs, u8 devad, u32 reg,
+			  u16 val);
+int nxp_serdes_xpcs_modify(struct nxp_serdes_xpcs *xpcs, u8 devad, u32 reg,
+			   u16 mask, u16 set);
+
+#endif /* __LINUX_PCS_NXP_SERDES_XPCS_H */

-- 
2.55.0



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

* [PATCH RFC v3 05/12] net: pcs: Add NXP S32G XPCS driver
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
                   ` (3 preceding siblings ...)
  2026-09-19  6:54 ` [PATCH RFC v3 04/12] net: pcs: add NXP SerDes XPCS shared core Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 06/12] phy: freescale: s32g: Add SerDes subsystem PHY Jan Petrous via B4 Relay
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS),
	Alexandru-Catalin Ionita, Ionut Vicovan, Bogdan Roman

From: Vincent Guittot <vincent.guittot@linaro.org>

The S32G SoC family includes two SerDes subsystems, each made of one
PCIe controller, two DesignWare XPCS instances and a shared 2-lane
combo PHY. Add the phylink PCS driver for the XPCS instances, with 1G
SGMII support.

The XPCS PMA/PLL bring-up is a property of the shared combo PHY: in the
PCIe+SGMII modes the two XPCS instances share the PHY with the PCIe
lane, so the XPCS cannot be a self-contained PCS platform device that
owns its own reset/bring-up sequence. This driver therefore exposes a
small init/bring-up API which the SerDes PHY driver sequences (it owns
the shared PMA and enforces the required ordering, e.g. XPCS1 before
XPCS0 in the dual-SGMII mode), while the phylink_pcs ops consumed by the
MAC live here in the PCS driver. The MAC obtains its phylink PCS via
s32g_serdes_pcs_create().

The busy-waits of the vendor driver are converted to
read_poll_timeout(); the PMA is only touched after the SerDes PHY
reports operational (MPLL_STATE), which the SerDes driver guarantees.

2.5G (2500BASE-X) register paths are retained but not wired up; they
are enabled by a follow-up series, as there is currently no in-tree
consumer of the 2.5G links.

Tested on an S32G3-VNP-RDB3 board in both SerDes working modes this
series describes: mode 1 (PCIe x1 on lane 0 + 1G SGMII via XPCS0 on
lane 1) and mode 3 (dual SGMII, XPCS0 on lane 0 and XPCS1 on lane 1).
GMAC0 links at 1G over XPCS0 and passes traffic in both.

Signed-off-by: Vincent Guittot <vincent.guittot@linaro.org>
Co-developed-by: Ciprian Marian Costea <ciprianmarian.costea@oss.nxp.com>
Signed-off-by: Ciprian Marian Costea <ciprianmarian.costea@oss.nxp.com>
Co-developed-by: Alexandru-Catalin Ionita <alexandru-catalin.ionita@nxp.com>
Signed-off-by: Alexandru-Catalin Ionita <alexandru-catalin.ionita@nxp.com>
Co-developed-by: Ghennadi Procopciuc <ghennadi.procopciuc@nxp.com>
Signed-off-by: Ghennadi Procopciuc <ghennadi.procopciuc@nxp.com>
Co-developed-by: Ionut Vicovan <Ionut.Vicovan@nxp.com>
Signed-off-by: Ionut Vicovan <Ionut.Vicovan@nxp.com>
Co-developed-by: Bogdan Roman <bogdan-gabriel.roman@nxp.com>
Signed-off-by: Bogdan Roman <bogdan-gabriel.roman@nxp.com>
Co-developed-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 drivers/net/pcs/Kconfig               |  14 +
 drivers/net/pcs/Makefile              |   1 +
 drivers/net/pcs/pcs-nxp-s32g-xpcs.c   | 947 ++++++++++++++++++++++++++++++++++
 include/linux/pcs/pcs-nxp-s32g-xpcs.h |  48 ++
 4 files changed, 1010 insertions(+)

diff --git a/drivers/net/pcs/Kconfig b/drivers/net/pcs/Kconfig
index ec4af294003f..7a4c0da406c9 100644
--- a/drivers/net/pcs/Kconfig
+++ b/drivers/net/pcs/Kconfig
@@ -46,4 +46,18 @@ config PCS_RZN1_MIIC
 	  Renesas RZ/N1, RZ/N2H, and RZ/T2H SoCs. This PCS converts MII to
 	  RMII/RGMII, or can be set in pass-through mode for MII.
 
+config PCS_NXP_S32G_XPCS
+	tristate "NXP S32G XPCS support"
+	select PHYLINK
+	select REGMAP
+	select PCS_NXP_SERDES_XPCS
+	help
+	  This option enables the NXP S32G SerDes XPCS platform glue. It
+	  provides the indirect-MMIO register transport and the S32G PMA/PLL
+	  bring-up sequences on top of the shared NXP SerDes xPCS core
+	  (PCS_NXP_SERDES_XPCS), which supplies the phylink PCS layer for the
+	  SGMII / 2500BASE-X lanes of the shared SerDes combo PHY. It is
+	  driven by the S32G SerDes PHY driver. Say Y or M here if you need
+	  SGMII Ethernet on an NXP S32G2 or S32G3 SoC.
+
 endmenu
diff --git a/drivers/net/pcs/Makefile b/drivers/net/pcs/Makefile
index bbf063b18f1f..337e9b503553 100644
--- a/drivers/net/pcs/Makefile
+++ b/drivers/net/pcs/Makefile
@@ -9,3 +9,4 @@ obj-$(CONFIG_PCS_NXP_SERDES_XPCS) += pcs-nxp-serdes-xpcs.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
+obj-$(CONFIG_PCS_NXP_S32G_XPCS)	+= pcs-nxp-s32g-xpcs.o
diff --git a/drivers/net/pcs/pcs-nxp-s32g-xpcs.c b/drivers/net/pcs/pcs-nxp-s32g-xpcs.c
new file mode 100644
index 000000000000..a44f1da54429
--- /dev/null
+++ b/drivers/net/pcs/pcs-nxp-s32g-xpcs.c
@@ -0,0 +1,947 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2021-2026 NXP
+ *
+ * S32G SerDes XPCS platform glue for the NXP SerDes xPCS shared core.
+ *
+ * This module no longer implements its own phylink PCS. It provides the
+ * S32G specific parts the shared core (pcs-nxp-serdes-xpcs.c) consumes:
+ *   - the indirect-MMIO regmap transport. The S32G flat register address
+ *     0x1fXXXX is already identical to the shared core's regmap key
+ *     "(devad << 16) | reg" (devad 0x1f == MDIO_MMD_VEND2 for the VR_MII
+ *     page, devad 0x1f reg 0x0000 == SR_MII_CTRL/MII_BMCR), so the same
+ *     regmap serves both the S32G PMA sequences below and the shared core.
+ *   - the S32G PMA/PLL bring-up sequences, exported to the SerDes PHY
+ *     driver which drives the ordered dual-XPCS bring-up barrier.
+ *   - a struct nxp_serdes_xpcs_desc describing the S32G revision, with a
+ *     compat[] table (C37 SGMII + 2500BASE-X) and two pma_config() hooks
+ *     (SGMII link-timer / CL37 override, and the 1G<->2.5G PLL switch).
+ */
+
+#include <linux/bitfield.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/pcs/pcs-nxp-s32g-xpcs.h>
+#include <linux/pcs/pcs-nxp-serdes-xpcs.h>
+#include <linux/phy.h>
+#include <linux/phylink.h>
+#include <linux/regmap.h>
+#include <linux/units.h>
+
+enum s32g_xpcs_pll {
+	S32G_XPCS_PLLA,	/* Slow PLL */
+	S32G_XPCS_PLLB,	/* Fast PLL */
+};
+
+struct s32g_xpcs {
+	void __iomem *base;
+	struct device *dev;
+	unsigned char id;
+	struct regmap *regmap;
+	enum s32g_xpcs_pll ref;
+	bool ext_clk;
+	bool mhz125;
+	enum s32g_xpcs_shared pcie_shared;
+	/* phylink PCS owned by the shared core, created in s32g_xpcs_create() */
+	struct phylink_pcs *pcs;
+};
+
+#define XPCS_TIMEOUT_US				(300 * USEC_PER_MSEC)
+#define XPCS_POLL_SLEEP_US			100
+
+#define ADDR1_OFS				0x3fc
+
+#define SR_MII_CTRL				0x1f0000
+#define   SS13					BIT(13)
+#define   AN_ENABLE				BIT(12)
+#define   RESTART_AN				BIT(9)
+#define   DUPLEX_MODE				BIT(8)
+#define   SS6					BIT(6)
+#define SR_MII_STS				0x1f0001
+#define   LINK_STS				BIT(2)
+#define VR_MII_DIG_CTRL1			0x1f8000
+#define   BYP_PWRUP				BIT(1)
+#define   EN_2_5G_MODE				BIT(2)
+#define   CL37_TMR_OVRRIDE			BIT(3)
+#define   INIT					BIT(8)
+#define   MAC_AUTO_SW				BIT(9)
+#define   VR_RST				BIT(15)
+#define VR_MII_AN_CTRL				0x1f8001
+#define   MII_AN_INTR_EN			BIT(0)
+#define   PCS_MODE_MASK				GENMASK(2, 1)
+#define    PCS_MODE_SGMII			2
+#define   MII_CTRL				BIT(8)
+#define VR_MII_AN_INTR_STS			0x1f8002
+#define  CL37_ANCMPLT_INTR			BIT(0)
+#define  CL37_ANSGM_STS_DUPLEX			BIT(1)
+#define  CL37_ANSGM_STS_SPEED_MASK		GENMASK(3, 2)
+#define   CL37_ANSGM_10MBPS			0
+#define   CL37_ANSGM_100MBPS			1
+#define   CL37_ANSGM_1000MBPS			2
+#define  CL37_ANSGM_STS_LINK			BIT(4)
+#define VR_MII_DBG_CTRL				0x1f8005
+#define   SUPPRESS_LOS_DET			BIT(4)
+#define   RX_DT_EN_CTL				BIT(6)
+#define VR_MII_LINK_TIMER_CTRL			0x1f800a
+#define VR_MII_DIG_STS				0x1f8010
+#define   PSEQ_STATE_MASK			GENMASK(4, 2)
+#define     POWER_GOOD_STATE			0x4
+#define VR_MII_GEN5_12G_16G_TX_GENCTRL1		0x1f8031
+#define   TX_CLK_RDY_0				BIT(12)
+#define	VR_MII_GEN5_12G_16G_TX_GENCTRL2		0x1f8032
+#define	  TX_REQ_0				BIT(0)
+#define VR_MII_GEN5_12G_16G_TX_RATE_CTRL	0x1f8034
+#define   TX0_RATE_MASK				GENMASK(2, 0)
+#define     TX0_BAUD_DIV_1			0
+#define     TX0_BAUD_DIV_4			2
+#define VR_MII_GEN5_12G_16G_TX_EQ_CTRL0		0x1f8036
+#define   TX_EQ_MAIN_MASK			GENMASK(13, 8)
+#define VR_MII_GEN5_12G_16G_TX_EQ_CTRL1		0x1f8037
+#define   TX_EQ_OVR_RIDE			BIT(6)
+#define VR_MII_CONSUMER_10G_TX_TERM_CTRL	0x1f803c
+#define   TX0_TERM_MASK				GENMASK(2, 0)
+#define VR_MII_GEN5_12G_16G_RX_GENCTRL1		0x1f8051
+#define   RX_RST_0				BIT(4)
+#define VR_MII_GEN5_12G_16G_RX_GENCTRL2		0x1f8052
+#define   RX_REQ_0				BIT(0)
+#define VR_MII_GEN5_12G_16G_RX_RATE_CTRL	0x1f8054
+#define   RX0_RATE_MASK				GENMASK(1, 0)
+#define     RX0_BAUD_DIV_2			0x1
+#define     RX0_BAUD_DIV_8			0x3
+#define VR_MII_GEN5_12G_16G_CDR_CTRL		0x1f8056
+#define   VCO_LOW_FREQ_0			BIT(8)
+#define VR_MII_GEN5_12G_16G_MPLL_CMN_CTRL	0x1f8070
+#define   MPLLB_SEL_0				BIT(4)
+#define VR_MII_GEN5_12G_16G_MPLLA_CTRL0		0x1f8071
+#define   MPLLA_CAL_DISABLE			BIT(15)
+#define   MLLA_MULTIPLIER_MASK			GENMASK(7, 0)
+#define VR_MII_GEN5_12G_MPLLA_CTRL1		0x1f8072
+#define   MPLLA_FRACN_CTRL_MASK			GENMASK(15, 5)
+#define VR_MII_GEN5_12G_16G_MPLLA_CTRL2		0x1f8073
+#define   MPLLA_TX_CLK_DIV_MASK			GENMASK(13, 11)
+#define   MPLLA_DIV10_CLK_EN			BIT(9)
+#define VR_MII_GEN5_12G_16G_MPLLB_CTRL0		0x1f8074
+#define   MPLLB_CAL_DISABLE			BIT(15)
+#define   MLLB_MULTIPLIER_MASK			GENMASK(7, 0)
+#define VR_MII_GEN5_12G_MPLLB_CTRL1		0x1f8075
+#define   MPLLB_FRACN_CTRL_MASK			GENMASK(15, 5)
+#define VR_MII_GEN5_12G_16G_MPLLB_CTRL2		0x1f8076
+#define   MPLLB_TX_CLK_DIV_MASK			GENMASK(13, 11)
+#define   MPLLB_DIV10_CLK_EN			BIT(9)
+#define VR_MII_GEN5_12G_MPLLA_CTRL3		0x1f8077
+#define   MPLLA_BANDWIDTH_MASK			GENMASK(15, 0)
+#define VR_MII_GEN5_12G_MPLLB_CTRL3		0x1f8078
+#define   MPLLB_BANDWIDTH_MASK			GENMASK(15, 0)
+#define VR_MII_GEN5_12G_16G_MISC_CTRL0		0x1f8090
+#define   PLL_CTRL				BIT(15)
+#define VR_MII_GEN5_12G_16G_REF_CLK_CTRL	0x1f8091
+#define   REF_USE_PAD				BIT(1)
+#define   REF_CLK_DIV2				BIT(2)
+#define   REF_RANGE_MASK			GENMASK(5, 3)
+#define     RANGE_26_53_MHZ			0x1
+#define     RANGE_52_78_MHZ			0x2
+#define   REF_MPLLA_DIV2			BIT(6)
+#define   REF_MPLLB_DIV2			BIT(7)
+#define VR_MII_GEN5_12G_16G_VCO_CAL_LD0		0x1f8092
+#define   VCO_LD_VAL_0_MASK			GENMASK(12, 0)
+#define VR_MII_GEN5_12G_VCO_CAL_REF0		0x1f8096
+#define   VCO_REF_LD_0_MASK			GENMASK(5, 0)
+
+typedef bool (*xpcs_poll_func_t)(struct s32g_xpcs *);
+
+/*
+ * XPCS registers can't be accessed directly and an indirect address method
+ * must be used instead. The 32-bit "reg" is the flat XPCS address and is
+ * identical to the shared core's "(devad << 16) | reg" key.
+ */
+
+static const struct regmap_range s32g_xpcs_wr_ranges[] = {
+	regmap_reg_range(0x1f0000, 0x1f0000),
+	regmap_reg_range(0x1f0004, 0x1f0004),
+	regmap_reg_range(0x1f8000, 0x1f8003),
+	regmap_reg_range(0x1f8005, 0x1f8005),
+	regmap_reg_range(0x1f800a, 0x1f800a),
+	regmap_reg_range(0x1f8012, 0x1f8012),
+	regmap_reg_range(0x1f8015, 0x1f8015),
+	regmap_reg_range(0x1f8030, 0x1f8037),
+	regmap_reg_range(0x1f803c, 0x1f803e),
+	regmap_reg_range(0x1f8050, 0x1f8058),
+	regmap_reg_range(0x1f805c, 0x1f805e),
+	regmap_reg_range(0x1f8064, 0x1f8064),
+	regmap_reg_range(0x1f806b, 0x1f806b),
+	regmap_reg_range(0x1f8070, 0x1f8078),
+	regmap_reg_range(0x1f8090, 0x1f8092),
+	regmap_reg_range(0x1f8096, 0x1f8096),
+	regmap_reg_range(0x1f8099, 0x1f8099),
+	regmap_reg_range(0x1f80a0, 0x1f80a2),
+	regmap_reg_range(0x1f80e1, 0x1f80e1),
+};
+
+static const struct regmap_access_table s32g_xpcs_wr_table = {
+	.yes_ranges = s32g_xpcs_wr_ranges,
+	.n_yes_ranges = ARRAY_SIZE(s32g_xpcs_wr_ranges),
+};
+
+static const struct regmap_range s32g_xpcs_rd_ranges[] = {
+	regmap_reg_range(0x1f0000, 0x1f0006),
+	regmap_reg_range(0x1f000f, 0x1f000f),
+	regmap_reg_range(0x1f0708, 0x1f0710),
+	regmap_reg_range(0x1f8000, 0x1f8003),
+	regmap_reg_range(0x1f8005, 0x1f8005),
+	regmap_reg_range(0x1f800a, 0x1f800a),
+	regmap_reg_range(0x1f8010, 0x1f8012),
+	regmap_reg_range(0x1f8015, 0x1f8015),
+	regmap_reg_range(0x1f8018, 0x1f8018),
+	regmap_reg_range(0x1f8020, 0x1f8020),
+	regmap_reg_range(0x1f8030, 0x1f8037),
+	regmap_reg_range(0x1f803c, 0x1f803c),
+	regmap_reg_range(0x1f8040, 0x1f8040),
+	regmap_reg_range(0x1f8050, 0x1f8058),
+	regmap_reg_range(0x1f805c, 0x1f805e),
+	regmap_reg_range(0x1f8060, 0x1f8060),
+	regmap_reg_range(0x1f8064, 0x1f8064),
+	regmap_reg_range(0x1f806b, 0x1f806b),
+	regmap_reg_range(0x1f8070, 0x1f8078),
+	regmap_reg_range(0x1f8090, 0x1f8092),
+	regmap_reg_range(0x1f8096, 0x1f8096),
+	regmap_reg_range(0x1f8098, 0x1f8099),
+	regmap_reg_range(0x1f80a0, 0x1f80a2),
+	regmap_reg_range(0x1f80e1, 0x1f80e1),
+};
+
+static const struct regmap_access_table s32g_xpcs_rd_table = {
+	.yes_ranges = s32g_xpcs_rd_ranges,
+	.n_yes_ranges = ARRAY_SIZE(s32g_xpcs_rd_ranges),
+};
+
+static int s32g_xpcs_regmap_reg_read(void *context, unsigned int reg,
+				     unsigned int *result)
+{
+	struct s32g_xpcs *xpcs = context;
+	u16 ofsleft = (reg >> 8) & 0xffffU;
+	u16 ofsright = (reg & 0xffU);
+
+	writew(ofsleft, xpcs->base + ADDR1_OFS);
+	*result = readw(xpcs->base + (ofsright * 4));
+
+	return 0;
+}
+
+static int s32g_xpcs_regmap_reg_write(void *context, unsigned int reg,
+				      unsigned int val)
+{
+	struct s32g_xpcs *xpcs = context;
+	u16 ofsleft = (reg >> 8) & 0xffffU;
+	u16 ofsright = (reg & 0xffU);
+
+	writew(ofsleft, xpcs->base + ADDR1_OFS);
+	writew(val, xpcs->base + (ofsright * 4));
+
+	return 0;
+}
+
+static const struct regmap_config s32g_xpcs0_regmap_config = {
+	.reg_bits = 32,
+	.val_bits = 16,
+	.reg_read = s32g_xpcs_regmap_reg_read,
+	.reg_write = s32g_xpcs_regmap_reg_write,
+	.wr_table = &s32g_xpcs_wr_table,
+	.rd_table = &s32g_xpcs_rd_table,
+	.max_register = 0x1f80e1,
+	.name = "xpcs0",
+};
+
+static const struct regmap_config s32g_xpcs1_regmap_config = {
+	.reg_bits = 32,
+	.val_bits = 16,
+	.reg_read = s32g_xpcs_regmap_reg_read,
+	.reg_write = s32g_xpcs_regmap_reg_write,
+	.wr_table = &s32g_xpcs_wr_table,
+	.rd_table = &s32g_xpcs_rd_table,
+	.max_register = 0x1f80e1,
+	.name = "xpcs1",
+};
+
+static void s32g_xpcs_write_bits(struct s32g_xpcs *xpcs, unsigned int reg,
+				 unsigned int mask, unsigned int value)
+{
+	int ret = regmap_write_bits(xpcs->regmap, reg, mask, value);
+
+	if (ret)
+		dev_err(xpcs->dev, "Failed to write bits of XPCS reg: 0x%x\n", reg);
+}
+
+static void s32g_xpcs_write(struct s32g_xpcs *xpcs, unsigned int reg,
+			    unsigned int value)
+{
+	int ret = regmap_write(xpcs->regmap, reg, value);
+
+	if (ret)
+		dev_err(xpcs->dev, "Failed to write XPCS reg: 0x%x\n", reg);
+}
+
+static unsigned int s32g_xpcs_read(struct s32g_xpcs *xpcs, unsigned int reg)
+{
+	unsigned int val = 0;
+	int ret;
+
+	ret = regmap_read(xpcs->regmap, reg, &val);
+	if (ret)
+		dev_err(xpcs->dev, "Failed to read XPCS reg: 0x%x\n", reg);
+
+	return val;
+}
+
+/*
+ * Internal XPCS function
+ */
+
+static int s32g_xpcs_wait(struct s32g_xpcs *xpcs, xpcs_poll_func_t func)
+{
+	bool val;
+
+	return read_poll_timeout(func, val, val, XPCS_POLL_SLEEP_US,
+				 XPCS_TIMEOUT_US, false, xpcs);
+}
+
+static int s32g_xpcs_wait_bits(struct s32g_xpcs *xpcs, unsigned int reg,
+			       unsigned int mask, unsigned int bits)
+{
+	unsigned int val;
+
+	return read_poll_timeout(s32g_xpcs_read, val, (val & mask) == bits,
+				 XPCS_POLL_SLEEP_US, XPCS_TIMEOUT_US, false,
+				 xpcs, reg);
+}
+
+static unsigned int s32g_xpcs_digital_status(struct s32g_xpcs *xpcs)
+{
+	return s32g_xpcs_read(xpcs, VR_MII_DIG_STS);
+}
+
+static int s32g_xpcs_wait_power_good_state(struct s32g_xpcs *xpcs)
+{
+	unsigned int val;
+
+	return read_poll_timeout(s32g_xpcs_digital_status, val,
+				 FIELD_GET(PSEQ_STATE_MASK, val) == POWER_GOOD_STATE,
+				 XPCS_POLL_SLEEP_US,
+				 XPCS_TIMEOUT_US, false, xpcs);
+}
+
+void s32g_xpcs_vreset(struct s32g_xpcs *xpcs)
+{
+	/* Step 19 */
+	s32g_xpcs_write_bits(xpcs, VR_MII_DIG_CTRL1, VR_RST, VR_RST);
+}
+EXPORT_SYMBOL_GPL(s32g_xpcs_vreset);
+
+static bool s32g_xpcs_is_not_in_reset(struct s32g_xpcs *xpcs)
+{
+	unsigned int val;
+
+	val = s32g_xpcs_read(xpcs, VR_MII_DIG_CTRL1);
+
+	return !(val & VR_RST);
+}
+
+int s32g_xpcs_wait_vreset(struct s32g_xpcs *xpcs)
+{
+	int ret;
+
+	/* Step 20 */
+	ret = s32g_xpcs_wait(xpcs, s32g_xpcs_is_not_in_reset);
+	if (ret)
+		dev_err(xpcs->dev, "XPCS%d is in reset\n", xpcs->id);
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(s32g_xpcs_wait_vreset);
+
+int s32g_xpcs_reset_rx(struct s32g_xpcs *xpcs)
+{
+	int ret = 0;
+
+	/*
+	 * On the shared 1G combo lane the PMA power-up is gated by the PCIe
+	 * side and BYP_PWRUP stays set, so the power sequencer never reaches
+	 * POWER_GOOD on its own and the wait would always time out.
+	 */
+	if (xpcs->pcie_shared != S32G_PCIE_XPCS_1G) {
+		ret = s32g_xpcs_wait_power_good_state(xpcs);
+		if (ret) {
+			dev_err(xpcs->dev, "Failed to enter in PGOOD state after vendor reset\n");
+			return ret;
+		}
+	}
+
+	/* Step 21 */
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_RX_GENCTRL1,
+			     RX_RST_0, RX_RST_0);
+
+	/* Step 22 */
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_RX_GENCTRL1,
+			     RX_RST_0, 0);
+
+	/* Step 23 */
+	/* Wait until SR_MII_STS[LINK_STS] = 1 */
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(s32g_xpcs_reset_rx);
+
+static int s32g_xpcs_ref_clk_sel(struct s32g_xpcs *xpcs,
+				 enum s32g_xpcs_pll ref_pll)
+{
+	switch (ref_pll) {
+	case S32G_XPCS_PLLA:
+		s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_MPLL_CMN_CTRL,
+				     MPLLB_SEL_0, 0);
+		xpcs->ref = S32G_XPCS_PLLA;
+		break;
+	case S32G_XPCS_PLLB:
+		s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_MPLL_CMN_CTRL,
+				     MPLLB_SEL_0, MPLLB_SEL_0);
+		xpcs->ref = S32G_XPCS_PLLB;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static void s32g_xpcs_electrical_configure(struct s32g_xpcs *xpcs)
+{
+	/* Step 2 */
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_TX_EQ_CTRL0,
+			     TX_EQ_MAIN_MASK, FIELD_PREP(TX_EQ_MAIN_MASK, 0xc));
+
+	/* Step 3 */
+	s32g_xpcs_write_bits(xpcs, VR_MII_CONSUMER_10G_TX_TERM_CTRL,
+			     TX0_TERM_MASK, FIELD_PREP(TX0_TERM_MASK, 0x4));
+}
+
+static int s32g_xpcs_vco_cfg(struct s32g_xpcs *xpcs, enum s32g_xpcs_pll vco_pll)
+{
+	unsigned int vco_ld = 0;
+	unsigned int vco_ref = 0;
+	unsigned int rx_baud = 0;
+	unsigned int tx_baud = 0;
+
+	switch (vco_pll) {
+	case S32G_XPCS_PLLA:
+		if (xpcs->mhz125) {
+			vco_ld = FIELD_PREP(VCO_LD_VAL_0_MASK, 1360);
+			vco_ref = FIELD_PREP(VCO_REF_LD_0_MASK, 17);
+		} else {
+			vco_ld = FIELD_PREP(VCO_LD_VAL_0_MASK, 1350);
+			vco_ref = FIELD_PREP(VCO_REF_LD_0_MASK, 27);
+		}
+
+		rx_baud = FIELD_PREP(RX0_RATE_MASK, RX0_BAUD_DIV_8);
+		tx_baud = FIELD_PREP(TX0_RATE_MASK, TX0_BAUD_DIV_4);
+		break;
+	case S32G_XPCS_PLLB:
+		if (xpcs->mhz125) {
+			vco_ld = FIELD_PREP(VCO_LD_VAL_0_MASK, 1350);
+			vco_ref = FIELD_PREP(VCO_REF_LD_0_MASK, 27);
+		} else {
+			vco_ld = FIELD_PREP(VCO_LD_VAL_0_MASK, 1344);
+			vco_ref = FIELD_PREP(VCO_REF_LD_0_MASK, 43);
+		}
+
+		rx_baud = FIELD_PREP(RX0_RATE_MASK, RX0_BAUD_DIV_2);
+		tx_baud = FIELD_PREP(TX0_RATE_MASK, TX0_BAUD_DIV_1);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_VCO_CAL_LD0,
+			     VCO_LD_VAL_0_MASK, vco_ld);
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_VCO_CAL_REF0,
+			     VCO_REF_LD_0_MASK, vco_ref);
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_TX_RATE_CTRL,
+			     TX0_RATE_MASK, tx_baud);
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_RX_RATE_CTRL,
+			     RX0_RATE_MASK, rx_baud);
+
+	if (vco_pll == S32G_XPCS_PLLB) {
+		s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_CDR_CTRL,
+				     VCO_LOW_FREQ_0, VCO_LOW_FREQ_0);
+	} else {
+		s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_CDR_CTRL,
+				     VCO_LOW_FREQ_0, 0);
+	}
+
+	return 0;
+}
+
+static void s32g_xpcs_init_mplla(struct s32g_xpcs *xpcs)
+{
+	unsigned int val;
+
+	/* Step 7 */
+	val = 0;
+	if (xpcs->ext_clk)
+		val |= REF_USE_PAD;
+
+	if (xpcs->mhz125) {
+		val |= REF_MPLLA_DIV2;
+		val |= REF_CLK_DIV2;
+		val |= FIELD_PREP(REF_RANGE_MASK, RANGE_52_78_MHZ);
+	} else {
+		/*
+		 * A non-125 MHz reference is 100 MHz, which falls in the
+		 * 26-53 MHz range after the internal divider, so select
+		 * RANGE_26_53_MHZ.
+		 */
+		val |= FIELD_PREP(REF_RANGE_MASK, RANGE_26_53_MHZ);
+	}
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_REF_CLK_CTRL,
+			     REF_MPLLA_DIV2 | REF_USE_PAD | REF_RANGE_MASK |
+			     REF_CLK_DIV2, val);
+
+	/* Step 8 */
+	if (xpcs->mhz125)
+		val = FIELD_PREP(MLLA_MULTIPLIER_MASK, 80);
+	else
+		val = FIELD_PREP(MLLA_MULTIPLIER_MASK, 25);
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_MPLLA_CTRL0,
+			     MPLLA_CAL_DISABLE | MLLA_MULTIPLIER_MASK,
+			     val);
+
+	/* Step 9 */
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_MPLLA_CTRL1,
+			     MPLLA_FRACN_CTRL_MASK, 0);
+
+	/* Step 10 */
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_MPLLA_CTRL2,
+			     MPLLA_TX_CLK_DIV_MASK | MPLLA_DIV10_CLK_EN,
+			     FIELD_PREP(MPLLA_TX_CLK_DIV_MASK, 1) | MPLLA_DIV10_CLK_EN);
+
+	/* Step 11 */
+	if (xpcs->mhz125)
+		val = FIELD_PREP(MPLLA_BANDWIDTH_MASK, 43);
+	else
+		val = FIELD_PREP(MPLLA_BANDWIDTH_MASK, 357);
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_MPLLA_CTRL3,
+			     MPLLA_BANDWIDTH_MASK, val);
+}
+
+static void s32g_xpcs_init_mpllb(struct s32g_xpcs *xpcs)
+{
+	unsigned int val;
+
+	/* Step 7 */
+	val = 0;
+	if (xpcs->ext_clk)
+		val |= REF_USE_PAD;
+
+	if (xpcs->mhz125) {
+		val |= REF_MPLLB_DIV2;
+		val |= REF_CLK_DIV2;
+		val |= FIELD_PREP(REF_RANGE_MASK, RANGE_52_78_MHZ);
+	} else {
+		/*
+		 * REF_RANGE selects the reference-clock frequency band and is
+		 * shared by both PLLs. For a 100 MHz reference this is the
+		 * 26-53 MHz range after the internal divider: RANGE_26_53_MHZ.
+		 */
+		val |= FIELD_PREP(REF_RANGE_MASK, RANGE_26_53_MHZ);
+	}
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_REF_CLK_CTRL,
+			     REF_MPLLB_DIV2 | REF_USE_PAD | REF_RANGE_MASK |
+			     REF_CLK_DIV2, val);
+
+	/* Step 8 */
+	if (xpcs->mhz125)
+		val = FIELD_PREP(MLLB_MULTIPLIER_MASK, 125);
+	else
+		val = FIELD_PREP(MLLB_MULTIPLIER_MASK, 39);
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_MPLLB_CTRL0,
+			     MPLLB_CAL_DISABLE | MLLB_MULTIPLIER_MASK,
+			     val);
+
+	/* Step 9 */
+	if (xpcs->mhz125)
+		val = FIELD_PREP(MPLLB_FRACN_CTRL_MASK, 0);
+	else
+		val = FIELD_PREP(MPLLB_FRACN_CTRL_MASK, 1044);
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_MPLLB_CTRL1,
+			     MPLLB_FRACN_CTRL_MASK, val);
+
+	/* Step 10 */
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_MPLLB_CTRL2,
+			     MPLLB_TX_CLK_DIV_MASK | MPLLB_DIV10_CLK_EN,
+			     FIELD_PREP(MPLLB_TX_CLK_DIV_MASK, 5) | MPLLB_DIV10_CLK_EN);
+
+	/* Step 11 */
+	if (xpcs->mhz125)
+		val = FIELD_PREP(MPLLB_BANDWIDTH_MASK, 68);
+	else
+		val = FIELD_PREP(MPLLB_BANDWIDTH_MASK, 102);
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_MPLLB_CTRL3,
+			     MPLLB_BANDWIDTH_MASK, val);
+}
+
+static void s32g_serdes_pma_high_freq_recovery(struct s32g_xpcs *xpcs)
+{
+	/* PCS signal protection, PLL railout recovery */
+	s32g_xpcs_write_bits(xpcs, VR_MII_DBG_CTRL, SUPPRESS_LOS_DET | RX_DT_EN_CTL,
+			     SUPPRESS_LOS_DET | RX_DT_EN_CTL);
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_MISC_CTRL0,
+			     PLL_CTRL, PLL_CTRL);
+}
+
+static void s32g_serdes_pma_configure_tx_eq_post(struct s32g_xpcs *xpcs)
+{
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_TX_EQ_CTRL1,
+			     TX_EQ_OVR_RIDE, TX_EQ_OVR_RIDE);
+}
+
+static int s32g_serdes_bifurcation_pll_transit(struct s32g_xpcs *xpcs,
+					       enum s32g_xpcs_pll target_pll)
+{
+	int ret = 0;
+	struct device *dev = xpcs->dev;
+
+	/* Configure XPCS speed and VCO */
+	if (target_pll == S32G_XPCS_PLLA) {
+		s32g_xpcs_write_bits(xpcs, VR_MII_DIG_CTRL1, EN_2_5G_MODE, 0);
+		s32g_xpcs_vco_cfg(xpcs, S32G_XPCS_PLLA);
+	} else {
+		s32g_xpcs_write_bits(xpcs, VR_MII_DIG_CTRL1,
+				     EN_2_5G_MODE, EN_2_5G_MODE);
+		s32g_xpcs_vco_cfg(xpcs, S32G_XPCS_PLLB);
+	}
+
+	/* Signal that clock are not available */
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_TX_GENCTRL1,
+			     TX_CLK_RDY_0, 0);
+
+	/* Select PLL reference */
+	if (target_pll == S32G_XPCS_PLLA)
+		s32g_xpcs_ref_clk_sel(xpcs, S32G_XPCS_PLLA);
+	else
+		s32g_xpcs_ref_clk_sel(xpcs, S32G_XPCS_PLLB);
+
+	/* Initiate transmitter TX reconfiguration request */
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_TX_GENCTRL2,
+			     TX_REQ_0, TX_REQ_0);
+
+	/* Wait for transmitter to reconfigure */
+	ret = s32g_xpcs_wait_bits(xpcs, VR_MII_GEN5_12G_16G_TX_GENCTRL2,
+				  TX_REQ_0, 0);
+	if (ret) {
+		dev_err(dev, "Switch to TX_REQ_0 failed\n");
+		return ret;
+	}
+
+	/* Initiate transmitter RX reconfiguration request */
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_RX_GENCTRL2,
+			     RX_REQ_0, RX_REQ_0);
+
+	/* Wait for receiver to reconfigure */
+	ret = s32g_xpcs_wait_bits(xpcs, VR_MII_GEN5_12G_16G_RX_GENCTRL2,
+				  RX_REQ_0, 0);
+	if (ret) {
+		dev_err(dev, "Switch to RX_REQ_0 failed\n");
+		return ret;
+	}
+
+	/* Signal that clock are available */
+	s32g_xpcs_write_bits(xpcs, VR_MII_GEN5_12G_16G_TX_GENCTRL1,
+			     TX_CLK_RDY_0, TX_CLK_RDY_0);
+
+	/* Flush internal logic */
+	s32g_xpcs_write_bits(xpcs, VR_MII_DIG_CTRL1, INIT, INIT);
+
+	/* Wait for init */
+	ret = s32g_xpcs_wait_bits(xpcs, VR_MII_DIG_CTRL1, INIT, 0);
+	if (ret) {
+		dev_err(dev, "XPCS INIT failed\n");
+		return ret;
+	}
+
+	return ret;
+}
+
+/*
+ * PMA hooks, run from the shared core's pcs_config() after the generic C37
+ * SGMII / 2500BASE-X setup. The S32G context hangs off the shared
+ * nxp_serdes_xpcs priv pointer, set in s32g_xpcs_create().
+ */
+
+static int s32g_xpcs_pma_sgmii(struct nxp_serdes_xpcs *nxpcs)
+{
+	struct s32g_xpcs *xpcs = nxp_serdes_xpcs_get_drvdata(nxpcs);
+	int ret;
+
+	/*
+	 * The shared core sets PCS_MODE_SGMII, TX_CONFIG and MAC_AUTO_SW. The
+	 * link timer differs on the PCIe-shared 2.5G lane, where
+	 * CL37_TMR_OVRRIDE must be set for the override value to take effect.
+	 */
+	if (xpcs->pcie_shared == S32G_PCIE_XPCS_2G5) {
+		s32g_xpcs_write(xpcs, VR_MII_LINK_TIMER_CTRL, 0x2faf);
+		s32g_xpcs_write_bits(xpcs, VR_MII_DIG_CTRL1,
+				     MAC_AUTO_SW, MAC_AUTO_SW);
+	} else {
+		s32g_xpcs_write(xpcs, VR_MII_LINK_TIMER_CTRL, 0x7a1);
+
+		/*
+		 * Clearing EN_2_5G_MODE does not reprogram the PMA line rate:
+		 * the VCO load, the baud dividers and MPLLB_SEL stay at 2.5G if
+		 * the lane last came up as 2500BASE-X. A 2.5G to 1G
+		 * renegotiation then links at 1000 Mbps while the PMA keeps
+		 * clocking at 3.125 Gbaud and no traffic passes, so run the
+		 * full transition back to PLLA. Idempotent on a fresh 1G lane.
+		 */
+		ret = s32g_serdes_bifurcation_pll_transit(xpcs,
+							  S32G_XPCS_PLLA);
+		if (ret)
+			return ret;
+	}
+
+	s32g_xpcs_write_bits(xpcs, VR_MII_DIG_CTRL1,
+			     CL37_TMR_OVRRIDE, CL37_TMR_OVRRIDE);
+
+	return 0;
+}
+
+static int s32g_xpcs_pma_2500basex(struct nxp_serdes_xpcs *nxpcs)
+{
+	struct s32g_xpcs *xpcs = nxp_serdes_xpcs_get_drvdata(nxpcs);
+
+	/*
+	 * The shared core has set EN_2_5G_MODE (DW_VR_MII_DIG_CTRL1_2G5_EN)
+	 * via nxp_serdes_xpcs_config_2500basex(). S32G additionally needs the
+	 * PMA switched from the slow PLLA to the fast PLLB and re-initialised.
+	 */
+	return s32g_serdes_bifurcation_pll_transit(xpcs, S32G_XPCS_PLLB);
+}
+
+static int s32g_xpcs_pma_link_up_sgmii(struct nxp_serdes_xpcs *nxpcs,
+				       int speed, int duplex)
+{
+	struct s32g_xpcs *xpcs = nxp_serdes_xpcs_get_drvdata(nxpcs);
+
+	/*
+	 * A 2.5G SGMII fixed-link keeps interface == SGMII and never reaches
+	 * the 2500BASE-X hook, so the PMA would stay on the 1G-class PLLA that
+	 * pma_sgmii() programmed at config time. Move to PLLB (which also sets
+	 * EN_2_5G_MODE) once the link resolves to 2.5G; below that the
+	 * config-time PLLA setting is already right.
+	 */
+	if (speed != SPEED_2500)
+		return 0;
+
+	return s32g_serdes_bifurcation_pll_transit(xpcs, S32G_XPCS_PLLB);
+}
+
+static const struct nxp_serdes_xpcs_compat s32g_xpcs_compat[] = {
+	{
+		.interface = PHY_INTERFACE_MODE_SGMII,
+		.supported = nxp_serdes_xpcs_sgmii_features,
+		.an_mode = NXP_SERDES_AN_C37_SGMII,
+		.pma_config = s32g_xpcs_pma_sgmii,
+		.pma_link_up = s32g_xpcs_pma_link_up_sgmii,
+	},
+	{
+		.interface = PHY_INTERFACE_MODE_2500BASEX,
+		.supported = nxp_serdes_xpcs_2500basex_features,
+		.an_mode = NXP_SERDES_2500BASEX,
+		.pma_config = s32g_xpcs_pma_2500basex,
+	},
+	{ }
+};
+
+static const struct nxp_serdes_xpcs_desc s32g_xpcs_desc = {
+	.name = "s32g-serdes-xpcs",
+	.compat = s32g_xpcs_compat,
+	/*
+	 * S32G runs its own vendor reset (VR_RST) as part of the ordered
+	 * dual-XPCS bring-up barrier in the SerDes PHY driver, so the shared
+	 * core must not issue the generic BMCR soft reset. S32G3 does not
+	 * implement the EEE VR_MII_EEE_MCTRL0/1 registers either.
+	 */
+	.quirks = NXP_SERDES_QUIRK_NO_SOFT_RESET | NXP_SERDES_QUIRK_NO_EEE,
+	.reset = NULL,
+};
+
+/*
+ * Serdes functions for initializing/configuring/releasing the xpcs
+ */
+
+int s32g_xpcs_init_plls(struct s32g_xpcs *xpcs)
+{
+	int ret;
+
+	if (!xpcs->ext_clk) {
+		/* Step 1 */
+		s32g_xpcs_write_bits(xpcs, VR_MII_DIG_CTRL1, BYP_PWRUP, BYP_PWRUP);
+	} else if (xpcs->pcie_shared == S32G_NOT_SHARED) {
+		ret = s32g_xpcs_wait_power_good_state(xpcs);
+		if (ret)
+			return ret;
+	} else if (xpcs->pcie_shared == S32G_PCIE_XPCS_2G5) {
+		ret = s32g_xpcs_wait_power_good_state(xpcs);
+		if (ret)
+			return ret;
+		/* Configure equalization */
+		s32g_serdes_pma_configure_tx_eq_post(xpcs);
+		s32g_xpcs_electrical_configure(xpcs);
+
+		/* Enable receiver recover */
+		s32g_serdes_pma_high_freq_recovery(xpcs);
+		return 0;
+	}
+
+	s32g_xpcs_electrical_configure(xpcs);
+
+	s32g_xpcs_ref_clk_sel(xpcs, S32G_XPCS_PLLA);
+	s32g_xpcs_init_mplla(xpcs);
+	s32g_xpcs_init_mpllb(xpcs);
+	s32g_xpcs_vco_cfg(xpcs, S32G_XPCS_PLLA);
+
+	/*
+	 * Step 18: for an internal reference BYP_PWRUP was set in Step 1 to
+	 * bypass the power-up sequencer; clear it so the PMA powers up. The
+	 * shared 1G lane keeps it set - there the PCIe side drives power-up
+	 * and clearing it stalls the XPCS bring-up.
+	 */
+	if (!xpcs->ext_clk && xpcs->pcie_shared != S32G_PCIE_XPCS_1G)
+		s32g_xpcs_write_bits(xpcs, VR_MII_DIG_CTRL1, BYP_PWRUP, 0);
+
+	/* Will be cleared by Step 19 Vreset */
+	s32g_xpcs_write_bits(xpcs, SR_MII_CTRL, AN_ENABLE, 0);
+	s32g_xpcs_write_bits(xpcs, SR_MII_CTRL, DUPLEX_MODE, DUPLEX_MODE);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(s32g_xpcs_init_plls);
+
+void s32g_xpcs_disable_an(struct s32g_xpcs *xpcs)
+{
+	s32g_xpcs_write_bits(xpcs, SR_MII_CTRL, DUPLEX_MODE, DUPLEX_MODE);
+	s32g_xpcs_write_bits(xpcs, SR_MII_CTRL, AN_ENABLE, 0);
+}
+EXPORT_SYMBOL_GPL(s32g_xpcs_disable_an);
+
+static void s32g_xpcs_pcs_destroy(void *pcs)
+{
+	nxp_serdes_xpcs_destroy(pcs);
+}
+
+/**
+ * s32g_xpcs_create() - allocate and set up one XPCS instance
+ * @dev: the SerDes device the XPCS belongs to; owns the allocation
+ * @id: XPCS instance number within the SerDes subsystem (0 or 1)
+ * @base: mapped register block of that instance
+ * @ext_clk: reference is taken from the external pad rather than internal
+ * @rate: reference clock rate, 100 or 125 MHz
+ * @pcie_shared: whether the lane shares the combo PHY with PCIe
+ *
+ * The instance is devm-allocated against @dev, so it lives as long as the
+ * SerDes device and must not be freed by the caller. The returned handle
+ * is opaque; use s32g_xpcs_pcs() to obtain the phylink PCS created on top
+ * of the NXP SerDes xPCS shared core.
+ *
+ * Return: the XPCS handle, or an ERR_PTR() on failure.
+ */
+struct s32g_xpcs *s32g_xpcs_create(struct device *dev, unsigned char id,
+				   void __iomem *base, bool ext_clk,
+				   unsigned long rate,
+				   enum s32g_xpcs_shared pcie_shared)
+{
+	const struct regmap_config *conf;
+	struct nxp_serdes_xpcs *nxpcs;
+	struct s32g_xpcs *xpcs;
+	struct phylink_pcs *pcs;
+	int ret;
+
+	if (rate != (125 * HZ_PER_MHZ) && rate != (100 * HZ_PER_MHZ)) {
+		dev_err(dev, "XPCS cannot operate @%lu HZ\n", rate);
+		return ERR_PTR(-EINVAL);
+	}
+
+	xpcs = devm_kzalloc(dev, sizeof(*xpcs), GFP_KERNEL);
+	if (!xpcs)
+		return ERR_PTR(-ENOMEM);
+
+	xpcs->base = base;
+	xpcs->ext_clk = ext_clk;
+	xpcs->id = id;
+	xpcs->dev = dev;
+	xpcs->pcie_shared = pcie_shared;
+
+	if (rate == (125 * HZ_PER_MHZ))
+		xpcs->mhz125 = true;
+	else
+		xpcs->mhz125 = false;
+
+	if (!id)
+		conf = &s32g_xpcs0_regmap_config;
+	else
+		conf = &s32g_xpcs1_regmap_config;
+
+	xpcs->regmap = devm_regmap_init(dev, NULL, xpcs, conf);
+	if (IS_ERR(xpcs->regmap))
+		return ERR_PTR(dev_err_probe(dev, PTR_ERR(xpcs->regmap),
+					     "Failed to init register map\n"));
+
+	/*
+	 * Create the phylink PCS on the shared core, handing it the S32G
+	 * regmap as the register transport. SGMII is the initial interface;
+	 * phylink re-runs pcs_config() with the real interface at link time.
+	 */
+	pcs = nxp_serdes_xpcs_create(dev, xpcs->regmap, NULL, &s32g_xpcs_desc,
+				     id, PHY_INTERFACE_MODE_SGMII);
+	if (IS_ERR(pcs))
+		return ERR_PTR(dev_err_probe(dev, PTR_ERR(pcs),
+					     "Failed to create SerDes xPCS\n"));
+
+	ret = devm_add_action_or_reset(dev, s32g_xpcs_pcs_destroy, pcs);
+	if (ret)
+		return ERR_PTR(ret);
+
+	/* Let the S32G PMA hooks recover their context from the shared PCS. */
+	nxpcs = phylink_pcs_to_nxp_serdes_xpcs(pcs);
+	nxp_serdes_xpcs_set_drvdata(nxpcs, xpcs);
+
+	xpcs->pcs = pcs;
+
+	return xpcs;
+}
+EXPORT_SYMBOL_GPL(s32g_xpcs_create);
+
+/**
+ * s32g_xpcs_pcs() - the phylink PCS of an XPCS instance
+ * @xpcs: handle from s32g_xpcs_create()
+ *
+ * Return: the phylink_pcs, owned by the SerDes device.
+ */
+struct phylink_pcs *s32g_xpcs_pcs(struct s32g_xpcs *xpcs)
+{
+	return xpcs->pcs;
+}
+EXPORT_SYMBOL_GPL(s32g_xpcs_pcs);
+
+MODULE_DESCRIPTION("NXP S32G SerDes XPCS driver");
+MODULE_AUTHOR("Jan Petrous (OSS) <jan.petrous@oss.nxp.com>");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/pcs/pcs-nxp-s32g-xpcs.h b/include/linux/pcs/pcs-nxp-s32g-xpcs.h
new file mode 100644
index 000000000000..7e1dfb6b638f
--- /dev/null
+++ b/include/linux/pcs/pcs-nxp-s32g-xpcs.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright 2021-2026 NXP
+ */
+#ifndef PCS_NXP_S32G_XPCS_H
+#define PCS_NXP_S32G_XPCS_H
+
+#include <linux/types.h>
+
+struct device;
+struct phylink_pcs;
+
+/*
+ * Opaque. The XPCS state, including its regmap and the embedded
+ * phylink_pcs, is private to the PCS driver; the SerDes PHY driver only
+ * ever holds pointers handed out by s32g_xpcs_create().
+ */
+struct s32g_xpcs;
+
+/*
+ * Whether the XPCS shares the combo PHY with the PCIe lane. In the shared
+ * modes the PMA power-up is driven from the PCIe side, which changes the
+ * bring-up sequence.
+ */
+enum s32g_xpcs_shared {
+	S32G_NOT_SHARED,
+	S32G_PCIE_XPCS_1G,
+	S32G_PCIE_XPCS_2G5,
+};
+
+struct s32g_xpcs *s32g_xpcs_create(struct device *dev, unsigned char id,
+				   void __iomem *base, bool ext_clk,
+				   unsigned long rate,
+				   enum s32g_xpcs_shared pcie_shared);
+struct phylink_pcs *s32g_xpcs_pcs(struct s32g_xpcs *xpcs);
+
+/*
+ * Bring-up steps, called by the SerDes PHY driver. They are separate
+ * because the dual-XPCS mode needs cross-instance barriers: every
+ * instance must complete a step before any instance starts the next.
+ */
+int s32g_xpcs_init_plls(struct s32g_xpcs *xpcs);
+void s32g_xpcs_vreset(struct s32g_xpcs *xpcs);
+int s32g_xpcs_wait_vreset(struct s32g_xpcs *xpcs);
+int s32g_xpcs_reset_rx(struct s32g_xpcs *xpcs);
+void s32g_xpcs_disable_an(struct s32g_xpcs *xpcs);
+
+#endif /* PCS_NXP_S32G_XPCS_H */

-- 
2.55.0



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

* [PATCH RFC v3 06/12] phy: freescale: s32g: Add SerDes subsystem PHY
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
                   ` (4 preceding siblings ...)
  2026-09-19  6:54 ` [PATCH RFC v3 05/12] net: pcs: Add NXP S32G XPCS driver Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 07/12] net: stmmac: dwmac-s32: Add SGMII support Jan Petrous via B4 Relay
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS),
	Alexandru-Catalin Ionita, Ionut Vicovan, Bogdan Roman

From: Vincent Guittot <vincent.guittot@linaro.org>

The S32G SoC family includes two SerDes subsystems, each made of one
PCIe controller, two XPCS and a shared 2-lane combo PHY. The lanes can
be configured to carry PCIe and/or SGMII.

Add the SerDes subsystem PHY driver: the PCIe PHY, the shared combo-PHY
bring-up, and the setup of the XPCS instances added by the previous
patch. The MAC obtains its phylink PCS through
s32g_serdes_pcs_create(), which also creates a device link so the MAC
is unbound and suspended before the SerDes it depends on.

The SS_RW_REG_0[SUBSYS_MODE] value is not taken from the devicetree.
It is derived at probe from the per-lane child nodes - the lane
function and, for an SGMII lane, the XPCS instance it feeds. Each
working mode in scope has a unique lane mux, so the first match is the
only match. The reference-clock rate is validated against the derived
mode; it is never used to select it. Per-lane link speed is a runtime
property and does not influence mode selection.

After the subsystem reset is released the PHY needs up to 5.2 ms to
become operational (MPLL_STATE). Poll it before any further PHY or
XPCS PMA access, on every working mode - relying on incidental timing
made the init sequence fragile.

SUBMODE_MASK is GENMASK(2, 0): valid SUBSYS_MODE values are 0 to 4 and
the vendor driver masks the field with 0x7.

Tested on an S32G3-VNP-RDB3 board in both modes the driver derives from
the lane children: mode 1 (PCIe on lane 0, XPCS0 on lane 1) and mode 3
(XPCS0 on lane 0, XPCS1 on lane 1). In mode 1 both consumers of the
shared subsystem are up at the same time - both PCIe root complexes
enumerate and GMAC0 links at 1G over SGMII. Mode 3 routes both lanes to
the XPCS instances, so it has no PCIe consumer.

Signed-off-by: Vincent Guittot <vincent.guittot@linaro.org>
Co-developed-by: Ciprian Marian Costea <ciprianmarian.costea@oss.nxp.com>
Signed-off-by: Ciprian Marian Costea <ciprianmarian.costea@oss.nxp.com>
Co-developed-by: Alexandru-Catalin Ionita <alexandru-catalin.ionita@nxp.com>
Signed-off-by: Alexandru-Catalin Ionita <alexandru-catalin.ionita@nxp.com>
Co-developed-by: Ghennadi Procopciuc <ghennadi.procopciuc@nxp.com>
Signed-off-by: Ghennadi Procopciuc <ghennadi.procopciuc@nxp.com>
Co-developed-by: Ionut Vicovan <Ionut.Vicovan@nxp.com>
Signed-off-by: Ionut Vicovan <Ionut.Vicovan@nxp.com>
Co-developed-by: Bogdan Roman <bogdan-gabriel.roman@nxp.com>
Signed-off-by: Bogdan Roman <bogdan-gabriel.roman@nxp.com>
Co-developed-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 drivers/phy/freescale/Kconfig               |    9 +
 drivers/phy/freescale/Makefile              |    1 +
 drivers/phy/freescale/phy-nxp-s32g-serdes.c | 1057 +++++++++++++++++++++++++++
 include/linux/phy/nxp-s32g-serdes.h         |   25 +
 4 files changed, 1092 insertions(+)

diff --git a/drivers/phy/freescale/Kconfig b/drivers/phy/freescale/Kconfig
index 7557b624be8f..59a86597eaa0 100644
--- a/drivers/phy/freescale/Kconfig
+++ b/drivers/phy/freescale/Kconfig
@@ -77,3 +77,12 @@ config PHY_FSL_LYNX_28G
 	  found on NXP's Layerscape platforms such as LX2160A.
 	  Used to change the protocol running on SerDes lanes at runtime.
 	  Only useful for a restricted set of Ethernet protocols.
+
+config PHY_S32G_SERDES
+	tristate "NXP S32G SERDES support"
+	depends on ARCH_S32 || COMPILE_TEST
+	depends on PCS_NXP_S32G_XPCS
+	select GENERIC_PHY
+	help
+	  This option enables support for S32G SerDes PHY used for
+	  PCIe & Ethernet
diff --git a/drivers/phy/freescale/Makefile b/drivers/phy/freescale/Makefile
index 5b0e180d6972..1a0aec790917 100644
--- a/drivers/phy/freescale/Makefile
+++ b/drivers/phy/freescale/Makefile
@@ -8,3 +8,4 @@ obj-$(CONFIG_PHY_FSL_LYNX_CORE)		+= phy-fsl-lynx-core.o
 obj-$(CONFIG_PHY_FSL_LYNX_10G)		+= phy-fsl-lynx-10g.o
 obj-$(CONFIG_PHY_FSL_LYNX_28G)		+= phy-fsl-lynx-28g.o
 obj-$(CONFIG_PHY_FSL_SAMSUNG_HDMI_PHY)	+= phy-fsl-samsung-hdmi.o
+obj-$(CONFIG_PHY_S32G_SERDES)		+= phy-nxp-s32g-serdes.o
diff --git a/drivers/phy/freescale/phy-nxp-s32g-serdes.c b/drivers/phy/freescale/phy-nxp-s32g-serdes.c
new file mode 100644
index 000000000000..e81878137f2e
--- /dev/null
+++ b/drivers/phy/freescale/phy-nxp-s32g-serdes.c
@@ -0,0 +1,1057 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SerDes driver for S32G SoCs
+ *
+ * Copyright 2021-2026 NXP
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <linux/pcs/pcs-nxp-s32g-xpcs.h>
+#include <linux/phy/nxp-s32g-serdes.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+#include <linux/units.h>
+
+#define S32G_SERDES_XPCS_MAX			2
+
+#define EXTERNAL_CLK_NAME			"ext"
+#define INTERNAL_CLK_NAME			"ref"
+
+/* Serdes Sub system registers */
+
+#define S32G_PCIE_PHY_GEN_CTRL			0x0
+#define  REF_USE_PAD				BIT(17)
+#define  RX_SRIS_MODE				BIT(9)
+
+#define S32G_PCIE_PHY_MPLLA_CTRL		0x10
+#define  MPLL_STATE				BIT(30)
+
+#define S32G_SS_RW_REG_0			0xF0
+#define  SUBMODE_MASK				GENMASK(2, 0)
+#define  CLKEN_MASK				BIT(23)
+#define  PHY0_CR_PARA_SEL			BIT(9)
+
+/* PCIe phy subsystem registers */
+
+#define S32G_PHY_REG_ADDR			0x0
+#define  PHY_REG_EN				BIT(31)
+
+#define S32G_PHY_REG_DATA			0x4
+
+#define RAWLANE0_DIG_PCS_XF_RX_EQ_DELTA_IQ_OVRD_IN	0x3019
+#define RAWLANE1_DIG_PCS_XF_RX_EQ_DELTA_IQ_OVRD_IN	0x3119
+
+/*
+ * DesignWare PCIe PHY reference-clock architecture, passed as the submode
+ * to phy_set_mode_ext(phy, PHY_MODE_PCIE, submode).
+ *
+ * These describe how the reference clock is distributed and whether spread
+ * spectrum is used, a different axis from the role submodes in
+ * include/linux/phy/pcie.h (PHY_MODE_PCIE_RC/_EP/_BIFURCATION, 20..22), which
+ * they do not overlap. Kept driver-private until a second driver needs a
+ * non-default architecture; pcie-nxp-s32g.c always asks for CRNS.
+ */
+enum pcie_phy_mode {
+	CRNS = 0, /* Common Reference Clock, No Spread Spectrum */
+	CRSS = 1, /* Common Reference Clock, Spread Spectrum */
+	SRNS = 2, /* Separate Reference Clock, No Spread Spectrum */
+	SRIS = 3  /* Separate Reference Clock, Spread Spectrum */
+};
+
+/*
+ * SS_RW_REG_0[SUBSYS_MODE] holds the reference-manual "Mode N" number. It is
+ * not in the devicetree: it is derived from the per-lane child nodes (lane
+ * function + XPCS instance). The reference clock is validated against the
+ * result, never used to select it.
+ */
+
+enum s32g_lane_fn {
+	S32G_LANE_UNUSED = 0,
+	S32G_LANE_PCIE,
+	S32G_LANE_XPCS,
+};
+
+struct s32g_lane_desc {
+	enum s32g_lane_fn fn;
+	u32 xpcs_instance;	/* valid iff fn == S32G_LANE_XPCS */
+};
+
+struct s32g_serdes_mode {
+	u32 submode;			/* SS_RW_REG_0[SUBSYS_MODE] value */
+	struct s32g_lane_desc lane[2];
+	unsigned long refclk[2];	/* permitted reference clock rates (Hz), 0-terminated */
+};
+
+struct s32g_serdes_soc_data {
+	const struct s32g_serdes_mode *modes;
+	unsigned int n_modes;
+};
+
+#define S32G_LANE_IS_PCIE	{ .fn = S32G_LANE_PCIE }
+#define S32G_LANE_IS_XPCS(i)	{ .fn = S32G_LANE_XPCS, .xpcs_instance = (i) }
+
+#define S32G_REFCLK_100MHZ	(100 * HZ_PER_MHZ)
+#define S32G_REFCLK_125MHZ	(125 * HZ_PER_MHZ)
+
+/*
+ * Supported working modes (S32G2/S32G3, 1G). Each row has a unique lane mux.
+ * PCIe needs a 100 MHz reference, so modes 0/1/2 take 100 MHz only; the
+ * dual-XPCS mode 3 takes 100 or 125 MHz.
+ */
+static const struct s32g_serdes_mode s32g_serdes_modes[] = {
+	{ 0, { S32G_LANE_IS_PCIE,    S32G_LANE_IS_PCIE    },
+	  { S32G_REFCLK_100MHZ } },
+	{ 1, { S32G_LANE_IS_PCIE,    S32G_LANE_IS_XPCS(0) },
+	  { S32G_REFCLK_100MHZ } },
+	{ 2, { S32G_LANE_IS_PCIE,    S32G_LANE_IS_XPCS(1) },
+	  { S32G_REFCLK_100MHZ } },
+	{ 3, { S32G_LANE_IS_XPCS(0), S32G_LANE_IS_XPCS(1) },
+	  { S32G_REFCLK_100MHZ, S32G_REFCLK_125MHZ } },
+};
+
+/*
+ * S32G2 and S32G3 share this table for now but keep distinct compatibles with
+ * no fallback: the full RM mode tables differ per SoC and per SerDes instance
+ * (only S32G3 SerDes_1 has the 3.125G dual-XPCS mode 4) and will diverge once
+ * 2500BASE-X lands.
+ */
+static const struct s32g_serdes_soc_data s32g2_serdes_soc_data = {
+	.modes = s32g_serdes_modes,
+	.n_modes = ARRAY_SIZE(s32g_serdes_modes),
+};
+
+static const struct s32g_serdes_soc_data s32g3_serdes_soc_data = {
+	.modes = s32g_serdes_modes,
+	.n_modes = ARRAY_SIZE(s32g_serdes_modes),
+};
+
+struct s32g_serdes_ctrl {
+	void __iomem *ss_base;
+	struct reset_control *rst;
+	struct clk_bulk_data *clks;
+	int nclks;
+	u32 ss_mode;
+	unsigned long ref_clk_rate;
+	bool ext_clk;
+};
+
+struct s32g_pcie_ctrl {
+	void __iomem *phy_base;
+	struct reset_control *rst;
+	struct phy *phy;
+	enum pcie_phy_mode phy_mode;
+	bool powered_on;
+};
+
+struct s32g_xpcs_ctrl {
+	struct s32g_xpcs *phys[2];	/* indexed by XPCS instance */
+	void __iomem *base[2];		/* xpcs0 / xpcs1 register blocks */
+};
+
+struct s32g_serdes {
+	struct s32g_serdes_ctrl ctrl;
+	struct s32g_pcie_ctrl pcie;
+	struct s32g_xpcs_ctrl xpcs;
+	struct s32g_lane_desc lane[2];
+	const struct s32g_serdes_soc_data *soc_data;
+	struct device *dev;
+};
+
+/* PCIe phy subsystem */
+
+#define S32G_SERDES_PCIE_FREQ			(100 * HZ_PER_MHZ)
+
+static int s32g_pcie_check_clk(struct s32g_serdes *serdes)
+{
+	struct s32g_serdes_ctrl *sctrl = &serdes->ctrl;
+	unsigned long rate = sctrl->ref_clk_rate;
+
+	if (rate != S32G_SERDES_PCIE_FREQ) {
+		dev_err(serdes->dev, "PCIe PHY cannot operate at %lu HZ\n", rate);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+/* Serdes RM says the PHY is operational within 3.4 to 5.2 ms after reset
+ * release, depending on the reference clock (MPLL_STATE = 1).
+ */
+#define S32G_SERDES_LOCK_TIMEOUT_US	(10 * USEC_PER_MSEC)
+#define S32G_SERDES_LOCK_SLEEP_US	50
+
+/*
+ * MPLL_STATE signals "PHY operational" after the subsystem reset is released.
+ * It must be observed before any PHY or XPCS PMA access in every mode, not
+ * just on the PCIe path: skipping it on the XPCS-only modes left init
+ * dependent on incidental timing, and a printk between reset release and XPCS
+ * setup was enough to break it.
+ */
+static int s32g_serdes_wait_phy_operational(struct s32g_serdes *serdes)
+{
+	void __iomem *reg = serdes->ctrl.ss_base + S32G_PCIE_PHY_MPLLA_CTRL;
+	u32 val;
+	int ret;
+
+	ret = readl_poll_timeout(reg, val, (val & MPLL_STATE),
+				 S32G_SERDES_LOCK_SLEEP_US,
+				 S32G_SERDES_LOCK_TIMEOUT_US);
+	if (ret)
+		dev_err(serdes->dev, "SerDes PHY not operational\n");
+
+	return ret;
+}
+
+static int s32g_pcie_phy_power_on_common(struct s32g_serdes *serdes)
+{
+	struct s32g_serdes_ctrl *sctrl = &serdes->ctrl;
+	struct s32g_pcie_ctrl *pcie = &serdes->pcie;
+	u32 reg;
+	int ret;
+
+	ret = s32g_pcie_check_clk(serdes);
+	if (ret)
+		return ret;
+
+	reg = readl(sctrl->ss_base + S32G_PCIE_PHY_GEN_CTRL);
+
+	if (pcie->phy_mode == SRIS)
+		reg |= RX_SRIS_MODE;
+	else
+		reg &= ~RX_SRIS_MODE;
+
+	if (sctrl->ext_clk)
+		reg |= REF_USE_PAD;
+	else
+		reg &= ~REF_USE_PAD;
+
+	writel(reg, sctrl->ss_base + S32G_PCIE_PHY_GEN_CTRL);
+
+	ret = s32g_serdes_wait_phy_operational(serdes);
+	if (ret)
+		return ret;
+
+	/* Set PHY register access to CR interface */
+	reg = readl(sctrl->ss_base + S32G_SS_RW_REG_0);
+	reg |=  PHY0_CR_PARA_SEL;
+	writel(reg, sctrl->ss_base + S32G_SS_RW_REG_0);
+
+	return 0;
+}
+
+static void s32g_pcie_phy_write(struct s32g_serdes *serdes, u32 reg, u32 val)
+{
+	writel(PHY_REG_EN, serdes->pcie.phy_base + S32G_PHY_REG_ADDR);
+	writel(reg | PHY_REG_EN, serdes->pcie.phy_base + S32G_PHY_REG_ADDR);
+	usleep_range(100, 110);
+	writel(val, serdes->pcie.phy_base + S32G_PHY_REG_DATA);
+	usleep_range(100, 110);
+	writel(0, serdes->pcie.phy_base + S32G_PHY_REG_ADDR);
+}
+
+static int s32g_pcie_phy_power_on(struct s32g_serdes *serdes)
+{
+	struct s32g_pcie_ctrl *pcie = &serdes->pcie;
+	struct s32g_serdes_ctrl *ctrl = &serdes->ctrl;
+	u32 iq_ovrd_in;
+	int ret;
+
+	ret = s32g_pcie_phy_power_on_common(serdes);
+	if (ret)
+		return ret;
+
+	/* RX_EQ_DELTA_IQ_OVRD enable and override value for PCIe lanes */
+	iq_ovrd_in = RAWLANE0_DIG_PCS_XF_RX_EQ_DELTA_IQ_OVRD_IN;
+
+	s32g_pcie_phy_write(serdes, iq_ovrd_in, 0x3);
+	s32g_pcie_phy_write(serdes, iq_ovrd_in, 0x13);
+
+	if (ctrl->ss_mode == 0) {
+		iq_ovrd_in = RAWLANE1_DIG_PCS_XF_RX_EQ_DELTA_IQ_OVRD_IN;
+
+		s32g_pcie_phy_write(serdes, iq_ovrd_in, 0x3);
+		s32g_pcie_phy_write(serdes, iq_ovrd_in, 0x13);
+	}
+
+	pcie->powered_on = true;
+
+	return 0;
+}
+
+/* PCIe phy ops function */
+
+static int s32g_serdes_phy_power_on(struct phy *p)
+{
+	struct s32g_serdes *serdes = phy_get_drvdata(p);
+
+	return s32g_pcie_phy_power_on(serdes);
+}
+
+static int s32g_serdes_phy_power_off(struct phy *p)
+{
+	struct s32g_serdes *serdes = phy_get_drvdata(p);
+
+	serdes->pcie.powered_on = false;
+
+	return 0;
+}
+
+static inline bool is_pcie_phy_mode_valid(int mode)
+{
+	switch (mode) {
+	case CRNS:
+	case CRSS:
+	case SRNS:
+	case SRIS:
+		return true;
+	default:
+		return false;
+	}
+}
+
+static int s32g_serdes_phy_set_mode_ext(struct phy *p,
+					enum phy_mode mode, int submode)
+{
+	struct s32g_serdes *serdes = phy_get_drvdata(p);
+
+	if (mode != PHY_MODE_PCIE)
+		return -EINVAL;
+
+	if (!is_pcie_phy_mode_valid(submode))
+		return -EINVAL;
+
+	/*
+	 * Spread-spectrum and common-reference clocking both need an
+	 * external shared reference, and neither may be combined with an
+	 * SGMII lane on the same subsystem.
+	 */
+	if ((submode == CRSS || submode == SRIS) &&
+	    (serdes->ctrl.ss_mode != 0 || !serdes->ctrl.ext_clk))
+		return -EINVAL;
+
+	if (serdes->pcie.powered_on)
+		dev_warn(serdes->dev, "The phy is already powered on.\n");
+
+	serdes->pcie.phy_mode = submode;
+
+	return 0;
+}
+
+static const struct phy_ops serdes_pcie_ops = {
+	.power_on	= s32g_serdes_phy_power_on,
+	.power_off	= s32g_serdes_phy_power_off,
+	.set_mode	= s32g_serdes_phy_set_mode_ext,
+};
+
+static struct phy *s32g_serdes_phy_xlate(struct device *dev,
+					 const struct of_phandle_args *args)
+{
+	struct s32g_serdes *serdes;
+
+	serdes = dev_get_drvdata(dev);
+	if (!serdes)
+		return ERR_PTR(-EINVAL);
+
+	return serdes->pcie.phy;
+}
+
+/* XPCS subsystem */
+
+static int s32g_serdes_xpcs_setup(struct s32g_serdes *serdes, u32 instance)
+{
+	struct s32g_serdes_ctrl *ctrl = &serdes->ctrl;
+	struct s32g_xpcs_ctrl *xpcs_ctrl = &serdes->xpcs;
+	enum s32g_xpcs_shared shared = S32G_NOT_SHARED;
+	struct device *dev = serdes->dev;
+	struct s32g_xpcs *xpcs;
+
+	/*
+	 * In the PCIe + SGMII modes the XPCS shares the combo PHY with the
+	 * PCIe lane. The 2.5G shared variant (S32G_PCIE_XPCS_2G5) is selected at
+	 * run time and is not wired up yet.
+	 */
+	if (ctrl->ss_mode == 1 || ctrl->ss_mode == 2)
+		shared = S32G_PCIE_XPCS_1G;
+
+	xpcs = s32g_xpcs_create(dev, instance, xpcs_ctrl->base[instance],
+				ctrl->ext_clk, ctrl->ref_clk_rate, shared);
+	if (IS_ERR(xpcs))
+		return PTR_ERR(xpcs);
+
+	xpcs_ctrl->phys[instance] = xpcs;
+
+	return 0;
+}
+
+static int s32g_serdes_init_xpcs(struct s32g_serdes *serdes)
+{
+	struct s32g_serdes_ctrl *ctrl = &serdes->ctrl;
+	struct s32g_xpcs_ctrl *xpcs = &serdes->xpcs;
+	struct s32g_xpcs *order[2];
+	size_t i;
+	int ret;
+
+	/*
+	 * Mode 3 initialises XPCS1 before XPCS0. Carried over from the vendor
+	 * driver and undocumented in the RM. Do not reorder.
+	 */
+	switch (ctrl->ss_mode) {
+	case 0:
+		return 0;
+	case 1:
+		order[0] = xpcs->phys[0];
+		order[1] = NULL;
+		break;
+	case 2:
+		order[0] = xpcs->phys[1];
+		order[1] = NULL;
+		break;
+	case 3:
+		order[0] = xpcs->phys[1];
+		order[1] = xpcs->phys[0];
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(order); i++) {
+		if (!order[i])
+			continue;
+
+		ret = s32g_xpcs_init_plls(order[i]);
+		if (ret)
+			return ret;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(order); i++) {
+		if (!order[i])
+			continue;
+
+		s32g_xpcs_vreset(order[i]);
+	}
+
+	for (i = 0; i < ARRAY_SIZE(order); i++) {
+		if (!order[i])
+			continue;
+
+		ret = s32g_xpcs_wait_vreset(order[i]);
+		if (ret)
+			return ret;
+
+		ret = s32g_xpcs_reset_rx(order[i]);
+		if (ret)
+			return ret;
+
+		s32g_xpcs_disable_an(order[i]);
+	}
+
+	return 0;
+}
+
+/* Serdes subsystem */
+
+static int s32g_serdes_assert_reset(struct s32g_serdes *serdes)
+{
+	struct device *dev = serdes->dev;
+	int ret;
+
+	ret = reset_control_assert(serdes->pcie.rst);
+	if (ret) {
+		dev_err(dev, "Failed to assert PCIE reset: %d\n", ret);
+		return ret;
+	}
+
+	ret = reset_control_assert(serdes->ctrl.rst);
+	if (ret) {
+		dev_err(dev, "Failed to assert SerDes reset: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int s32g_serdes_deassert_reset(struct s32g_serdes *serdes)
+{
+	struct device *dev = serdes->dev;
+	int ret;
+
+	ret = reset_control_deassert(serdes->pcie.rst);
+	if (ret) {
+		dev_err(dev, "Failed to deassert PCIE reset: %d\n", ret);
+		return ret;
+	}
+
+	ret = reset_control_deassert(serdes->ctrl.rst);
+	if (ret) {
+		dev_err(dev, "Failed to deassert SerDes reset: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static bool s32g_serdes_lane_matches(const struct s32g_lane_desc *mode_lane,
+				     const struct s32g_lane_desc *req_lane)
+{
+	if (mode_lane->fn != req_lane->fn)
+		return false;
+
+	if (mode_lane->fn == S32G_LANE_XPCS)
+		return mode_lane->xpcs_instance == req_lane->xpcs_instance;
+
+	return true;
+}
+
+static const char *s32g_lane_fn_str(const struct s32g_lane_desc *lane)
+{
+	switch (lane->fn) {
+	case S32G_LANE_PCIE:
+		return "PCIe";
+	case S32G_LANE_XPCS:
+		return lane->xpcs_instance ? "XPCS1" : "XPCS0";
+	default:
+		return "unused";
+	}
+}
+
+static int s32g_serdes_derive_mode(struct s32g_serdes *serdes)
+{
+	const struct s32g_serdes_mode *match = NULL;
+	unsigned int i;
+	bool refclk_ok = false;
+
+	for (i = 0; i < serdes->soc_data->n_modes; i++) {
+		const struct s32g_serdes_mode *m = &serdes->soc_data->modes[i];
+
+		if (s32g_serdes_lane_matches(&m->lane[0], &serdes->lane[0]) &&
+		    s32g_serdes_lane_matches(&m->lane[1], &serdes->lane[1])) {
+			match = m;
+			break;
+		}
+	}
+
+	if (!match)
+		return dev_err_probe(serdes->dev, -EINVAL,
+				     "No SerDes mode matches the lane configuration\n");
+
+	for (i = 0; i < ARRAY_SIZE(match->refclk) && match->refclk[i]; i++) {
+		if (serdes->ctrl.ref_clk_rate == match->refclk[i]) {
+			refclk_ok = true;
+			break;
+		}
+	}
+
+	if (!refclk_ok)
+		return dev_err_probe(serdes->dev, -EINVAL,
+				     "Reference clock %lu Hz invalid for mode %u\n",
+				     serdes->ctrl.ref_clk_rate, match->submode);
+
+	serdes->ctrl.ss_mode = match->submode;
+
+	dev_dbg(serdes->dev,
+		"SerDes mode %u selected: lane0=%s lane1=%s, ref-clk %lu MHz (%s)\n",
+		match->submode,
+		s32g_lane_fn_str(&serdes->lane[0]),
+		s32g_lane_fn_str(&serdes->lane[1]),
+		serdes->ctrl.ref_clk_rate / HZ_PER_MHZ,
+		serdes->ctrl.ext_clk ? "external" : "internal");
+
+	return 0;
+}
+
+static int s32g_serdes_init(struct s32g_serdes *serdes)
+{
+	struct s32g_serdes_ctrl *ctrl = &serdes->ctrl;
+	u32 reg0;
+	int ret;
+
+	ret = clk_bulk_prepare_enable(ctrl->nclks, ctrl->clks);
+	if (ret) {
+		dev_err(serdes->dev, "Failed to enable SerDes clocks\n");
+		return ret;
+	}
+
+	ret = s32g_serdes_assert_reset(serdes);
+	if (ret)
+		goto disable_clks;
+
+	/* Set serdes mode */
+	reg0 = readl(ctrl->ss_base + S32G_SS_RW_REG_0);
+	reg0 &= ~SUBMODE_MASK;
+	reg0 |= FIELD_PREP(SUBMODE_MASK, ctrl->ss_mode);
+	writel(reg0, ctrl->ss_base + S32G_SS_RW_REG_0);
+
+	/* Set Clock source: internal or external */
+	reg0 = readl(ctrl->ss_base + S32G_SS_RW_REG_0);
+	if (ctrl->ext_clk)
+		reg0 &= ~CLKEN_MASK;
+	else
+		reg0 |= CLKEN_MASK;
+
+	writel(reg0, ctrl->ss_base + S32G_SS_RW_REG_0);
+
+	/*
+	 * Route the combo-PHY reference: REF_USE_PAD is 0 for the internal
+	 * reference, 1 for the external pad. s32g_pcie_phy_power_on_common()
+	 * programs it too, but the XPCS lane is brought up before any PCIe
+	 * power-on. Left at the bootloader default the XPCS PMA never powers
+	 * up, VR_RST never self-clears and every register reads back constant.
+	 */
+	reg0 = readl(ctrl->ss_base + S32G_PCIE_PHY_GEN_CTRL);
+	if (ctrl->ext_clk)
+		reg0 |= REF_USE_PAD;
+	else
+		reg0 &= ~REF_USE_PAD;
+	writel(reg0, ctrl->ss_base + S32G_PCIE_PHY_GEN_CTRL);
+	dev_dbg(serdes->dev,
+		"GEN_CTRL after ref-clk route: 0x%08x (REF_USE_PAD=%d)\n",
+		reg0, !!(reg0 & REF_USE_PAD));
+
+	/* Wait for the selection of working mode (as per the manual specs) */
+	usleep_range(100, 110);
+
+	ret = s32g_serdes_deassert_reset(serdes);
+	if (ret)
+		goto disable_clks;
+
+	/*
+	 * The PHY needs up to 5.2 ms after reset release to become
+	 * operational. Nothing may touch the PHY or the XPCS PMA before
+	 * that point.
+	 */
+	ret = s32g_serdes_wait_phy_operational(serdes);
+	if (ret)
+		goto disable_clks;
+
+	ret = s32g_serdes_init_xpcs(serdes);
+	if (ret) {
+		dev_err(serdes->dev, "XPCS init failed\n");
+		goto disable_clks;
+	}
+
+	dev_info(serdes->dev, "SerDes subsystem mode %u (refclk=%lu MHz)\n",
+		 ctrl->ss_mode, ctrl->ref_clk_rate / HZ_PER_MHZ);
+
+	return 0;
+
+disable_clks:
+	/* Already on an error path; ignore the return value. */
+	s32g_serdes_assert_reset(serdes);
+	clk_bulk_disable_unprepare(serdes->ctrl.nclks,
+				   serdes->ctrl.clks);
+
+	return ret;
+}
+
+static void s32g_serdes_disable(void *data)
+{
+	struct s32g_serdes *serdes = data;
+
+	s32g_serdes_assert_reset(serdes);
+	clk_bulk_disable_unprepare(serdes->ctrl.nclks, serdes->ctrl.clks);
+}
+
+static int s32g_serdes_get_ctrl_resources(struct platform_device *pdev,
+					  struct s32g_serdes *serdes)
+{
+	struct s32g_serdes_ctrl *ctrl = &serdes->ctrl;
+	struct device *dev = &pdev->dev;
+	int ret, idx, i;
+
+	ctrl->ss_base = devm_platform_ioremap_resource_byname(pdev, "ss-pcie");
+	if (IS_ERR(ctrl->ss_base))
+		return dev_err_probe(dev, PTR_ERR(ctrl->ss_base),
+				     "Failed to map 'ss-pcie'\n");
+
+	ctrl->rst = devm_reset_control_get_exclusive(dev, "serdes");
+	if (IS_ERR(ctrl->rst))
+		return dev_err_probe(dev, PTR_ERR(ctrl->rst),
+				     "Failed to get 'serdes' reset control\n");
+
+	ctrl->nclks = devm_clk_bulk_get_all(dev, &ctrl->clks);
+	if (ctrl->nclks < 1) {
+		ret = ctrl->nclks ? : -EINVAL;
+		return dev_err_probe(dev, ret,
+				     "Failed to get SerDes clocks\n");
+	}
+
+	idx = -1;
+	for (i = 0; i < ctrl->nclks; i++) {
+		if (!ctrl->clks[i].id)
+			continue;
+
+		if (!strcmp(ctrl->clks[i].id, EXTERNAL_CLK_NAME)) {
+			idx = i;
+			ctrl->ext_clk = true;
+			break;
+		}
+
+		if (!strcmp(ctrl->clks[i].id, INTERNAL_CLK_NAME))
+			idx = i;
+	}
+
+	if (idx < 0) {
+		dev_err(dev, "Failed to get Phy reference clock source\n");
+		return -EINVAL;
+	}
+
+	ctrl->ref_clk_rate = clk_get_rate(ctrl->clks[idx].clk);
+	if (!ctrl->ref_clk_rate) {
+		dev_err(dev, "Failed to get Phy reference clock rate\n");
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int s32g_serdes_get_pcie_resources(struct platform_device *pdev,
+					  struct s32g_serdes *serdes)
+{
+	struct s32g_pcie_ctrl *pcie = &serdes->pcie;
+	struct device *dev = &pdev->dev;
+
+	pcie->phy_base = devm_platform_ioremap_resource_byname(pdev,
+							       "pcie-phy");
+	if (IS_ERR(pcie->phy_base))
+		return dev_err_probe(dev, PTR_ERR(pcie->phy_base),
+				     "Failed to map 'pcie-phy'\n");
+
+	pcie->rst = devm_reset_control_get_exclusive(dev, "pcie");
+	if (IS_ERR(pcie->rst))
+		return dev_err_probe(dev, PTR_ERR(pcie->rst),
+				     "Failed to get 'pcie' reset control\n");
+
+	return 0;
+}
+
+static int s32g_serdes_get_xpcs_resources(struct platform_device *pdev,
+					  struct s32g_serdes *serdes)
+{
+	struct s32g_xpcs_ctrl *xpcs = &serdes->xpcs;
+	struct device *dev = &pdev->dev;
+
+	xpcs->base[0] = devm_platform_ioremap_resource_byname(pdev, "xpcs0");
+	if (IS_ERR(xpcs->base[0]))
+		return dev_err_probe(dev, PTR_ERR(xpcs->base[0]),
+				     "Failed to map 'xpcs0'\n");
+
+	xpcs->base[1] = devm_platform_ioremap_resource_byname(pdev, "xpcs1");
+	if (IS_ERR(xpcs->base[1]))
+		return dev_err_probe(dev, PTR_ERR(xpcs->base[1]),
+				     "Failed to map 'xpcs1'\n");
+
+	return 0;
+}
+
+static int s32g_serdes_parse_lane(struct s32g_serdes *serdes,
+				  struct device_node *child_node)
+{
+	struct device *dev = serdes->dev;
+	u32 index, instance;
+	int ret;
+
+	ret = of_property_read_u32(child_node, "reg", &index);
+	if (ret || index > 1)
+		return dev_err_probe(dev, ret ? : -EINVAL,
+				     "Invalid lane index in %pOFn\n",
+				     child_node);
+
+	if (serdes->lane[index].fn != S32G_LANE_UNUSED)
+		return dev_err_probe(dev, -EINVAL,
+				     "Duplicate lane %u\n", index);
+
+	if (of_device_is_compatible(child_node, "nxp,s32g-serdes-pcie-phy")) {
+		serdes->lane[index].fn = S32G_LANE_PCIE;
+		return 0;
+	}
+
+	if (of_device_is_compatible(child_node, "nxp,s32g-serdes-xpcs")) {
+		ret = of_property_read_u32(child_node, "nxp,xpcs-instance",
+					   &instance);
+		if (ret || instance > 1)
+			return dev_err_probe(dev, ret ? : -EINVAL,
+					     "Invalid nxp,xpcs-instance in %pOFn\n",
+					     child_node);
+
+		serdes->lane[index].fn = S32G_LANE_XPCS;
+		serdes->lane[index].xpcs_instance = instance;
+		return 0;
+	}
+
+	dev_warn(dev, "Skipping unknown child node %pOFn\n", child_node);
+
+	return 0;
+}
+
+static int s32g_serdes_create_pcie_phy(struct s32g_serdes *serdes,
+				       struct device_node *child_node)
+{
+	struct phy_provider *phy_provider;
+	struct device *dev = serdes->dev;
+	struct phy *phy;
+
+	phy = devm_phy_create(dev, child_node, &serdes_pcie_ops);
+	if (IS_ERR(phy))
+		return PTR_ERR(phy);
+
+	phy_set_drvdata(phy, serdes);
+
+	phy->attrs.mode = PHY_MODE_PCIE;
+	serdes->pcie.phy = phy;
+
+	phy_provider = devm_of_phy_provider_register(&phy->dev,
+						     s32g_serdes_phy_xlate);
+	if (IS_ERR(phy_provider))
+		return PTR_ERR(phy_provider);
+
+	return 0;
+}
+
+static int s32g_serdes_parse_lanes(struct device *dev,
+				   struct s32g_serdes *serdes)
+{
+	int ret;
+
+	for_each_available_child_of_node_scoped(dev->of_node, of_port) {
+		ret = s32g_serdes_parse_lane(serdes, of_port);
+		if (ret)
+			return ret;
+	}
+
+	/*
+	 * In the x2 mode both lane children are PCIe, but they form one link
+	 * behind one controller, so create a single phy bound to the first
+	 * PCIe child. One phy per child would register two providers for one
+	 * link.
+	 */
+	for_each_available_child_of_node_scoped(dev->of_node, of_port) {
+		if (!of_device_is_compatible(of_port,
+					     "nxp,s32g-serdes-pcie-phy"))
+			continue;
+
+		ret = s32g_serdes_create_pcie_phy(serdes, of_port);
+		if (ret)
+			return ret;
+
+		break;
+	}
+
+	return 0;
+}
+
+static int s32g_serdes_setup_xpcs_lanes(struct s32g_serdes *serdes)
+{
+	unsigned int i;
+	int ret;
+
+	for (i = 0; i < ARRAY_SIZE(serdes->lane); i++) {
+		if (serdes->lane[i].fn != S32G_LANE_XPCS)
+			continue;
+
+		ret = s32g_serdes_xpcs_setup(serdes,
+					     serdes->lane[i].xpcs_instance);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static int s32g_serdes_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	const struct s32g_serdes_soc_data *soc_data;
+	struct s32g_serdes *serdes;
+	int ret;
+
+	serdes = devm_kzalloc(dev, sizeof(*serdes), GFP_KERNEL);
+	if (!serdes)
+		return -ENOMEM;
+
+	serdes->dev = dev;
+
+	soc_data = of_device_get_match_data(dev);
+	if (!soc_data)
+		return -EINVAL;
+
+	serdes->soc_data = soc_data;
+
+	ret = s32g_serdes_get_ctrl_resources(pdev, serdes);
+	if (ret)
+		return ret;
+
+	ret = s32g_serdes_get_pcie_resources(pdev, serdes);
+	if (ret)
+		return ret;
+
+	ret = s32g_serdes_get_xpcs_resources(pdev, serdes);
+	if (ret)
+		return ret;
+
+	ret = s32g_serdes_parse_lanes(dev, serdes);
+	if (ret)
+		return ret;
+
+	ret = s32g_serdes_derive_mode(serdes);
+	if (ret)
+		return ret;
+
+	ret = s32g_serdes_setup_xpcs_lanes(serdes);
+	if (ret)
+		return ret;
+
+	ret = s32g_serdes_init(serdes);
+	if (ret)
+		return ret;
+
+	ret = devm_add_action_or_reset(dev, s32g_serdes_disable, serdes);
+	if (ret)
+		return ret;
+
+	/*
+	 * Publish drvdata last: s32g_serdes_pcs_create() takes a NULL drvdata
+	 * as its -EPROBE_DEFER condition, so setting it earlier can hand a MAC
+	 * a PCS while the PLLs are still down, or after a failed probe has
+	 * released it.
+	 */
+	platform_set_drvdata(pdev, serdes);
+
+	return 0;
+}
+
+static int s32g_serdes_suspend(struct device *device)
+{
+	struct s32g_serdes *serdes = dev_get_drvdata(device);
+
+	clk_bulk_disable_unprepare(serdes->ctrl.nclks, serdes->ctrl.clks);
+
+	return 0;
+}
+
+static int s32g_serdes_resume(struct device *device)
+{
+	struct s32g_serdes *serdes = dev_get_drvdata(device);
+	struct s32g_pcie_ctrl *pcie = &serdes->pcie;
+	int ret;
+
+	ret = s32g_serdes_init(serdes);
+	if (ret) {
+		dev_err(device, "Failed to initialize\n");
+		return ret;
+	}
+
+	/* Restore PCIe phy power */
+	if (pcie->powered_on) {
+		ret = s32g_pcie_phy_power_on(serdes);
+		if (ret)
+			dev_err(device, "Failed to power-on PCIe phy\n");
+	}
+
+	return ret;
+}
+
+/**
+ * s32g_serdes_pcs_create() - look up the phylink PCS for a SerDes XPCS lane
+ * @dev: the consumer (MAC) device
+ * @np: the SerDes XPCS lane child node
+ *
+ * A device link is created between @dev and the SerDes device so that the
+ * MAC is unbound before the SerDes it depends on. If the SerDes device has
+ * not probed (far enough) yet, -EPROBE_DEFER is returned so the caller
+ * retries later.
+ *
+ * Return: the phylink_pcs on success, or an ERR_PTR() on failure. The
+ * returned phylink_pcs is owned by the SerDes device and must not be freed
+ * by the caller.
+ */
+struct phylink_pcs *s32g_serdes_pcs_create(struct device *dev,
+					   struct device_node *np)
+{
+	struct platform_device *pdev;
+	struct device_node *pcs_np;
+	struct s32g_serdes *serdes;
+	struct device_link *link;
+	u32 instance;
+
+	/*
+	 * The lane child's unit address is the physical lane; the PCS is
+	 * identified by the XPCS instance routed to that lane.
+	 */
+	if (of_property_read_u32(np, "nxp,xpcs-instance", &instance))
+		return ERR_PTR(-EINVAL);
+
+	if (instance >= S32G_SERDES_XPCS_MAX)
+		return ERR_PTR(-EINVAL);
+
+	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);
+	}
+
+	pdev = of_find_device_by_node(pcs_np);
+	of_node_put(pcs_np);
+	if (!pdev)
+		return ERR_PTR(-EPROBE_DEFER);
+
+	serdes = platform_get_drvdata(pdev);
+	if (!serdes) {
+		put_device(&pdev->dev);
+		return ERR_PTR(-EPROBE_DEFER);
+	}
+
+	if (!serdes->xpcs.phys[instance]) {
+		put_device(&pdev->dev);
+		return ERR_PTR(-EPROBE_DEFER);
+	}
+
+	link = device_link_add(dev, &pdev->dev, DL_FLAG_AUTOREMOVE_CONSUMER);
+	put_device(&pdev->dev);
+	if (!link)
+		return ERR_PTR(-EINVAL);
+
+	return s32g_xpcs_pcs(serdes->xpcs.phys[instance]);
+}
+EXPORT_SYMBOL_GPL(s32g_serdes_pcs_create);
+
+static const struct of_device_id s32g_serdes_match[] = {
+	{
+		.compatible = "nxp,s32g2-serdes",
+		.data = &s32g2_serdes_soc_data,
+	},
+	{
+		.compatible = "nxp,s32g3-serdes",
+		.data = &s32g3_serdes_soc_data,
+	},
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, s32g_serdes_match);
+
+/* noirq, like the PCIe controller using this PHY */
+static const struct dev_pm_ops s32g_serdes_pm_ops = {
+	NOIRQ_SYSTEM_SLEEP_PM_OPS(s32g_serdes_suspend,
+				  s32g_serdes_resume)
+};
+
+static struct platform_driver s32g_serdes_driver = {
+	.probe		= s32g_serdes_probe,
+	.driver		= {
+		.name	= "phy-s32g-serdes",
+		.of_match_table = s32g_serdes_match,
+		.pm = pm_sleep_ptr(&s32g_serdes_pm_ops),
+	},
+};
+module_platform_driver(s32g_serdes_driver);
+
+MODULE_AUTHOR("Ghennadi Procopciuc <ghennadi.procopciuc@nxp.com>");
+MODULE_DESCRIPTION("NXP S32G SerDes driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/phy/nxp-s32g-serdes.h b/include/linux/phy/nxp-s32g-serdes.h
new file mode 100644
index 000000000000..d14fcde1d933
--- /dev/null
+++ b/include/linux/phy/nxp-s32g-serdes.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright 2021-2026 NXP
+ */
+#ifndef PHY_NXP_S32G_SERDES_H
+#define PHY_NXP_S32G_SERDES_H
+
+#include <linux/err.h>
+
+struct device;
+struct device_node;
+struct phylink_pcs;
+
+#if IS_REACHABLE(CONFIG_PHY_S32G_SERDES)
+struct phylink_pcs *s32g_serdes_pcs_create(struct device *dev,
+					   struct device_node *np);
+#else
+static inline struct phylink_pcs *
+s32g_serdes_pcs_create(struct device *dev, struct device_node *np)
+{
+	return ERR_PTR(-ENODEV);
+}
+#endif
+
+#endif /* PHY_NXP_S32G_SERDES_H */

-- 
2.55.0



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

* [PATCH RFC v3 07/12] net: stmmac: dwmac-s32: Add SGMII support
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
                   ` (5 preceding siblings ...)
  2026-09-19  6:54 ` [PATCH RFC v3 06/12] phy: freescale: s32g: Add SerDes subsystem PHY Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19 12:04   ` Maxime Chevallier
  2026-09-19  6:54 ` [PATCH RFC v3 08/12] MAINTAINERS: Add NXP S32G SerDes and SerDes xPCS core entries Jan Petrous via B4 Relay
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS)

From: "Jan Petrous (OSS)" <jan.petrous@oss.nxp.com>

Wire the NXP S32G SerDes XPCS PCS into the dwmac-s32 glue layer so the
MAC can run SGMII over a SerDes lane.

Changes:
- s32_gmac_write_phy_intf_select: replace the hard-coded RGMII selector
  with a switch on phy_interface so SGMII (0x01), RGMII, RMII and MII
  all programme the SoC control register correctly.
- Add s32_gmac_pcs_init: obtain the phylink_pcs handle from the SerDes
  subsystem via s32g_serdes_pcs_create(), keyed by the standard
  pcs-handle property on the GMAC node pointing at the SerDes XPCS lane
  child. Propagates -EPROBE_DEFER if the SerDes driver has not probed
  yet. There is no matching exit hook: the device link created by
  s32g_serdes_pcs_create() covers the lifetime.
- Add s32_gmac_select_pcs: return the stored PCS handle when the
  negotiated interface is SGMII.
- Hook pcs_init and select_pcs into plat_stmmacenet_data.

Note that installing a select_pcs callback suppresses the core's
integrated_pcs fallback, because stmmac treats a NULL return from
select_pcs as "no PCS" rather than "fall through". This matches what
dwmac-rzn1 and dwmac-intel already do.

Both negotiation modes are supported by the PCS. The board device
trees added later in this series describe the MAC-side link as fixed,
so they exercise the out-of-band path.

The XPCS header guards its s32g_serdes_pcs_create() declaration with
IS_REACHABLE(CONFIG_PHY_S32G_SERDES) and provides a static-inline stub
returning -ENODEV otherwise. IS_REACHABLE is false both when the SerDes
driver is disabled and in the built-in-consumer / modular-provider case
(DWMAC_S32=y with PHY_S32G_SERDES=m), so this driver links in all four
combinations of the two symbols.

Linking is not sufficient on its own, though. In the DWMAC_S32=y with
PHY_S32G_SERDES=m case the stub makes s32_gmac_pcs_init() return
-ENODEV, and stmmac treats a failing plat->pcs_init() as fatal, so a
built-in MAC would refuse to probe on any device tree describing
pcs-handle. Add a Kconfig dependency forbidding that combination
rather than letting it build into a MAC that cannot probe.

Tested on an S32G3-VNP-RDB3 board: GMAC0 links at 1G over SGMII and
passes traffic, both in SerDes mode 1 (alongside two active PCIe root
complexes) and in mode 3.

Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 drivers/net/ethernet/stmicro/stmmac/Kconfig     |  6 ++
 drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c | 74 ++++++++++++++++++++++++-
 2 files changed, 78 insertions(+), 2 deletions(-)

diff --git a/drivers/net/ethernet/stmicro/stmmac/Kconfig b/drivers/net/ethernet/stmicro/stmmac/Kconfig
index e3dd5adda5ac..1443ff4149b4 100644
--- a/drivers/net/ethernet/stmicro/stmmac/Kconfig
+++ b/drivers/net/ethernet/stmicro/stmmac/Kconfig
@@ -192,6 +192,12 @@ config DWMAC_S32
 	tristate "NXP S32G/S32R GMAC support"
 	default ARCH_S32
 	depends on OF && (ARCH_S32 || COMPILE_TEST)
+	# SGMII needs the PCS from the SerDes driver. The header falls back
+	# to a stub returning -ENODEV when that driver is not reachable, and
+	# stmmac makes a failing plat->pcs_init() fatal, so a built-in MAC
+	# with a modular SerDes would refuse to probe on any DT describing
+	# pcs-handle. Forbid that combination instead.
+	depends on PHY_S32G_SERDES || PHY_S32G_SERDES=n
 	help
 	  Support for ethernet controller on NXP S32CC SOCs.
 
diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c
index 024d8e10e918..a3a56b21181a 100644
--- a/drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c
+++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c
@@ -16,12 +16,14 @@
 #include <linux/of_mdio.h>
 #include <linux/of_address.h>
 #include <linux/phy.h>
+#include <linux/phy/nxp-s32g-serdes.h>
 #include <linux/phylink.h>
 #include <linux/platform_device.h>
 #include <linux/regmap.h>
 #include <linux/stmmac.h>
 
 #include "stmmac_platform.h"
+#include "stmmac.h"
 
 #define GMAC_INTF_RATE_125M	125000000	/* 125MHz */
 
@@ -40,23 +42,89 @@ struct s32_priv_data {
 	phy_interface_t *intf_mode;
 	struct clk *tx_clk;
 	struct clk *rx_clk;
+	/* SGMII PCS provided by the SerDes subsystem (NULL if not SGMII) */
+	struct phylink_pcs *pcs;
 };
 
 static int s32_gmac_write_phy_intf_select(struct s32_priv_data *gmac)
 {
+	u32 intf_sel;
 	int ret = 0;
 
+	switch (*gmac->intf_mode) {
+	case PHY_INTERFACE_MODE_SGMII:
+		intf_sel = S32_PHY_INTF_SEL_SGMII;
+		break;
+	case PHY_INTERFACE_MODE_RGMII:
+	case PHY_INTERFACE_MODE_RGMII_ID:
+	case PHY_INTERFACE_MODE_RGMII_RXID:
+	case PHY_INTERFACE_MODE_RGMII_TXID:
+		intf_sel = S32_PHY_INTF_SEL_RGMII;
+		break;
+	case PHY_INTERFACE_MODE_RMII:
+		intf_sel = S32_PHY_INTF_SEL_RMII;
+		break;
+	case PHY_INTERFACE_MODE_MII:
+		intf_sel = S32_PHY_INTF_SEL_MII;
+		break;
+	default:
+		dev_err(gmac->dev, "Unsupported phy interface mode: %s\n",
+			phy_modes(*gmac->intf_mode));
+		return -EINVAL;
+	}
+
 	if (gmac->ctrl_sts)
-		writel(S32_PHY_INTF_SEL_RGMII, gmac->ctrl_sts);
+		writel(intf_sel, gmac->ctrl_sts);
 	else
 		ret = regmap_write(gmac->sts_regmap, gmac->sts_offset,
-				   S32_PHY_INTF_SEL_RGMII);
+				   intf_sel);
 
 	dev_dbg(gmac->dev, "PHY mode set to %s\n", phy_modes(*gmac->intf_mode));
 
 	return ret;
 }
 
+static int s32_gmac_pcs_init(struct stmmac_priv *priv)
+{
+	struct s32_priv_data *gmac = priv->plat->bsp_priv;
+	struct device_node *pcs_node;
+
+	/*
+	 * pcs-handle points at the SerDes XPCS lane child node; its
+	 * nxp,xpcs-instance property is what identifies the XPCS instance.
+	 */
+	pcs_node = of_parse_phandle(gmac->dev->of_node, "pcs-handle", 0);
+	if (!pcs_node) {
+		dev_dbg(gmac->dev, "no 'pcs-handle' property, SGMII PCS unavailable\n");
+		return 0;
+	}
+
+	gmac->pcs = s32g_serdes_pcs_create(gmac->dev, pcs_node);
+	of_node_put(pcs_node);
+
+	if (IS_ERR(gmac->pcs)) {
+		int err = PTR_ERR(gmac->pcs);
+
+		gmac->pcs = NULL;
+		/* SerDes may not be probed yet - defer probe. */
+		return err;
+	}
+
+	dev_dbg(gmac->dev, "SGMII PCS created via SerDes XPCS\n");
+	return 0;
+}
+
+static struct phylink_pcs *s32_gmac_select_pcs(struct stmmac_priv *priv,
+					       phy_interface_t interface)
+{
+	struct s32_priv_data *gmac = priv->plat->bsp_priv;
+
+	if (interface == PHY_INTERFACE_MODE_SGMII && gmac->pcs)
+		return gmac->pcs;
+
+	return NULL;
+}
+
 static int s32_gmac_init(struct device *dev, void *priv)
 {
 	struct s32_priv_data *gmac = priv;
@@ -204,6 +272,8 @@ static int s32_dwmac_probe(struct platform_device *pdev)
 
 	plat->init = s32_gmac_init;
 	plat->exit = s32_gmac_exit;
+	plat->pcs_init = s32_gmac_pcs_init;
+	plat->select_pcs = s32_gmac_select_pcs;
 
 	plat->clk_tx_i = gmac->tx_clk;
 	plat->set_clk_tx_rate = stmmac_set_clk_tx_rate;

-- 
2.55.0



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

* [PATCH RFC v3 08/12] MAINTAINERS: Add NXP S32G SerDes and SerDes xPCS core entries
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
                   ` (6 preceding siblings ...)
  2026-09-19  6:54 ` [PATCH RFC v3 07/12] net: stmmac: dwmac-s32: Add SGMII support Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 09/12] arm64: dts: s32g: Add SCMI reset controller Jan Petrous via B4 Relay
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS)

From: Vincent Guittot <vincent.guittot@linaro.org>

Add two entries.

ARM/NXP S32G SERDES DRIVER covers the S32G specific parts: the SerDes
PHY driver, the S32G XPCS platform glue, their headers and the SerDes
devicetree binding. The R: and name forms match the adjacent ARM/NXP
S32G/S32R DWMAC ETHERNET DRIVER entry, and netdev is listed because the
PCS glue lives under drivers/net/pcs/.

NXP SERDES XPCS SHARED CORE covers the transport agnostic core on its
own. The core is not S32G specific - it is shared with i.MX9, whose
glue is a separate driver - so keeping it in the S32G entry would make
the S32G maintainers the default contact for i.MX transport work and
vice versa. This follows how drivers/net/pcs/ is already organised:
SYNOPSYS DESIGNWARE ETHERNET XPCS DRIVER carries the shared pcs-xpcs
core, while each vendor glue (pcs-xpcs-nxp.c, pcs-xpcs-wx.c,
pcs-rzn1-miic.c) is listed under its own vendor entry. Only netdev is
listed for the core, so i.MX changes to it do not reach the ARM lists.

Signed-off-by: Vincent Guittot <vincent.guittot@linaro.org>
Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 MAINTAINERS | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 3b2eb2a7a89a..38c6b5e1af0d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3371,6 +3371,20 @@ S:	Maintained
 F:	Documentation/devicetree/bindings/pci/nxp,s32g-pcie.yaml
 F:	drivers/pci/controller/dwc/pcie-nxp-s32g*
 
+ARM/NXP S32G SERDES DRIVER
+M:	Ciprian Marian Costea <ciprianmarian.costea@oss.nxp.com>
+M:	Jan Petrous <jan.petrous@oss.nxp.com>
+R:	NXP S32 Linux Team <s32@nxp.com>
+L:	linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
+L:	linux-phy@lists.infradead.org
+L:	netdev@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/phy/nxp,s32g-serdes.yaml
+F:	drivers/net/pcs/pcs-nxp-s32g-xpcs.c
+F:	drivers/phy/freescale/phy-nxp-s32g-serdes.c
+F:	include/linux/pcs/pcs-nxp-s32g-xpcs.h
+F:	include/linux/phy/nxp-s32g-serdes.h
+
 ARM/NXP S32G/S32R DWMAC ETHERNET DRIVER
 M:	Jan Petrous <jan.petrous@oss.nxp.com>
 R:	s32@nxp.com
@@ -19788,6 +19802,15 @@ S:	Maintained
 F:	Documentation/devicetree/bindings/extcon/extcon-ptn5150.yaml
 F:	drivers/extcon/extcon-ptn5150.c
 
+NXP SERDES XPCS SHARED CORE
+M:	Jan Petrous <jan.petrous@oss.nxp.com>
+M:	Clark Wang <xiaoning.wang@nxp.com>
+R:	NXP S32 Linux Team <s32@nxp.com>
+L:	netdev@vger.kernel.org
+S:	Maintained
+F:	drivers/net/pcs/pcs-nxp-serdes-xpcs.c
+F:	include/linux/pcs/pcs-nxp-serdes-xpcs.h
+
 NXP SGTL5000 DRIVER
 M:	Fabio Estevam <festevam@gmail.com>
 L:	linux-sound@vger.kernel.org

-- 
2.55.0



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

* [PATCH RFC v3 09/12] arm64: dts: s32g: Add SCMI reset controller
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
                   ` (7 preceding siblings ...)
  2026-09-19  6:54 ` [PATCH RFC v3 08/12] MAINTAINERS: Add NXP S32G SerDes and SerDes xPCS core entries Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 10/12] arm64: dts: s32g: Add SerDes controller nodes Jan Petrous via B4 Relay
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS)

From: "Jan Petrous (OSS)" <jan.petrous@oss.nxp.com>

Add the SCMI reset domain protocol (protocol@16) node under the SCMI
firmware node for both S32G2 and S32G3, so that on-SoC controllers can
request resets through the SCMI reset controller.

The node is labelled scmi_reset rather than reset: a label that
generic in a SoC .dtsi invites collisions and reads poorly at the use
sites, and the sibling clock protocol node already uses the shortened
clks form.

Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 arch/arm64/boot/dts/freescale/s32g2.dtsi | 5 +++++
 arch/arm64/boot/dts/freescale/s32g3.dtsi | 5 +++++
 2 files changed, 10 insertions(+)

diff --git a/arch/arm64/boot/dts/freescale/s32g2.dtsi b/arch/arm64/boot/dts/freescale/s32g2.dtsi
index 996494e34593..6a2774d53747 100644
--- a/arch/arm64/boot/dts/freescale/s32g2.dtsi
+++ b/arch/arm64/boot/dts/freescale/s32g2.dtsi
@@ -100,6 +100,11 @@ clks: protocol@14 {
 				reg = <0x14>;
 				#clock-cells = <1>;
 			};
+
+			scmi_reset: protocol@16 {
+				reg = <0x16>;
+				#reset-cells = <1>;
+			};
 		};
 
 		psci {
diff --git a/arch/arm64/boot/dts/freescale/s32g3.dtsi b/arch/arm64/boot/dts/freescale/s32g3.dtsi
index 31f7aad00b76..da3915b7e915 100644
--- a/arch/arm64/boot/dts/freescale/s32g3.dtsi
+++ b/arch/arm64/boot/dts/freescale/s32g3.dtsi
@@ -139,6 +139,11 @@ clks: protocol@14 {
 				reg = <0x14>;
 				#clock-cells = <1>;
 			};
+
+			scmi_reset: protocol@16 {
+				reg = <0x16>;
+				#reset-cells = <1>;
+			};
 		};
 
 		psci: psci {

-- 
2.55.0



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

* [PATCH RFC v3 10/12] arm64: dts: s32g: Add SerDes controller nodes
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
                   ` (8 preceding siblings ...)
  2026-09-19  6:54 ` [PATCH RFC v3 09/12] arm64: dts: s32g: Add SCMI reset controller Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 11/12] arm64: dts: s32g: Add PCIe " Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 12/12] arm64: dts: s32g: Add S32G3-RDB3 SerDes routing variants Jan Petrous via B4 Relay
  11 siblings, 0 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS)

From: "Jan Petrous (OSS)" <jan.petrous@oss.nxp.com>

Add the SerDes/PCIe combo-PHY controller nodes for S32G2 and S32G3 in
the disabled state. Each SoC integrates two SerDes instances (SerDes_0
and SerDes_1) with identical register layout; every instance exposes
the PCIe PHY and the two XPCS instances used for SGMII. Board device
trees enable the required instance and add the lane PHY child nodes
matching the wanted SerDes subsystem mode.

Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 arch/arm64/boot/dts/freescale/s32g2.dtsi | 32 ++++++++++++++++++++++++++++++++
 arch/arm64/boot/dts/freescale/s32g3.dtsi | 32 ++++++++++++++++++++++++++++++++
 2 files changed, 64 insertions(+)

diff --git a/arch/arm64/boot/dts/freescale/s32g2.dtsi b/arch/arm64/boot/dts/freescale/s32g2.dtsi
index 6a2774d53747..404913b208fb 100644
--- a/arch/arm64/boot/dts/freescale/s32g2.dtsi
+++ b/arch/arm64/boot/dts/freescale/s32g2.dtsi
@@ -877,6 +877,38 @@ gmac0mdio: mdio {
 			};
 		};
 
+		serdes0: serdes@40480000 {
+			compatible = "nxp,s32g2-serdes0", "nxp,s32g2-serdes";
+			reg = <0x40480000 0x108>,
+			      <0x40483008 0x10>,
+			      <0x40482000 0x800>,
+			      <0x40482800 0x800>;
+			reg-names = "ss-pcie", "pcie-phy", "xpcs0", "xpcs1";
+			clocks = <&clks 1>, <&clks 2>, <&clks 3>, <&clks 4>;
+			clock-names = "axi", "aux", "apb", "ref";
+			resets = <&scmi_reset 9>, <&scmi_reset 8>;
+			reset-names = "serdes", "pcie";
+			#address-cells = <1>;
+			#size-cells = <0>;
+			status = "disabled";
+		};
+
+		serdes1: serdes@44180000 {
+			compatible = "nxp,s32g2-serdes1", "nxp,s32g2-serdes";
+			reg = <0x44180000 0x108>,
+			      <0x44183008 0x10>,
+			      <0x44182000 0x800>,
+			      <0x44182800 0x800>;
+			reg-names = "ss-pcie", "pcie-phy", "xpcs0", "xpcs1";
+			clocks = <&clks 1>, <&clks 2>, <&clks 3>, <&clks 4>;
+			clock-names = "axi", "aux", "apb", "ref";
+			resets = <&scmi_reset 11>, <&scmi_reset 10>;
+			reset-names = "serdes", "pcie";
+			#address-cells = <1>;
+			#size-cells = <0>;
+			status = "disabled";
+		};
+
 		gic: interrupt-controller@50800000 {
 			compatible = "arm,gic-v3";
 			reg = <0x50800000 0x10000>,
diff --git a/arch/arm64/boot/dts/freescale/s32g3.dtsi b/arch/arm64/boot/dts/freescale/s32g3.dtsi
index da3915b7e915..0d001b2895c6 100644
--- a/arch/arm64/boot/dts/freescale/s32g3.dtsi
+++ b/arch/arm64/boot/dts/freescale/s32g3.dtsi
@@ -956,6 +956,38 @@ gmac0mdio: mdio {
 			};
 		};
 
+		serdes0: serdes@40480000 {
+			compatible = "nxp,s32g3-serdes0", "nxp,s32g3-serdes";
+			reg = <0x40480000 0x108>,
+			      <0x40483008 0x10>,
+			      <0x40482000 0x800>,
+			      <0x40482800 0x800>;
+			reg-names = "ss-pcie", "pcie-phy", "xpcs0", "xpcs1";
+			clocks = <&clks 1>, <&clks 2>, <&clks 3>, <&clks 4>;
+			clock-names = "axi", "aux", "apb", "ref";
+			resets = <&scmi_reset 9>, <&scmi_reset 8>;
+			reset-names = "serdes", "pcie";
+			#address-cells = <1>;
+			#size-cells = <0>;
+			status = "disabled";
+		};
+
+		serdes1: serdes@44180000 {
+			compatible = "nxp,s32g3-serdes1", "nxp,s32g3-serdes";
+			reg = <0x44180000 0x108>,
+			      <0x44183008 0x10>,
+			      <0x44182000 0x800>,
+			      <0x44182800 0x800>;
+			reg-names = "ss-pcie", "pcie-phy", "xpcs0", "xpcs1";
+			clocks = <&clks 1>, <&clks 2>, <&clks 3>, <&clks 4>;
+			clock-names = "axi", "aux", "apb", "ref";
+			resets = <&scmi_reset 11>, <&scmi_reset 10>;
+			reset-names = "serdes", "pcie";
+			#address-cells = <1>;
+			#size-cells = <0>;
+			status = "disabled";
+		};
+
 		swt8: watchdog@40500000 {
 			compatible = "nxp,s32g3-swt", "nxp,s32g2-swt";
 			reg = <0x40500000 0x1000>;

-- 
2.55.0



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

* [PATCH RFC v3 11/12] arm64: dts: s32g: Add PCIe controller nodes
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
                   ` (9 preceding siblings ...)
  2026-09-19  6:54 ` [PATCH RFC v3 10/12] arm64: dts: s32g: Add SerDes controller nodes Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  2026-09-19  6:54 ` [PATCH RFC v3 12/12] arm64: dts: s32g: Add S32G3-RDB3 SerDes routing variants Jan Petrous via B4 Relay
  11 siblings, 0 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS)

From: "Jan Petrous (OSS)" <jan.petrous@oss.nxp.com>

Add the two PCIe controllers of the S32G2 and S32G3, disabled. Each is
the root complex half of one SerDes subsystem: PCIe_0 pairs with
SerDes_0 and PCIe_1 with SerDes_1. A board device tree that routes a
SerDes lane to PCIe enables the controller and points the root port at
the lane PHY child node of the matching SerDes.

Both SoCs integrate the controllers identically - same register layout
and same GIC SPI assignment - so the nodes differ only in the
compatible: S32G3 adds "nxp,s32g3-pcie" ahead of the "nxp,s32g2-pcie"
base, as the binding describes. The S32G3-VNP-RDB3 and the
S32G2-VNP-RDB2 wire the same two root complexes to the M.2 slots and
the PCIe card slot.

Their configuration space (0x5f_ffffe000 and 0x4f_ffffe000) lies above
the 32-bit window mapped by soc@0, so the controllers are described at
the device tree root rather than under soc@0.

The root ports are labelled (pcie0_rp, pcie1_rp) so board device trees
can extend them by label. An overlay that instead re-declares a pcie@0
child inside the controller node makes dtc emit a unit_address_vs_reg
warning, because the overlay alone carries no reg for that node.

The GIC nodes gain '#address-cells = <0>'. The PCIe interrupt-map
entries name the GIC as their interrupt parent, and without it dtc
warns on every entry and falls back to zero.

Only the S32G3-VNP-RDB3 board device trees in this series enable a
controller; the S32G2 nodes stay disabled until a board enables them.

Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 arch/arm64/boot/dts/freescale/s32g2.dtsi | 81 ++++++++++++++++++++++++++++++++
 arch/arm64/boot/dts/freescale/s32g3.dtsi | 81 ++++++++++++++++++++++++++++++++
 2 files changed, 162 insertions(+)

diff --git a/arch/arm64/boot/dts/freescale/s32g2.dtsi b/arch/arm64/boot/dts/freescale/s32g2.dtsi
index 404913b208fb..539840340318 100644
--- a/arch/arm64/boot/dts/freescale/s32g2.dtsi
+++ b/arch/arm64/boot/dts/freescale/s32g2.dtsi
@@ -918,7 +918,88 @@ gic: interrupt-controller@50800000 {
 			      <0x50420000 0x2000>;
 			interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;
 			interrupt-controller;
+			#address-cells = <0>;
 			#interrupt-cells = <3>;
 		};
 	};
+
+	/*
+	 * PCIe controllers. Their config space (0x5f_ffffe000 / 0x4f_ffffe000)
+	 * lies above the 32-bit window mapped by soc@0, so the root complexes
+	 * are described at the device tree root. Disabled here; a board file
+	 * that wires a lane to PCIe enables the controller and adds the phys.
+	 */
+	pcie0: pcie@40400000 {
+		compatible = "nxp,s32g2-pcie";
+		reg = <0x00 0x40400000 0x0 0x00001000>,	/* dbi */
+		      <0x00 0x40420000 0x0 0x00001000>,	/* dbi2 */
+		      <0x00 0x40460000 0x0 0x00001000>,	/* atu */
+		      <0x00 0x40470000 0x0 0x00001000>,	/* dma */
+		      <0x00 0x40481000 0x0 0x000000f8>,	/* ctrl */
+		      <0x5f 0xffffe000 0x0 0x00002000>;	/* config */
+		reg-names = "dbi", "dbi2", "atu", "dma", "ctrl", "config";
+		dma-coherent;
+		#address-cells = <3>;
+		#size-cells = <2>;
+		device_type = "pci";
+		ranges = <0x01000000 0x0 0x00000000 0x5f 0xfffe0000 0x0 0x00010000>,
+			 <0x02000000 0x0 0x00000000 0x58 0x00000000 0x0 0x80000000>,
+			 <0x02000000 0x1 0x00000000 0x59 0x00000000 0x6 0xfffe0000>;
+		bus-range = <0x0 0xff>;
+		interrupts = <GIC_SPI 125 IRQ_TYPE_LEVEL_HIGH>,
+			     <GIC_SPI 123 IRQ_TYPE_LEVEL_HIGH>;
+		interrupt-names = "msi", "dma";
+		#interrupt-cells = <1>;
+		interrupt-map-mask = <0 0 0 0x7>;
+		interrupt-map = <0 0 0 1 &gic GIC_SPI 128 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 2 &gic GIC_SPI 129 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 3 &gic GIC_SPI 130 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 4 &gic GIC_SPI 131 IRQ_TYPE_LEVEL_HIGH>;
+		status = "disabled";
+
+		pcie0_rp: pcie@0 {
+			reg = <0x0 0x0 0x0 0x0 0x0>;
+			#address-cells = <3>;
+			#size-cells = <2>;
+			ranges;
+			device_type = "pci";
+		};
+	};
+
+	pcie1: pcie@44100000 {
+		compatible = "nxp,s32g2-pcie";
+		reg = <0x00 0x44100000 0x0 0x00001000>,	/* dbi */
+		      <0x00 0x44120000 0x0 0x00001000>,	/* dbi2 */
+		      <0x00 0x44160000 0x0 0x00001000>,	/* atu */
+		      <0x00 0x44170000 0x0 0x00001000>,	/* dma */
+		      <0x00 0x44181000 0x0 0x000000f8>,	/* ctrl */
+		      <0x4f 0xffffe000 0x0 0x00002000>;	/* config */
+		reg-names = "dbi", "dbi2", "atu", "dma", "ctrl", "config";
+		dma-coherent;
+		#address-cells = <3>;
+		#size-cells = <2>;
+		device_type = "pci";
+		ranges = <0x01000000 0x0 0x00000000 0x4f 0xfffe0000 0x0 0x00010000>,
+			 <0x02000000 0x0 0x00000000 0x48 0x00000000 0x0 0x80000000>,
+			 <0x02000000 0x1 0x00000000 0x49 0x00000000 0x6 0xfffe0000>;
+		bus-range = <0x0 0xff>;
+		interrupts = <GIC_SPI 216 IRQ_TYPE_LEVEL_HIGH>,
+			     <GIC_SPI 214 IRQ_TYPE_LEVEL_HIGH>;
+		interrupt-names = "msi", "dma";
+		#interrupt-cells = <1>;
+		interrupt-map-mask = <0 0 0 0x7>;
+		interrupt-map = <0 0 0 1 &gic GIC_SPI 219 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 2 &gic GIC_SPI 220 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 3 &gic GIC_SPI 221 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 4 &gic GIC_SPI 222 IRQ_TYPE_LEVEL_HIGH>;
+		status = "disabled";
+
+		pcie1_rp: pcie@0 {
+			reg = <0x0 0x0 0x0 0x0 0x0>;
+			#address-cells = <3>;
+			#size-cells = <2>;
+			ranges;
+			device_type = "pci";
+		};
+	};
 };
diff --git a/arch/arm64/boot/dts/freescale/s32g3.dtsi b/arch/arm64/boot/dts/freescale/s32g3.dtsi
index 0d001b2895c6..fdf086363c02 100644
--- a/arch/arm64/boot/dts/freescale/s32g3.dtsi
+++ b/arch/arm64/boot/dts/freescale/s32g3.dtsi
@@ -1058,6 +1058,7 @@ stm11: timer@4052c000 {
 
 		gic: interrupt-controller@50800000 {
 			compatible = "arm,gic-v3";
+			#address-cells = <0>;
 			#interrupt-cells = <3>;
 			interrupt-controller;
 			reg = <0x50800000 0x10000>,
@@ -1069,6 +1070,86 @@ gic: interrupt-controller@50800000 {
 		};
 	};
 
+	/*
+	 * PCIe controllers. Their config space (0x5f_ffffe000 / 0x4f_ffffe000)
+	 * lies above the 32-bit window mapped by soc@0, so the root complexes
+	 * are described at the device tree root. Disabled here; a board file
+	 * that wires a lane to PCIe enables the controller and adds the phys.
+	 */
+	pcie0: pcie@40400000 {
+		compatible = "nxp,s32g3-pcie", "nxp,s32g2-pcie";
+		reg = <0x00 0x40400000 0x0 0x00001000>,	/* dbi */
+		      <0x00 0x40420000 0x0 0x00001000>,	/* dbi2 */
+		      <0x00 0x40460000 0x0 0x00001000>,	/* atu */
+		      <0x00 0x40470000 0x0 0x00001000>,	/* dma */
+		      <0x00 0x40481000 0x0 0x000000f8>,	/* ctrl */
+		      <0x5f 0xffffe000 0x0 0x00002000>;	/* config */
+		reg-names = "dbi", "dbi2", "atu", "dma", "ctrl", "config";
+		dma-coherent;
+		#address-cells = <3>;
+		#size-cells = <2>;
+		device_type = "pci";
+		ranges = <0x01000000 0x0 0x00000000 0x5f 0xfffe0000 0x0 0x00010000>,
+			 <0x02000000 0x0 0x00000000 0x58 0x00000000 0x0 0x80000000>,
+			 <0x02000000 0x1 0x00000000 0x59 0x00000000 0x6 0xfffe0000>;
+		bus-range = <0x0 0xff>;
+		interrupts = <GIC_SPI 125 IRQ_TYPE_LEVEL_HIGH>,
+			     <GIC_SPI 123 IRQ_TYPE_LEVEL_HIGH>;
+		interrupt-names = "msi", "dma";
+		#interrupt-cells = <1>;
+		interrupt-map-mask = <0 0 0 0x7>;
+		interrupt-map = <0 0 0 1 &gic GIC_SPI 128 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 2 &gic GIC_SPI 129 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 3 &gic GIC_SPI 130 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 4 &gic GIC_SPI 131 IRQ_TYPE_LEVEL_HIGH>;
+		status = "disabled";
+
+		pcie0_rp: pcie@0 {
+			reg = <0x0 0x0 0x0 0x0 0x0>;
+			#address-cells = <3>;
+			#size-cells = <2>;
+			ranges;
+			device_type = "pci";
+		};
+	};
+
+	pcie1: pcie@44100000 {
+		compatible = "nxp,s32g3-pcie", "nxp,s32g2-pcie";
+		reg = <0x00 0x44100000 0x0 0x00001000>,	/* dbi */
+		      <0x00 0x44120000 0x0 0x00001000>,	/* dbi2 */
+		      <0x00 0x44160000 0x0 0x00001000>,	/* atu */
+		      <0x00 0x44170000 0x0 0x00001000>,	/* dma */
+		      <0x00 0x44181000 0x0 0x000000f8>,	/* ctrl */
+		      <0x4f 0xffffe000 0x0 0x00002000>;	/* config */
+		reg-names = "dbi", "dbi2", "atu", "dma", "ctrl", "config";
+		dma-coherent;
+		#address-cells = <3>;
+		#size-cells = <2>;
+		device_type = "pci";
+		ranges = <0x01000000 0x0 0x00000000 0x4f 0xfffe0000 0x0 0x00010000>,
+			 <0x02000000 0x0 0x00000000 0x48 0x00000000 0x0 0x80000000>,
+			 <0x02000000 0x1 0x00000000 0x49 0x00000000 0x6 0xfffe0000>;
+		bus-range = <0x0 0xff>;
+		interrupts = <GIC_SPI 216 IRQ_TYPE_LEVEL_HIGH>,
+			     <GIC_SPI 214 IRQ_TYPE_LEVEL_HIGH>;
+		interrupt-names = "msi", "dma";
+		#interrupt-cells = <1>;
+		interrupt-map-mask = <0 0 0 0x7>;
+		interrupt-map = <0 0 0 1 &gic GIC_SPI 219 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 2 &gic GIC_SPI 220 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 3 &gic GIC_SPI 221 IRQ_TYPE_LEVEL_HIGH>,
+				<0 0 0 4 &gic GIC_SPI 222 IRQ_TYPE_LEVEL_HIGH>;
+		status = "disabled";
+
+		pcie1_rp: pcie@0 {
+			reg = <0x0 0x0 0x0 0x0 0x0>;
+			#address-cells = <3>;
+			#size-cells = <2>;
+			ranges;
+			device_type = "pci";
+		};
+	};
+
 	timer {
 		compatible = "arm,armv8-timer";
 		interrupt-parent = <&gic>;

-- 
2.55.0



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

* [PATCH RFC v3 12/12] arm64: dts: s32g: Add S32G3-RDB3 SerDes routing variants
  2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
                   ` (10 preceding siblings ...)
  2026-09-19  6:54 ` [PATCH RFC v3 11/12] arm64: dts: s32g: Add PCIe " Jan Petrous via B4 Relay
@ 2026-09-19  6:54 ` Jan Petrous via B4 Relay
  11 siblings, 0 replies; 16+ messages in thread
From: Jan Petrous via B4 Relay @ 2026-09-19  6:54 UTC (permalink / raw)
  To: Ciprian Marian Costea, NXP S32 Linux Team, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Chevallier, Maxime Coquelin,
	Alexandre Torgue, Chester Lin, Matthias Brugger,
	Ghennadi Procopciuc, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam, Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot,
	Jan Petrous (OSS)

From: "Jan Petrous (OSS)" <jan.petrous@oss.nxp.com>

The S32G3-VNP-RDB3 board wires GMAC0 to an on-board KSZ9031 RGMII PHY by
default. Add two device tree variants that reroute GMAC0 over the
SerDes_0 XPCS lane for SGMII, matching the two SerDes lane layouts:

  - Mode 1: PCIe x1 root complex on lane 0 plus 1G SGMII via GMAC0
    (XPCS0) on lane 1.
  - Mode 3: dual SGMII, GMAC0 (XPCS0) on lane 0 and PFE_EMAC_2 (XPCS1)
    on lane 1.

The SerDes SUBSYS_MODE is derived from the lane child nodes present, so
each layout requires its own board device tree. The Mode 1 variant
enables both PCIe root complexes and extends their root ports by label
(pcie0_rp, pcie1_rp), matching the RDB3 wiring: PCIe_0 reaches the M.2
slots when board switch SW17-2 is ON, PCIe_1 the PCIe card slot.

The GMAC references its PCS with the standard pcs-handle property. On
this board the SGMII lane runs back-to-back to the on-board SJA1110
switch, so there is no PHY on the segment and nothing for phy-handle to
point at - the link partner is a switch port. The MAC-side link is
therefore described as a fixed 1G link and phylink uses out-of-band
negotiation. The SJA1110 itself is not described by these overlays.

Verified on the board:

  - Mode 3: GMAC0 links at 1G and passes traffic.
  - Mode 1: GMAC0 SGMII path up, and both PCIe root complexes come up -
    PCIe_1 enumerates the card-slot NIC and passes traffic at full line
    rate, PCIe_0 enumerates the M.2 slot with SW17-2 ON.

Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
---
 arch/arm64/boot/dts/freescale/Makefile             |   6 ++
 .../dts/freescale/s32g399a-rdb3-serdes-mode1.dtso  | 111 +++++++++++++++++++++
 .../dts/freescale/s32g399a-rdb3-serdes-mode3.dtso  |  60 +++++++++++
 3 files changed, 177 insertions(+)

diff --git a/arch/arm64/boot/dts/freescale/Makefile b/arch/arm64/boot/dts/freescale/Makefile
index 95ae85ab4adf..3f68f36ac2be 100644
--- a/arch/arm64/boot/dts/freescale/Makefile
+++ b/arch/arm64/boot/dts/freescale/Makefile
@@ -786,5 +786,11 @@ dtb-$(CONFIG_ARCH_MXC) += imx8mm-phygate-tauri-l-rs232-rs485.dtb
 dtb-$(CONFIG_ARCH_S32) += s32g274a-evb.dtb
 dtb-$(CONFIG_ARCH_S32) += s32g274a-rdb2.dtb
 dtb-$(CONFIG_ARCH_S32) += s32g399a-rdb3.dtb
+dtb-$(CONFIG_ARCH_S32) += s32g399a-rdb3-serdes-mode1.dtb
+dtb-$(CONFIG_ARCH_S32) += s32g399a-rdb3-serdes-mode3.dtb
+s32g399a-rdb3-serdes-mode1-dtbs := s32g399a-rdb3.dtb s32g399a-rdb3-serdes-mode1.dtbo
+s32g399a-rdb3-serdes-mode3-dtbs := s32g399a-rdb3.dtb s32g399a-rdb3-serdes-mode3.dtbo
+dtb-$(CONFIG_ARCH_S32) += s32g399a-rdb3-serdes-mode1.dtbo
+dtb-$(CONFIG_ARCH_S32) += s32g399a-rdb3-serdes-mode3.dtbo
 dtb-$(CONFIG_ARCH_S32) += s32n79-rdb.dtb
 dtb-$(CONFIG_ARCH_S32) += s32v234-evb.dtb
diff --git a/arch/arm64/boot/dts/freescale/s32g399a-rdb3-serdes-mode1.dtso b/arch/arm64/boot/dts/freescale/s32g399a-rdb3-serdes-mode1.dtso
new file mode 100644
index 000000000000..43219d9bb6eb
--- /dev/null
+++ b/arch/arm64/boot/dts/freescale/s32g399a-rdb3-serdes-mode1.dtso
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
+/*
+ * Copyright 2021-2026 NXP
+ *
+ * NXP S32G3 Reference Design Board 3 (S32G-VNP-RDB3),
+ * SerDes_0 Mode 1: PCIe x1 (lane 0) + 1G SGMII via GMAC0 (XPCS0, lane 1).
+ *
+ * Overlay applied on top of s32g399a-rdb3.dtb. It reroutes GMAC0 from the
+ * on-board RGMII PHY to the SerDes_0 XPCS0 lane and brings up the PCIe
+ * controllers as root complexes. The lane layout present here selects
+ * SerDes SUBSYS_MODE 1.
+ */
+
+/dts-v1/;
+/plugin/;
+
+&serdes0 {
+	#address-cells = <1>;
+	#size-cells = <0>;
+	status = "okay";
+
+	/* Mode 1 lane layout: PCIe on lane 0, GMAC SGMII (XPCS0) on lane 1. */
+	serdes0_pcie0: phy@0 {
+		reg = <0>;
+		compatible = "nxp,s32g-serdes-pcie-phy";
+		#phy-cells = <0>;
+	};
+
+	serdes0_xpcs0: phy@1 {
+		reg = <1>;
+		compatible = "nxp,s32g-serdes-xpcs";
+		nxp,xpcs-instance = <0>;
+	};
+};
+
+&serdes1 {
+	#address-cells = <1>;
+	#size-cells = <0>;
+	status = "okay";
+
+	/*
+	 * SerDes_1 Mode 1 lane layout: PCIe on lane 0, SGMII (PFE_MAC0
+	 * via XPCS0) on lane 1.
+	 *
+	 * Both lanes must be described: the SerDes driver derives the
+	 * SUBSYS_MODE from the full lane mux, so lane 1 has to be present
+	 * as XPCS instance 0 (PFE_MAC0) for the mode-1 match to succeed.
+	 * PFE is not modeled as a MAC consumer in this upstreaming device
+	 * tree, so the XPCS lane is only brought up by the SerDes driver
+	 * and left without a pcs-handle user.
+	 */
+	serdes1_pcie0: phy@0 {
+		reg = <0>;
+		compatible = "nxp,s32g-serdes-pcie-phy";
+		#phy-cells = <0>;
+	};
+
+	serdes1_xpcs0: phy@1 {
+		reg = <1>;
+		compatible = "nxp,s32g-serdes-xpcs";
+		nxp,xpcs-instance = <0>;
+	};
+};
+
+/*
+ * Both PCIe root complexes are wired on the RDB3:
+ *  - PCIe_0 reaches the M.2 slots (M-key or E-key) when board switch
+ *    SW17-2 is set to ON. It consumes SerDes_0 lane-0 PCIe phy.
+ *  - PCIe_1 reaches the PCIe card slot. It consumes SerDes_1 lane-0 PCIe phy.
+ */
+&pcie0 {
+	status = "okay";
+};
+
+&pcie0_rp {
+	num-lanes = <1>;
+	phys = <&serdes0_pcie0>;
+};
+
+&pcie1 {
+	status = "okay";
+};
+
+&pcie1_rp {
+	num-lanes = <1>;
+	phys = <&serdes1_pcie0>;
+};
+
+&gmac0 {
+	/delete-property/ phy-handle;
+	clocks = <&clks 24>, <&clks 17>, <&clks 16>, <&clks 15>;
+	clock-names = "stmmaceth", "tx", "rx", "ptp_ref";
+	phy-mode = "sgmii";
+	pcs-handle = <&serdes0_xpcs0>;
+	status = "okay";
+
+	/*
+	 * GMAC0 is wired back-to-back to the on-board SJA1110 switch over
+	 * this SGMII lane. There is no PHY on the segment, so there is
+	 * nothing to reference with phy-handle: the link partner is a
+	 * switch port. Describe it as a fixed 1G link; phylink then uses
+	 * out-of-band negotiation. The switch itself is not described by
+	 * this overlay. The PCS also supports in-band negotiation, for
+	 * boards whose link partner is described.
+	 */
+	fixed-link {
+		speed = <1000>;
+		full-duplex;
+		pause;
+	};
+};
diff --git a/arch/arm64/boot/dts/freescale/s32g399a-rdb3-serdes-mode3.dtso b/arch/arm64/boot/dts/freescale/s32g399a-rdb3-serdes-mode3.dtso
new file mode 100644
index 000000000000..195c8043c18a
--- /dev/null
+++ b/arch/arm64/boot/dts/freescale/s32g399a-rdb3-serdes-mode3.dtso
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
+/*
+ * Copyright 2021-2026 NXP
+ *
+ * NXP S32G3 Reference Design Board 3 (S32G-VNP-RDB3),
+ * SerDes_0 Mode 3: dual SGMII, GMAC0 (XPCS0, lane 0) + PFE_EMAC_2
+ * (XPCS1, lane 1), both 1G.
+ *
+ * Overlay applied on top of s32g399a-rdb3.dtb. It reroutes GMAC0 from the
+ * on-board RGMII PHY to the SerDes_0 XPCS0 lane. The lane-1 XPCS1
+ * (PFE_EMAC_2) is described so the mode is derivable and both XPCS
+ * instances are initialised; it has no upstream MAC consumer in this
+ * device tree. The lane layout present here selects SerDes SUBSYS_MODE 3.
+ */
+
+/dts-v1/;
+/plugin/;
+
+&serdes0 {
+	#address-cells = <1>;
+	#size-cells = <0>;
+	status = "okay";
+
+	/* Mode 3 lane layout: GMAC SGMII (XPCS0) on lane 0, PFE2 (XPCS1) on lane 1. */
+	serdes0_xpcs0: phy@0 {
+		reg = <0>;
+		compatible = "nxp,s32g-serdes-xpcs";
+		nxp,xpcs-instance = <0>;
+	};
+
+	serdes0_xpcs1: phy@1 {
+		reg = <1>;
+		compatible = "nxp,s32g-serdes-xpcs";
+		nxp,xpcs-instance = <1>;
+	};
+};
+
+&gmac0 {
+	/delete-property/ phy-handle;
+	clocks = <&clks 24>, <&clks 17>, <&clks 16>, <&clks 15>;
+	clock-names = "stmmaceth", "tx", "rx", "ptp_ref";
+	phy-mode = "sgmii";
+	pcs-handle = <&serdes0_xpcs0>;
+	status = "okay";
+
+	/*
+	 * GMAC0 is wired back-to-back to the on-board SJA1110 switch over
+	 * this SGMII lane. There is no PHY on the segment, so there is
+	 * nothing to reference with phy-handle: the link partner is a
+	 * switch port. Describe it as a fixed 1G link; phylink then uses
+	 * out-of-band negotiation. The switch itself is not described by
+	 * this overlay. The PCS also supports in-band negotiation, for
+	 * boards whose link partner is described.
+	 */
+	fixed-link {
+		speed = <1000>;
+		full-duplex;
+		pause;
+	};
+};

-- 
2.55.0



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

* Re: [PATCH RFC v3 07/12] net: stmmac: dwmac-s32: Add SGMII support
  2026-09-19  6:54 ` [PATCH RFC v3 07/12] net: stmmac: dwmac-s32: Add SGMII support Jan Petrous via B4 Relay
@ 2026-09-19 12:04   ` Maxime Chevallier
  0 siblings, 0 replies; 16+ messages in thread
From: Maxime Chevallier @ 2026-09-19 12:04 UTC (permalink / raw)
  To: jan.petrous, Ciprian Marian Costea, NXP S32 Linux Team,
	Vinod Koul, Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Coquelin, Alexandre Torgue,
	Chester Lin, Matthias Brugger, Ghennadi Procopciuc, Frank Li,
	Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam,
	Richard Cochran
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot

Hi Jan,

On 9/19/26 08:54, Jan Petrous via B4 Relay wrote:
> From: "Jan Petrous (OSS)" <jan.petrous@oss.nxp.com>
> 
> Wire the NXP S32G SerDes XPCS PCS into the dwmac-s32 glue layer so the
> MAC can run SGMII over a SerDes lane.
> 
> Changes:
> - s32_gmac_write_phy_intf_select: replace the hard-coded RGMII selector
>   with a switch on phy_interface so SGMII (0x01), RGMII, RMII and MII
>   all programme the SoC control register correctly.
> - Add s32_gmac_pcs_init: obtain the phylink_pcs handle from the SerDes
>   subsystem via s32g_serdes_pcs_create(), keyed by the standard
>   pcs-handle property on the GMAC node pointing at the SerDes XPCS lane
>   child. Propagates -EPROBE_DEFER if the SerDes driver has not probed
>   yet. There is no matching exit hook: the device link created by
>   s32g_serdes_pcs_create() covers the lifetime.
> - Add s32_gmac_select_pcs: return the stored PCS handle when the
>   negotiated interface is SGMII.
> - Hook pcs_init and select_pcs into plat_stmmacenet_data.
> 
> Note that installing a select_pcs callback suppresses the core's
> integrated_pcs fallback, because stmmac treats a NULL return from
> select_pcs as "no PCS" rather than "fall through". This matches what
> dwmac-rzn1 and dwmac-intel already do.
> 
> Both negotiation modes are supported by the PCS. The board device
> trees added later in this series describe the MAC-side link as fixed,
> so they exercise the out-of-band path.
> 
> The XPCS header guards its s32g_serdes_pcs_create() declaration with
> IS_REACHABLE(CONFIG_PHY_S32G_SERDES) and provides a static-inline stub
> returning -ENODEV otherwise. IS_REACHABLE is false both when the SerDes
> driver is disabled and in the built-in-consumer / modular-provider case
> (DWMAC_S32=y with PHY_S32G_SERDES=m), so this driver links in all four
> combinations of the two symbols.
> 
> Linking is not sufficient on its own, though. In the DWMAC_S32=y with
> PHY_S32G_SERDES=m case the stub makes s32_gmac_pcs_init() return
> -ENODEV, and stmmac treats a failing plat->pcs_init() as fatal, so a
> built-in MAC would refuse to probe on any device tree describing
> pcs-handle. Add a Kconfig dependency forbidding that combination
> rather than letting it build into a MAC that cannot probe.
> 
> Tested on an S32G3-VNP-RDB3 board: GMAC0 links at 1G over SGMII and
> passes traffic, both in SerDes mode 1 (alongside two active PCIe root
> complexes) and in mode 3.
> 
> Signed-off-by: Jan Petrous (OSS) <jan.petrous@oss.nxp.com>
> ---
>  drivers/net/ethernet/stmicro/stmmac/Kconfig     |  6 ++
>  drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c | 74 ++++++++++++++++++++++++-
>  2 files changed, 78 insertions(+), 2 deletions(-)
> 
> diff --git a/drivers/net/ethernet/stmicro/stmmac/Kconfig b/drivers/net/ethernet/stmicro/stmmac/Kconfig
> index e3dd5adda5ac..1443ff4149b4 100644
> --- a/drivers/net/ethernet/stmicro/stmmac/Kconfig
> +++ b/drivers/net/ethernet/stmicro/stmmac/Kconfig
> @@ -192,6 +192,12 @@ config DWMAC_S32
>  	tristate "NXP S32G/S32R GMAC support"
>  	default ARCH_S32
>  	depends on OF && (ARCH_S32 || COMPILE_TEST)
> +	# SGMII needs the PCS from the SerDes driver. The header falls back
> +	# to a stub returning -ENODEV when that driver is not reachable, and
> +	# stmmac makes a failing plat->pcs_init() fatal, so a built-in MAC
> +	# with a modular SerDes would refuse to probe on any DT describing
> +	# pcs-handle. Forbid that combination instead.
> +	depends on PHY_S32G_SERDES || PHY_S32G_SERDES=n

select PHY_S32G_SERDES ? This is what other glues that require PCS do,
or there's a catch that prevents using it ?

>  	help
>  	  Support for ethernet controller on NXP S32CC SOCs.
>  
> diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c
> index 024d8e10e918..a3a56b21181a 100644
> --- a/drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c
> +++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-s32.c
> @@ -16,12 +16,14 @@
>  #include <linux/of_mdio.h>
>  #include <linux/of_address.h>
>  #include <linux/phy.h>
> +#include <linux/phy/nxp-s32g-serdes.h>
>  #include <linux/phylink.h>
>  #include <linux/platform_device.h>
>  #include <linux/regmap.h>
>  #include <linux/stmmac.h>
>  
>  #include "stmmac_platform.h"
> +#include "stmmac.h"
>  
>  #define GMAC_INTF_RATE_125M	125000000	/* 125MHz */
>  
> @@ -40,23 +42,89 @@ struct s32_priv_data {
>  	phy_interface_t *intf_mode;
>  	struct clk *tx_clk;
>  	struct clk *rx_clk;
> +	/* SGMII PCS provided by the SerDes subsystem (NULL if not SGMII) */
> +	struct phylink_pcs *pcs;

There's already a phylink_pcs pointer in priv->hw, you shouldn't have to
store your own here.

>  };
>  
>  static int s32_gmac_write_phy_intf_select(struct s32_priv_data *gmac)
>  {
> +	u32 intf_sel;
>  	int ret = 0;
>  
> +	switch (*gmac->intf_mode) {
> +	case PHY_INTERFACE_MODE_SGMII:
> +		intf_sel = S32_PHY_INTF_SEL_SGMII;
> +		break;
> +	case PHY_INTERFACE_MODE_RGMII:
> +	case PHY_INTERFACE_MODE_RGMII_ID:
> +	case PHY_INTERFACE_MODE_RGMII_RXID:
> +	case PHY_INTERFACE_MODE_RGMII_TXID:
> +		intf_sel = S32_PHY_INTF_SEL_RGMII;
> +		break;
> +	case PHY_INTERFACE_MODE_RMII:
> +		intf_sel = S32_PHY_INTF_SEL_RMII;
> +		break;
> +	case PHY_INTERFACE_MODE_MII:
> +		intf_sel = S32_PHY_INTF_SEL_MII;
> +		break;
> +	default:
> +		dev_err(gmac->dev, "Unsupported phy interface mode: %s\n",
> +			phy_modes(*gmac->intf_mode));
> +		return -EINVAL;
> +	}

We go from supporting RGMII only to RGMII + RMII + MII + SGMII.

Are RMII and MII actually supported on that platform ? if so, can you split
that into 2 commits : one for proper inft_sel support with RMII + MII added,
the another one for SGMII ? 

> +
>  	if (gmac->ctrl_sts)
> -		writel(S32_PHY_INTF_SEL_RGMII, gmac->ctrl_sts);
> +		writel(intf_sel, gmac->ctrl_sts);
>  	else
>  		ret = regmap_write(gmac->sts_regmap, gmac->sts_offset,
> -				   S32_PHY_INTF_SEL_RGMII);
> +				   intf_sel);
>  
>  	dev_dbg(gmac->dev, "PHY mode set to %s\n", phy_modes(*gmac->intf_mode));
>  
>  	return ret;
>  }
>  
> +static int s32_gmac_pcs_init(struct stmmac_priv *priv)
> +{
> +	struct s32_priv_data *gmac = priv->plat->bsp_priv;
> +	struct device_node *pcs_node;
> +
> +	/*
> +	 * pcs-handle points at the SerDes XPCS lane child node; its
> +	 * nxp,xpcs-instance property is what identifies the XPCS instance.
> +	 */
> +	pcs_node = of_parse_phandle(gmac->dev->of_node, "pcs-handle", 0);
> +	if (!pcs_node) {
> +		dev_dbg(gmac->dev, "no 'pcs-handle' property, SGMII PCS unavailable\n");
> +		return 0;
> +	}
> +
> +	gmac->pcs = s32g_serdes_pcs_create(gmac->dev, pcs_node);
> +	of_node_put(pcs_node);
> +
> +	if (IS_ERR(gmac->pcs)) {
> +		int err = PTR_ERR(gmac->pcs);
> +
> +		gmac->pcs = NULL;
> +		/* SerDes may not be probed yet - defer probe. */
> +		return err;
> +	}
> +
> +	dev_dbg(gmac->dev, "SGMII PCS created via SerDes XPCS\n");
> +	return 0;
> +}
> +
> +static struct phylink_pcs *s32_gmac_select_pcs(struct stmmac_priv *priv,
> +					       phy_interface_t interface)
> +{
> +	struct s32_priv_data *gmac = priv->plat->bsp_priv;
> +
> +	if (interface == PHY_INTERFACE_MODE_SGMII && gmac->pcs)
> +		return gmac->pcs;

No need for the && gmac->pcs here, if it's NULL we return NULL anyway so you
can return priv->hw->phylink_pcs

> +
> +	return NULL;
> +}
> +
>  static int s32_gmac_init(struct device *dev, void *priv)
>  {
>  	struct s32_priv_data *gmac = priv;
> @@ -204,6 +272,8 @@ static int s32_dwmac_probe(struct platform_device *pdev)
>  
>  	plat->init = s32_gmac_init;
>  	plat->exit = s32_gmac_exit;
> +	plat->pcs_init = s32_gmac_pcs_init;
> +	plat->select_pcs = s32_gmac_select_pcs;
>  
>  	plat->clk_tx_i = gmac->tx_clk;
>  	plat->set_clk_tx_rate = stmmac_set_clk_tx_rate;
> 

Thanks,

Maxime

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

* Re: [PATCH RFC v3 04/12] net: pcs: add NXP SerDes XPCS shared core
  2026-09-19  6:54 ` [PATCH RFC v3 04/12] net: pcs: add NXP SerDes XPCS shared core Jan Petrous via B4 Relay
@ 2026-09-19 15:31   ` Maxime Chevallier
  2026-09-19 16:31     ` Coia Prant
  0 siblings, 1 reply; 16+ messages in thread
From: Maxime Chevallier @ 2026-09-19 15:31 UTC (permalink / raw)
  To: jan.petrous, Ciprian Marian Costea, NXP S32 Linux Team,
	Vinod Koul, Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Coquelin, Alexandre Torgue,
	Chester Lin, Matthias Brugger, Ghennadi Procopciuc, Frank Li,
	Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam,
	Richard Cochran, Coia Prant
  Cc: linux-arm-kernel, linux-phy, netdev, devicetree, linux-kernel,
	linux-renesas-soc, imx, linux-pci, linux-stm32, Vincent Guittot

Hi Jan,

On 9/19/26 08:54, Jan Petrous via B4 Relay wrote:
> From: Clark Wang <xiaoning.wang@nxp.com>
> 
> The SerDes subsystems found on NXP S32G and i.MX SoCs integrate a
> Synopsys DesignWare Ethernet XPCS and an Ethernet PHY (PMA) with an
> identical register layout. Only the register transport differs: indirect
> memory mapped access on S32G, Clause 45 MDIO on i.MX.
> 
> Add a transport agnostic phylink PCS core which operates on regmaps
> provided by the platform glue. It implements the phylink PCS operations,
> the Clause 37 SGMII / 2500BASE-X / USXGMII / 10GBASE-R configuration and
> link state handling, soft reset and polling helpers, and the common
> feature sets. The platform specific register sequences, compatibility
> tables and quirks are supplied through a per revision descriptor by the
> platform drivers selecting this core.
> 
> The register sequences and the feature lists are derived from the
> Synopsys pcs-xpcs driver, so its copyright is retained. A separate driver
> is needed because struct dw_xpcs is built around an mdio_device and owns
> the transport, the reset and the identification of a standalone PCS,
> while here the PMA can be shared with a PCIe controller, the bring-up is
> sequenced across both XPCS instances by the SerDes PHY driver, and the
> transport is not always MDIO.

Is there a way to avoid code duplication by wrapping the accesses into
a mii_bus ?

This is what the Rockchip submission does : 

https://lore.kernel.org/r/20260919125119.2107089-8-coiaprant@gmail.com

Maxime


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

* Re: [PATCH RFC v3 04/12] net: pcs: add NXP SerDes XPCS shared core
  2026-09-19 15:31   ` Maxime Chevallier
@ 2026-09-19 16:31     ` Coia Prant
  0 siblings, 0 replies; 16+ messages in thread
From: Coia Prant @ 2026-09-19 16:31 UTC (permalink / raw)
  To: Maxime Chevallier
  Cc: jan.petrous, Ciprian Marian Costea, NXP S32 Linux Team,
	Vinod Koul, Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Ghennadi Procopciuc,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Geert Uytterhoeven, Magnus Damm, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Bjorn Helgaas, Bogdan Hamciuc,
	Ionut Vicovan, Andrew Lunn, Heiner Kallweit, Russell King,
	Clark Wang, Philipp Zabel, Maxime Coquelin, Alexandre Torgue,
	Chester Lin, Matthias Brugger, Ghennadi Procopciuc, Frank Li,
	Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam,
	Richard Cochran, linux-arm-kernel, linux-phy, netdev, devicetree,
	linux-kernel, linux-renesas-soc, imx, linux-pci, linux-stm32,
	Vincent Guittot

Maxime Chevallier <maxime.chevallier@bootlin.com> 于2026年9月19日周六 23:31写道:
>
> Hi Jan,
>
> On 9/19/26 08:54, Jan Petrous via B4 Relay wrote:
> > From: Clark Wang <xiaoning.wang@nxp.com>
> >
> > The SerDes subsystems found on NXP S32G and i.MX SoCs integrate a
> > Synopsys DesignWare Ethernet XPCS and an Ethernet PHY (PMA) with an
> > identical register layout. Only the register transport differs: indirect
> > memory mapped access on S32G, Clause 45 MDIO on i.MX.
> >
> > Add a transport agnostic phylink PCS core which operates on regmaps
> > provided by the platform glue. It implements the phylink PCS operations,
> > the Clause 37 SGMII / 2500BASE-X / USXGMII / 10GBASE-R configuration and
> > link state handling, soft reset and polling helpers, and the common
> > feature sets. The platform specific register sequences, compatibility
> > tables and quirks are supplied through a per revision descriptor by the
> > platform drivers selecting this core.
> >
> > The register sequences and the feature lists are derived from the
> > Synopsys pcs-xpcs driver, so its copyright is retained. A separate driver
> > is needed because struct dw_xpcs is built around an mdio_device and owns
> > the transport, the reset and the identification of a standalone PCS,
> > while here the PMA can be shared with a PCIe controller, the bring-up is
> > sequenced across both XPCS instances by the SerDes PHY driver, and the
> > transport is not always MDIO.
>
> Is there a way to avoid code duplication by wrapping the accesses into
> a mii_bus ?
>
> This is what the Rockchip submission does :
>
> https://lore.kernel.org/r/20260919125119.2107089-8-coiaprant@gmail.com
>
> Maxime

Hi Maxime,

The Rockchip approach (wrapping MMIO into a virtual mii_bus and reusing
pcs-xpcs.c) is indeed relevant here. Alex Elder's pcs-xpcs-regmap does
exactly that for the TC956x: it registers the XPCS through a regmap and
creates the virtual MDIO bus internally, with both direct and indirect
(page viewport) access modes.

https://lore.kernel.org/all/20260605010022.968612-4-elder@riscstar.com/

The indirect mode is very close to what S32G needs. For i.MX the C45
MDIO transport is different, but the bus wrapper could still be shared.

That said, I think there is a broader gap in pcs-xpcs.c: it hardcodes
behaviors that vary between integrations. RK3568 cannot use out-of-band
SGMII (fixed 125 MHz MAC clock), so its in-band caps should be
LINK_INBAND_ENABLE only. Today that means editing xpcs_inband_caps() or
duplicating it. NXP is hitting the same kind of problem with soft reset,
poll and EEE quirks.

A callback in dw_xpcs would let platform glues express those differences without
copying the register sequences. That's the same pattern stmmac uses with
plat->pcs_init/pcs_exit.

Once that callback mechanism lands, I'll send a separate patch to wire
it up in pcs-xpcs-rk.c. Since the Rockchip driver is not even merged
yet, there is nothing to change there for now.

Coia

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

end of thread, other threads:[~2026-09-19 16:31 UTC | newest]

Thread overview: 16+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-19  6:54 [PATCH RFC v3 00/12] Add support for the NXP S32G SerDes subsystem Jan Petrous via B4 Relay
2026-09-19  6:54 ` [PATCH RFC v3 01/12] dt-bindings: phy: Add " Jan Petrous via B4 Relay
2026-09-19  6:54 ` [PATCH RFC v3 02/12] dt-bindings: net: nxp,s32-dwmac: Document pcs-handle Jan Petrous via B4 Relay
2026-09-19  6:54 ` [PATCH RFC v3 03/12] dt-bindings: PCI: nxp,s32g-pcie: Fix SerDes PHY phandle in example Jan Petrous via B4 Relay
2026-09-19  6:54 ` [PATCH RFC v3 04/12] net: pcs: add NXP SerDes XPCS shared core Jan Petrous via B4 Relay
2026-09-19 15:31   ` Maxime Chevallier
2026-09-19 16:31     ` Coia Prant
2026-09-19  6:54 ` [PATCH RFC v3 05/12] net: pcs: Add NXP S32G XPCS driver Jan Petrous via B4 Relay
2026-09-19  6:54 ` [PATCH RFC v3 06/12] phy: freescale: s32g: Add SerDes subsystem PHY Jan Petrous via B4 Relay
2026-09-19  6:54 ` [PATCH RFC v3 07/12] net: stmmac: dwmac-s32: Add SGMII support Jan Petrous via B4 Relay
2026-09-19 12:04   ` Maxime Chevallier
2026-09-19  6:54 ` [PATCH RFC v3 08/12] MAINTAINERS: Add NXP S32G SerDes and SerDes xPCS core entries Jan Petrous via B4 Relay
2026-09-19  6:54 ` [PATCH RFC v3 09/12] arm64: dts: s32g: Add SCMI reset controller Jan Petrous via B4 Relay
2026-09-19  6:54 ` [PATCH RFC v3 10/12] arm64: dts: s32g: Add SerDes controller nodes Jan Petrous via B4 Relay
2026-09-19  6:54 ` [PATCH RFC v3 11/12] arm64: dts: s32g: Add PCIe " Jan Petrous via B4 Relay
2026-09-19  6:54 ` [PATCH RFC v3 12/12] arm64: dts: s32g: Add S32G3-RDB3 SerDes routing variants Jan Petrous via B4 Relay

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

all inboxes | Powered by JetHome®