Linux-ARM-Kernel Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600
@ 2026-10-14  7:20 Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 01/24] MAINTAINERS: Consolidate ARM/ZTE ZX SOC SUPPORT with Stefan Doesinger Navid Ghahremani
                   ` (24 more replies)
  0 siblings, 25 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel; +Cc: arnd, stefandoesinger, krzk, Navid Ghahremani

This series introduces mainline Linux support for the Sanechips (ZTE)
ZX279128S SoC and the ZTE ZXHN H3600 Wi-Fi 6 router.

The ZX279128S is a dual-core ARM Cortex-A9 network SoC with an ARM
PL310 / L2C-310 outer cache controller, dual PCIe interfaces, a 5-port
Gigabit Ethernet switch with hardware packet classification and flow
offload, SPI-NAND flash controller, and DWC3 USB 2.0 host.

Changes in v2:
- Dropped [PATCH RFC] tag.
- Decoupled controversial silicon bus/cache workaround (patches 01-03
  from RFC) from this platform series based on discussions with
  Arnd Bergmann and Stefan Doesinger. Kept downstream in OpenWrt while
  exploring a cleaner long-term MMIO solution (e.g. lib/logic_iomem.c).
- Consolidated maintainership under ARM/ZTE ZX SOC SUPPORT in
  MAINTAINERS with Stefan Doesinger and Navid Ghahremani as
  co-maintainers (Patch 01).
- Split CRM Device Tree schemas into separate dedicated topcrm and
  lspcrpm bindings with fixed clock lists (Patch 04).
- Added syscon-reboot child under topcrm with simple-mfd, dropping
  deprecated regmap properties (Patches 04, 06, 23).
- Converted reset bindings to sequential logical enum IDs (0, 1, 2...)
  in include/dt-bindings/reset/zte,zx279128s-reset.h with driver lookup
  table, ensuring stable DT ABI (Patches 05, 06).
- Synchronized clock gating and reset operations in CRM using shared
  spinlocks to protect shared registers (Patch 06).
- Used CLK_OF_DECLARE_DRIVER for topcrm to ensure simple-mfd populates
  child platform devices (Patch 06).
- Header hygiene: dropped <linux/mod_devicetable.h> from platform
  drivers in favor of <linux/platform_device.h> (Uwe Kleine-Koenig)
  (Patches 08, 13, 15, 16).
- Isolated PCIe SerDes/PLL into a Generic PHY driver under drivers/phy/
  with #phy-cells = <0>, completely removing raw syscon register poking
  from the DWC PCIe host controller driver (Patches 10, 11, 12, 13).
- Restored PCIe ASPM and CLKPM masking in controller setup, preventing
  MT7915 endpoint lockup when entering L0s/L1 power states (Patch 13).
- Replaced separate SPI driver with Stefan Doesinger unified spi-zx.c
  driver supporting both ZX29 and ZX279128S with dynamic CCF clock
  scaling via clk_get_rate(spifc->clk) (Patches 19, 20).
- Validated spi-zx.c in PIO mode on physical ZTE ZXHN H3600 hardware
  with 128 MiB HeYangTek SPI-NAND, UBI auto-attach, and UBIFS
  read/write.
- Fixed LED colors and shared PCIe reset GPIOs on H3600 (Patch 24).
- All DT schemas validated with dtschema and dt-doc-validate (0 warnings).
- All patches validated with scripts/checkpatch.pl (0 errors, 0 warnings).

Series breakdown:
- Patch 01: Consolidated MAINTAINERS entry.
- Patches 02-03: SoC platform integration, SMP CPU hotplug trampoline.
- Patches 04-06: Split CRM clock bindings, logical reset bindings,
  CCF/reset driver.
- Patches 07-09: GPIO controller bindings/driver, pinctrl-single
  bindings.
- Patches 10-13: Generic PCIe PHY bindings/driver, DWC PCIe controller.
- Patches 14-18: Networking: MDIO bus controller, ZX5201 Gigabit PHY,
  Gigabit Ethernet switch driver.
- Patches 19-20: Unified SPI flash controller bindings and spi-zx driver.
- Patches 21-22: Synopsys DWC3 USB platform glue binding and enablement.
- Patches 23-24: Device tree descriptions for the ZX279128S SoC and
  ZTE ZXHN H3600 board.

Hardware testing:
- Validated on physical ZTE ZXHN H3600 hardware running Linux 6.18.55.
- Dual-core SMP, CPU hotplug, and thermal sensors operational.
- Unified spi-zx driver verified with 128 MiB HeYangTek SPI-NAND,
  10 MTD partitions, UBI attachment, and UBIFS rootfs overlay.
- Dual-band MT7915 2.4/5 GHz Wi-Fi links initialized and stable.
- Gigabit Ethernet switch verified at line-rate 1 Gbps forwarding.
- USB 2.0 host verified.
---

Navid Ghahremani (24):
  MAINTAINERS: Consolidate ARM/ZTE ZX SOC SUPPORT with Stefan Doesinger
  dt-bindings: arm: Add ZTE ZX279128S platform descriptions
  ARM: zte: Add ZX279128S platform and CPU hotplug support
  dt-bindings: clock: Add ZTE ZX279128S CRM clock bindings
  dt-bindings: reset: Add ZTE ZX279128S reset controller bindings
  clk: zte: Add ZX279128S CRM clock and reset driver
  dt-bindings: gpio: Add ZTE ZX279128S GPIO controller
  gpio: Add ZTE ZX279128S GPIO driver
  dt-bindings: pinctrl: pinctrl-single: Add ZTE ZX279128S pin mux
  dt-bindings: phy: Add ZTE ZX279128S PCIe PHY binding
  phy: zte: Add ZTE ZX279128S PCIe PHY driver
  dt-bindings: PCI: Add ZTE ZX279128S host controller
  PCI: dwc: Add ZTE ZX279128S host controller driver
  dt-bindings: net: Add ZTE ZX279128S MDIO controller
  net: mdio: Add ZTE ZX279128S MDIO controller
  net: phy: Add Sanechips ZX5201 PHY support
  dt-bindings: net: Add ZTE ZX279128S Ethernet switch
  net: ethernet: zte: Add ZX279128S Ethernet switch driver
  dt-bindings: spi: Add ZTE ZX family SPI flash controller binding
  spi: zx: Add ZTE ZX family SPI flash controller driver
  dt-bindings: usb: Add ZTE ZX279128S DWC3 controller
  usb: dwc3: generic-plat: Add ZTE ZX279128S
  ARM: dts: zte: Add ZX279128S SoC description
  ARM: dts: zte: Add ZTE ZXHN H3600 board

 .../devicetree/bindings/arm/cpus.yaml         |    1 +
 .../devicetree/bindings/arm/zte.yaml          |    8 +-
 .../bindings/clock/zte,zx279128s-crm2.yaml    |   48 +
 .../bindings/clock/zte,zx279128s-lspcrpm.yaml |  101 ++
 .../bindings/clock/zte,zx279128s-topcrm.yaml  |   86 +
 .../bindings/gpio/zte,zx279128s-gpio.yaml     |   53 +
 .../bindings/net/zte,zx279128s-gmac.yaml      |  125 ++
 .../bindings/net/zte,zx279128s-mdio.yaml      |   65 +
 .../bindings/pci/zte,zx279128s-pcie.yaml      |  121 ++
 .../bindings/phy/zte,zx279128s-pcie-phy.yaml  |   40 +
 .../bindings/pinctrl/pinctrl-single.yaml      |    1 +
 .../bindings/spi/zte,zx279128s-spifc.yaml     |   72 +
 .../devicetree/bindings/sram/sram.yaml        |    1 +
 .../bindings/usb/zte,zx279128s-dwc3.yaml      |   76 +
 MAINTAINERS                                   |   27 +-
 arch/arm/boot/dts/zte/Makefile                |    2 +
 .../boot/dts/zte/zx279128s-zte-zxhn-h3600.dts |  364 ++++
 arch/arm/boot/dts/zte/zx279128s.dtsi          |  416 +++++
 arch/arm/mach-zte/Kconfig                     |   16 +
 arch/arm/mach-zte/Makefile                    |    4 +
 arch/arm/mach-zte/platsmp-zx279128s.c         |  173 ++
 arch/arm/mach-zte/zx279128s.c                 |   19 +
 drivers/clk/Kconfig                           |    1 +
 drivers/clk/Makefile                          |    1 +
 drivers/clk/zte/Kconfig                       |   14 +
 drivers/clk/zte/Makefile                      |    2 +
 drivers/clk/zte/clk-zx279128s.c               |  758 +++++++++
 drivers/gpio/Kconfig                          |   12 +
 drivers/gpio/Makefile                         |    1 +
 drivers/gpio/gpio-zx279128s.c                 |   79 +
 drivers/net/ethernet/zte/Kconfig              |   13 +
 drivers/net/ethernet/zte/Makefile             |    3 +
 drivers/net/ethernet/zte/zx279128s-eth.h      |  386 +++++
 drivers/net/ethernet/zte/zx279128s-main.c     | 1502 +++++++++++++++++
 drivers/net/ethernet/zte/zx279128s-ppe.c      |  805 +++++++++
 drivers/net/ethernet/zte/zx279128s-switch.c   |  368 ++++
 drivers/net/mdio/Kconfig                      |   11 +
 drivers/net/mdio/Makefile                     |    1 +
 drivers/net/mdio/mdio-zx279128s.c             |  157 ++
 drivers/net/phy/Kconfig                       |    8 +
 drivers/net/phy/Makefile                      |    1 +
 drivers/net/phy/sanechips.c                   |  152 ++
 drivers/pci/controller/dwc/Kconfig            |   13 +
 drivers/pci/controller/dwc/Makefile           |    1 +
 drivers/pci/controller/dwc/pcie-zx279128s.c   |  273 +++
 drivers/phy/Kconfig                           |   12 +
 drivers/phy/Makefile                          |    1 +
 drivers/phy/phy-zx279128s-pcie.c              |  156 ++
 drivers/spi/Kconfig                           |   15 +
 drivers/spi/Makefile                          |    1 +
 drivers/spi/spi-zx.c                          |  728 ++++++++
 drivers/usb/dwc3/Kconfig                      |    2 +-
 drivers/usb/dwc3/dwc3-generic-plat.c          |    1 +
 include/dt-bindings/clock/zte,zx279128s-crm.h |   51 +
 .../dt-bindings/reset/zte,zx279128s-reset.h   |   26 +
 55 files changed, 7370 insertions(+), 4 deletions(-)
 create mode 100644 Documentation/devicetree/bindings/clock/zte,zx279128s-crm2.yaml
 create mode 100644 Documentation/devicetree/bindings/clock/zte,zx279128s-lspcrpm.yaml
 create mode 100644 Documentation/devicetree/bindings/clock/zte,zx279128s-topcrm.yaml
 create mode 100644 Documentation/devicetree/bindings/gpio/zte,zx279128s-gpio.yaml
 create mode 100644 Documentation/devicetree/bindings/net/zte,zx279128s-gmac.yaml
 create mode 100644 Documentation/devicetree/bindings/net/zte,zx279128s-mdio.yaml
 create mode 100644 Documentation/devicetree/bindings/pci/zte,zx279128s-pcie.yaml
 create mode 100644 Documentation/devicetree/bindings/phy/zte,zx279128s-pcie-phy.yaml
 create mode 100644 Documentation/devicetree/bindings/spi/zte,zx279128s-spifc.yaml
 create mode 100644 Documentation/devicetree/bindings/usb/zte,zx279128s-dwc3.yaml
 create mode 100644 arch/arm/boot/dts/zte/zx279128s-zte-zxhn-h3600.dts
 create mode 100644 arch/arm/boot/dts/zte/zx279128s.dtsi
 create mode 100644 arch/arm/mach-zte/platsmp-zx279128s.c
 create mode 100644 arch/arm/mach-zte/zx279128s.c
 create mode 100644 drivers/clk/zte/Kconfig
 create mode 100644 drivers/clk/zte/Makefile
 create mode 100644 drivers/clk/zte/clk-zx279128s.c
 create mode 100644 drivers/gpio/gpio-zx279128s.c
 create mode 100644 drivers/net/ethernet/zte/zx279128s-eth.h
 create mode 100644 drivers/net/ethernet/zte/zx279128s-main.c
 create mode 100644 drivers/net/ethernet/zte/zx279128s-ppe.c
 create mode 100644 drivers/net/ethernet/zte/zx279128s-switch.c
 create mode 100644 drivers/net/mdio/mdio-zx279128s.c
 create mode 100644 drivers/net/phy/sanechips.c
 create mode 100644 drivers/pci/controller/dwc/pcie-zx279128s.c
 create mode 100644 drivers/phy/phy-zx279128s-pcie.c
 create mode 100644 drivers/spi/spi-zx.c
 create mode 100644 include/dt-bindings/clock/zte,zx279128s-crm.h
 create mode 100644 include/dt-bindings/reset/zte,zx279128s-reset.h

-- 
2.55.0



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

* [PATCH v2 01/24] MAINTAINERS: Consolidate ARM/ZTE ZX SOC SUPPORT with Stefan Doesinger
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14 20:11   ` Stefan Dösinger
  2026-10-14  7:20 ` [PATCH v2 02/24] dt-bindings: arm: Add ZTE ZX279128S platform descriptions Navid Ghahremani
                   ` (23 subsequent siblings)
  24 siblings, 1 reply; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, linux-kernel

Unify the ARM/ZTE ZX SoC entries under ARM/ZTE ZX SOC SUPPORT with
Stefan Doesinger and Navid Ghahremani as co-maintainers, covering
both ZX29 and ZX279128S platforms.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 MAINTAINERS | 27 +++++++++++++++++++++++++--
 1 file changed, 25 insertions(+), 2 deletions(-)

diff --git a/MAINTAINERS b/MAINTAINERS
index 4f72b4ac27..b806d1fc1c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3894,14 +3894,37 @@ F:	drivers/video/fbdev/vt8500lcdfb.*
 F:	drivers/video/fbdev/wm8505fb*
 F:	drivers/video/fbdev/wmt_ge_rops.*
 
-ARM/ZTE ZX29 SOC SUPPORT
+ARM/ZTE ZX SOC SUPPORT
 M:	Stefan Dösinger <stefandoesinger@gmail.com>
+M:	Navid Ghahremani <ghahramani.navid@gmail.com>
 L:	linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
-S:	Odd fixes
+S:	Maintained
 F:	Documentation/arch/arm/zte/
+F:	Documentation/devicetree/bindings/arm/zte/
 F:	Documentation/devicetree/bindings/arm/zte.yaml
+F:	Documentation/devicetree/bindings/clock/zte,zx*
+F:	Documentation/devicetree/bindings/gpio/zte,zx*
+F:	Documentation/devicetree/bindings/net/zte,zx*
+F:	Documentation/devicetree/bindings/pci/zte,zx*
+F:	Documentation/devicetree/bindings/phy/zte,zx*
+F:	Documentation/devicetree/bindings/reset/zte,zx*
+F:	Documentation/devicetree/bindings/spi/zte,zx*
+F:	Documentation/devicetree/bindings/usb/zte,zx*
 F:	arch/arm/boot/dts/zte/
 F:	arch/arm/mach-zte/
+F:	drivers/clk/zte/
+F:	drivers/gpio/gpio-zx*
+F:	drivers/net/ethernet/zte/
+F:	drivers/net/mdio/mdio-zx*
+F:	drivers/net/phy/sanechips.c
+F:	drivers/pci/controller/dwc/pcie-zx*
+F:	drivers/phy/phy-zx*
+F:	drivers/reset/reset-zx*
+F:	drivers/spi/spi-zx*
+F:	include/dt-bindings/clock/zte,zx*
+F:	include/dt-bindings/reset/zte,zx*
+
+
 
 ARM/ZYNQ ARCHITECTURE
 M:	Michal Simek <michal.simek@amd.com>
-- 
2.55.0



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

* [PATCH v2 02/24] dt-bindings: arm: Add ZTE ZX279128S platform descriptions
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 01/24] MAINTAINERS: Consolidate ARM/ZTE ZX SOC SUPPORT with Stefan Doesinger Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 03/24] ARM: zte: Add ZX279128S platform and CPU hotplug support Navid Ghahremani
                   ` (22 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Lorenzo Pieralisi, devicetree,
	linux-kernel

Describe the SoC board compatibility, SRAM used by the parked second CPU
and the SMP enable method.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 Documentation/devicetree/bindings/arm/cpus.yaml  | 1 +
 Documentation/devicetree/bindings/arm/zte.yaml   | 8 +++++++-
 Documentation/devicetree/bindings/sram/sram.yaml | 1 +
 3 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/Documentation/devicetree/bindings/arm/cpus.yaml b/Documentation/devicetree/bindings/arm/cpus.yaml
index 5be89c5840..25a141f80b 100644
--- a/Documentation/devicetree/bindings/arm/cpus.yaml
+++ b/Documentation/devicetree/bindings/arm/cpus.yaml
@@ -284,6 +284,7 @@ properties:
           - ste,dbx500-smp
           - ti,am3352
           - ti,am4372
+          - zte,zx279128s-smp
 
   cpu-release-addr:
     oneOf:
diff --git a/Documentation/devicetree/bindings/arm/zte.yaml b/Documentation/devicetree/bindings/arm/zte.yaml
index f028d2cec7..a1a3742219 100644
--- a/Documentation/devicetree/bindings/arm/zte.yaml
+++ b/Documentation/devicetree/bindings/arm/zte.yaml
@@ -11,7 +11,8 @@ maintainers:
 
 description: |
   ARM platforms using SoCs designed by ZTE. Currently this supports devices
-  based on the zx297520v3 SoC which is found in LTE routers.
+  based on the zx297520v3 SoC which is found in LTE routers, and on the
+  zx279128s SoC which is found in home gateways.
 
 properties:
   $nodename:
@@ -23,4 +24,9 @@ properties:
               - dlink,dwr932m
           - const: zte,zx297520v3
 
+      - items:
+          - enum:
+              - zte,zxhn-h3600
+          - const: zte,zx279128s
+
 additionalProperties: true
diff --git a/Documentation/devicetree/bindings/sram/sram.yaml b/Documentation/devicetree/bindings/sram/sram.yaml
index b83fc68319..d1389156c1 100644
--- a/Documentation/devicetree/bindings/sram/sram.yaml
+++ b/Documentation/devicetree/bindings/sram/sram.yaml
@@ -104,6 +104,7 @@ patternProperties:
             - samsung,exynos4210-sysram-ns
             - socionext,milbeaut-smp-sram
             - stericsson,u8500-esram
+            - zte,zx279128s-smp-sram
 
       reg:
         description:
-- 
2.55.0



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

* [PATCH v2 03/24] ARM: zte: Add ZX279128S platform and CPU hotplug support
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 01/24] MAINTAINERS: Consolidate ARM/ZTE ZX SOC SUPPORT with Stefan Doesinger Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 02/24] dt-bindings: arm: Add ZTE ZX279128S platform descriptions Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 04/24] dt-bindings: clock: Add ZTE ZX279128S CRM clock bindings Navid Ghahremani
                   ` (21 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Russell King,
	linux-kernel

Support the dual Cortex-A9 platform, second-CPU startup through
bootloader SRAM and CPU hotplug. Keep existing ZX297520V3 support
intact.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 arch/arm/mach-zte/Kconfig             |  16 +++
 arch/arm/mach-zte/Makefile            |   4 +
 arch/arm/mach-zte/platsmp-zx279128s.c | 173 ++++++++++++++++++++++++++
 arch/arm/mach-zte/zx279128s.c         |  19 +++
 4 files changed, 212 insertions(+)
 create mode 100644 arch/arm/mach-zte/platsmp-zx279128s.c
 create mode 100644 arch/arm/mach-zte/zx279128s.c

diff --git a/arch/arm/mach-zte/Kconfig b/arch/arm/mach-zte/Kconfig
index d3b404ca48..032079c958 100644
--- a/arch/arm/mach-zte/Kconfig
+++ b/arch/arm/mach-zte/Kconfig
@@ -26,4 +26,20 @@ config SOC_ZX297520V3
 	  Please read Documentation/arch/arm/zte/zx297520v3.rst on how to boot
 	  the kernel.
 
+config SOC_ZX279128S
+	bool "zx279128s SoC"
+	default y
+	select ARM_AMBA
+	select ARM_GIC
+	select ARM_GLOBAL_TIMER
+	select CACHE_L2X0
+	select CLKSRC_ARM_GLOBAL_TIMER_SCHED_CLOCK
+	select HAVE_ARM_SCU if SMP
+	select HAVE_ARM_TWD if SMP
+	help
+	  Support for the ZTE (Sanechips) zx279128s SoC, a dual-core
+	  Cortex-A9 with an L2C-310 cache controller, used in home gateways
+	  such as the ZTE ZXHN H3600. The second core is started through
+	  the on-chip SRAM, where the boot loader keeps it waiting.
+
 endif
diff --git a/arch/arm/mach-zte/Makefile b/arch/arm/mach-zte/Makefile
index 1bfe4fddd6..72f518632e 100644
--- a/arch/arm/mach-zte/Makefile
+++ b/arch/arm/mach-zte/Makefile
@@ -1,2 +1,6 @@
 # SPDX-License-Identifier: GPL-2.0-only
 obj-$(CONFIG_SOC_ZX297520V3) += zx297520v3.o
+obj-$(CONFIG_SOC_ZX279128S) += zx279128s.o
+ifdef CONFIG_SMP
+obj-$(CONFIG_SOC_ZX279128S) += platsmp-zx279128s.o
+endif
diff --git a/arch/arm/mach-zte/platsmp-zx279128s.c b/arch/arm/mach-zte/platsmp-zx279128s.c
new file mode 100644
index 0000000000..8285b8fb10
--- /dev/null
+++ b/arch/arm/mach-zte/platsmp-zx279128s.c
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ *
+ * The boot loader runs from on-chip SRAM.  Its reset code sends every
+ * core but CPU0 into a loop: wait for an event (WFE), then jump to the
+ * start of the SRAM, where the boot loader's reset vector leads back to
+ * the WFE.  To start the second core, replace that vector with a jump to
+ * secondary_startup and send an event.
+ *
+ * To take the second core offline again (CPU hotplug, and kexec, which needs
+ * it), the core puts the boot loader's vector back and returns to the boot
+ * loader's loop with the MMU and caches off. That loop is outside the
+ * kernel's memory, so the core survives the kernel being replaced, and the
+ * next start works the same way as the first.
+ */
+
+#include <linux/cacheflush.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/smp.h>
+
+#include <asm/cp15.h>
+#include <asm/idmap.h>
+#include <asm/proc-fns.h>
+#include <asm/smp_scu.h>
+#include <asm/tlbflush.h>
+
+/* ldr pc, [pc, #-4]: jump to the address stored in the next word */
+#define ZX279128S_JUMP_INSN	0xe51ff004
+
+static void __iomem *zx279128s_smp_sram;
+static phys_addr_t zx279128s_smp_sram_phys;
+/* the boot loader's vector, as found at boot */
+static u32 zx279128s_boot_vector[2];
+
+static int zx279128s_boot_secondary(unsigned int cpu, struct task_struct *idle)
+{
+	if (!zx279128s_smp_sram)
+		return -ENODEV;
+
+	/*
+	 * Store the address before the instruction: a core that wakes up
+	 * in between still runs the boot loader's vector and goes back to
+	 * sleep instead of jumping to a stale address.
+	 */
+	writel(__pa_symbol(secondary_startup), zx279128s_smp_sram + 4);
+	writel(ZX279128S_JUMP_INSN, zx279128s_smp_sram);
+
+	/* Complete both writes, then wake the core */
+	dsb_sev();
+
+	return 0;
+}
+
+static void __init zx279128s_smp_prepare_cpus(unsigned int max_cpus)
+{
+	struct device_node *np;
+	struct resource res;
+	void __iomem *scu;
+
+	np = of_find_compatible_node(NULL, NULL, "arm,cortex-a9-scu");
+	if (!np) {
+		pr_err("zx279128s: no SCU node\n");
+		return;
+	}
+	scu = of_iomap(np, 0);
+	of_node_put(np);
+	if (!scu) {
+		pr_err("zx279128s: cannot map the SCU\n");
+		return;
+	}
+	scu_enable(scu);
+	iounmap(scu);
+
+	np = of_find_compatible_node(NULL, NULL, "zte,zx279128s-smp-sram");
+	if (!np) {
+		pr_err("zx279128s: no SMP SRAM node\n");
+		return;
+	}
+	if (of_address_to_resource(np, 0, &res)) {
+		of_node_put(np);
+		pr_err("zx279128s: no SMP SRAM address\n");
+		return;
+	}
+	zx279128s_smp_sram = of_iomap(np, 0);
+	of_node_put(np);
+	if (!zx279128s_smp_sram) {
+		pr_err("zx279128s: cannot map the SMP SRAM\n");
+		return;
+	}
+	zx279128s_smp_sram_phys = res.start;
+	zx279128s_boot_vector[0] = readl(zx279128s_smp_sram);
+	zx279128s_boot_vector[1] = readl(zx279128s_smp_sram + 4);
+
+	/* zx279128s_cpu_die() reads these with its data cache off */
+	sync_cache_w(&zx279128s_smp_sram);
+	sync_cache_w(&zx279128s_smp_sram_phys);
+	sync_cache_w(&zx279128s_boot_vector);
+}
+
+#ifdef CONFIG_HOTPLUG_CPU
+typedef void (*phys_reset_t)(unsigned long addr, bool hvc);
+
+/*
+ * If the vector already holds a jump, an earlier kernel started the core
+ * and did not put the boot loader's vector back: the original is unknown,
+ * so the core cannot be parked safely.
+ */
+static bool zx279128s_cpu_can_disable(unsigned int cpu)
+{
+	return zx279128s_smp_sram &&
+	       zx279128s_boot_vector[0] != ZX279128S_JUMP_INSN;
+}
+
+static void zx279128s_cpu_die(unsigned int cpu)
+{
+	phys_reset_t phys_reset;
+
+	/* Take out a flat mapping, which keeps the kernel mappings too */
+	setup_mm_for_reboot();
+
+	/*
+	 * The boot loader's loop executes the vector, which
+	 * zx279128s_boot_secondary() changes with data writes. Stop caching
+	 * and predicting instructions, as at power-on, so that the parked
+	 * core sees the change.
+	 */
+	set_cr(get_cr() & ~(CR_I | CR_Z));
+	__flush_icache_all();
+	local_flush_bp_all();
+
+	/* Clean the caches and leave coherency with the other core */
+	v7_exit_coherency_flush(louis);
+
+	/*
+	 * Put the boot loader's vector back, instruction word last: the core
+	 * then wakes up into the boot loader's loop, and zx279128s_cpu_kill()
+	 * sees that it is about to leave the kernel.
+	 */
+	writel_relaxed(zx279128s_boot_vector[1], zx279128s_smp_sram + 4);
+	writel_relaxed(zx279128s_boot_vector[0], zx279128s_smp_sram);
+	dsb();
+
+	/* Turn the MMU off and continue at the boot loader's vector */
+	phys_reset = (phys_reset_t)virt_to_idmap(cpu_reset);
+	phys_reset(zx279128s_smp_sram_phys, false);
+}
+
+static int zx279128s_cpu_kill(unsigned int cpu)
+{
+	u32 val;
+
+	/* Wait until the dying core has put the vector back */
+	return !readl_poll_timeout(zx279128s_smp_sram, val,
+				   val == zx279128s_boot_vector[0], 1000,
+				   100 * USEC_PER_MSEC);
+}
+#endif
+
+static const struct smp_operations zx279128s_smp_ops __initconst = {
+	.smp_prepare_cpus	= zx279128s_smp_prepare_cpus,
+	.smp_boot_secondary	= zx279128s_boot_secondary,
+#ifdef CONFIG_HOTPLUG_CPU
+	.cpu_can_disable	= zx279128s_cpu_can_disable,
+	.cpu_die		= zx279128s_cpu_die,
+	.cpu_kill		= zx279128s_cpu_kill,
+#endif
+};
+
+CPU_METHOD_OF_DECLARE(zx279128s_smp, "zte,zx279128s-smp", &zx279128s_smp_ops);
diff --git a/arch/arm/mach-zte/zx279128s.c b/arch/arm/mach-zte/zx279128s.c
new file mode 100644
index 0000000000..064c76f8ad
--- /dev/null
+++ b/arch/arm/mach-zte/zx279128s.c
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#include <asm/mach/arch.h>
+#include <linux/init.h>
+
+static const char *const zx279128s_dt_compat[] __initconst = {
+	"zte,zx279128s",
+	NULL,
+};
+
+DT_MACHINE_START(ZX279128S, "ZTE zx279128s (Device Tree)")
+	.dt_compat	= zx279128s_dt_compat,
+	/* Keep the L2 cache setup of the boot loader and the device tree */
+	.l2c_aux_val	= 0,
+	.l2c_aux_mask	= ~0,
+MACHINE_END
-- 
2.55.0



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

* [PATCH v2 04/24] dt-bindings: clock: Add ZTE ZX279128S CRM clock bindings
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (2 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 03/24] ARM: zte: Add ZX279128S platform and CPU hotplug support Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 05/24] dt-bindings: reset: Add ZTE ZX279128S reset controller bindings Navid Ghahremani
                   ` (20 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Stephen Boyd,
	Brian Masney, Jerome Brunet, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, linux-clk, devicetree, linux-kernel

Add Device Tree binding schemas for the ZTE ZX279128S clock controllers:
topcrm, lspcrpm, and crm2. Define fixed clock lists and simple-mfd
for child nodes inside topcrm.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 .../bindings/clock/zte,zx279128s-crm2.yaml    |  48 +++++++++
 .../bindings/clock/zte,zx279128s-lspcrpm.yaml | 101 ++++++++++++++++++
 .../bindings/clock/zte,zx279128s-topcrm.yaml  |  86 +++++++++++++++
 include/dt-bindings/clock/zte,zx279128s-crm.h |  51 +++++++++
 4 files changed, 286 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/clock/zte,zx279128s-crm2.yaml
 create mode 100644 Documentation/devicetree/bindings/clock/zte,zx279128s-lspcrpm.yaml
 create mode 100644 Documentation/devicetree/bindings/clock/zte,zx279128s-topcrm.yaml
 create mode 100644 include/dt-bindings/clock/zte,zx279128s-crm.h

diff --git a/Documentation/devicetree/bindings/clock/zte,zx279128s-crm2.yaml b/Documentation/devicetree/bindings/clock/zte,zx279128s-crm2.yaml
new file mode 100644
index 0000000000..aa90ad15d1
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx279128s-crm2.yaml
@@ -0,0 +1,48 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx279128s-crm2.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE ZX279128S CRM2 clock and reset controller
+
+maintainers:
+  - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description:
+  Top CRM 2 (clock and reset module 2) provides clocks and resets for
+  PCIe controllers on the ZTE ZX279128S SoC.
+
+properties:
+  compatible:
+    oneOf:
+      - const: zte,zx279128s-crm2
+      - items:
+          - const: zte,zx279128s-crm2
+          - const: syscon
+
+  reg:
+    maxItems: 1
+
+  "#clock-cells":
+    const: 1
+
+  "#reset-cells":
+    const: 1
+
+required:
+  - compatible
+  - reg
+  - "#clock-cells"
+  - "#reset-cells"
+
+additionalProperties: false
+
+examples:
+  - |
+    clock-controller@94100000 {
+        compatible = "zte,zx279128s-crm2";
+        reg = <0x94100000 0x1000>;
+        #clock-cells = <1>;
+        #reset-cells = <1>;
+    };
diff --git a/Documentation/devicetree/bindings/clock/zte,zx279128s-lspcrpm.yaml b/Documentation/devicetree/bindings/clock/zte,zx279128s-lspcrpm.yaml
new file mode 100644
index 0000000000..3448829a69
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx279128s-lspcrpm.yaml
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx279128s-lspcrpm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE ZX279128S Low Speed Peripheral Clock Modules
+
+maintainers:
+  - Stefan Dösinger <stefandoesinger@gmail.com>
+  - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description: |
+  Each of the two groups of low speed peripherals (LSP) has a peripheral clock
+  module (LSP CRPM) of its own, fed by the top CRM.
+
+properties:
+  compatible:
+    enum:
+      - zte,zx279128s-lsp0crpm
+      - zte,zx279128s-lsp1crpm
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    minItems: 4
+    maxItems: 5
+
+  clock-names:
+    minItems: 4
+    maxItems: 5
+
+  '#clock-cells':
+    const: 1
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - clock-names
+  - '#clock-cells'
+
+allOf:
+  - if:
+      properties:
+        compatible:
+          contains:
+            const: zte,zx279128s-lsp0crpm
+    then:
+      properties:
+        clocks:
+          items:
+            - description: register (APB) clock of the group
+            - description: 25 MHz work clock
+            - description: 32.768 kHz work clock
+            - description: 100 MHz work clock
+        clock-names:
+          items:
+            - const: pclk
+            - const: wclk25m
+            - const: wclk32k
+            - const: wclk100m
+  - if:
+      properties:
+        compatible:
+          contains:
+            const: zte,zx279128s-lsp1crpm
+    then:
+      properties:
+        clocks:
+          items:
+            - description: register (APB) clock of the group
+            - description: AXI clock of the group
+            - description: 25 MHz work clock
+            - description: 49.152 MHz work clock
+            - description: 100 MHz work clock
+        clock-names:
+          items:
+            - const: pclk
+            - const: aclk
+            - const: wclk25m
+            - const: wclk49m
+            - const: wclk100m
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/zte,zx279128s-crm.h>
+
+    clock-controller@94400000 {
+        compatible = "zte,zx279128s-lsp0crpm";
+        reg = <0x94400000 0x1000>;
+        clocks = <&topcrm ZX279128S_TOP_LSP0_PCLK>,
+                 <&topcrm ZX279128S_TOP_LSP0_25M>,
+                 <&topcrm ZX279128S_TOP_LSP0_32K>,
+                 <&topcrm ZX279128S_TOP_LSP0_100M>;
+        clock-names = "pclk", "wclk25m", "wclk32k", "wclk100m";
+        #clock-cells = <1>;
+    };
diff --git a/Documentation/devicetree/bindings/clock/zte,zx279128s-topcrm.yaml b/Documentation/devicetree/bindings/clock/zte,zx279128s-topcrm.yaml
new file mode 100644
index 0000000000..9466661929
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx279128s-topcrm.yaml
@@ -0,0 +1,86 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx279128s-topcrm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE ZX279128S Top Clock and Reset Module
+
+maintainers:
+  - Stefan Dösinger <stefandoesinger@gmail.com>
+  - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description: |
+  The top clock and reset module (CRM) of the ZTE ZX279128S holds the PLLs,
+  bus clocks, peripheral clocks and the reset controller.
+
+properties:
+  compatible:
+    items:
+      - const: zte,zx279128s-topcrm
+      - const: syscon
+      - const: simple-mfd
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: 25 MHz crystal oscillator
+
+  clock-names:
+    items:
+      - const: osc
+
+  '#clock-cells':
+    const: 1
+
+  '#reset-cells':
+    const: 1
+
+  '#address-cells':
+    const: 1
+
+  '#size-cells':
+    const: 0
+
+  reboot:
+    type: object
+    $ref: /schemas/power/reset/syscon-reboot.yaml#
+    description: System reboot controller child node
+
+patternProperties:
+  "^phy@[01]$":
+    type: object
+    description: PCIe PHY child nodes
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - clock-names
+  - '#clock-cells'
+  - '#reset-cells'
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/zte,zx279128s-crm.h>
+    #include <dt-bindings/reset/zte,zx279128s-reset.h>
+
+    topcrm: clock-controller@94000000 {
+        compatible = "zte,zx279128s-topcrm", "syscon", "simple-mfd";
+        reg = <0x94000000 0x1000>;
+        clocks = <&osc>;
+        clock-names = "osc";
+        #clock-cells = <1>;
+        #reset-cells = <1>;
+
+        reboot {
+            compatible = "syscon-reboot";
+            offset = <0x44>;
+            mask = <0x1>;
+            value = <0x1>;
+        };
+    };
diff --git a/include/dt-bindings/clock/zte,zx279128s-crm.h b/include/dt-bindings/clock/zte,zx279128s-crm.h
new file mode 100644
index 0000000000..dadb2cd007
--- /dev/null
+++ b/include/dt-bindings/clock/zte,zx279128s-crm.h
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#ifndef _DT_BINDINGS_CLOCK_ZTE_ZX279128S_CRM_H
+#define _DT_BINDINGS_CLOCK_ZTE_ZX279128S_CRM_H
+
+/* Top CRM */
+#define ZX279128S_TOP_PLL_A9		0
+#define ZX279128S_TOP_PLL_LSP		1
+#define ZX279128S_TOP_CPU		2
+#define ZX279128S_TOP_A9_PERIPH		3
+#define ZX279128S_TOP_MATRIX_ACLK	4
+#define ZX279128S_TOP_MATRIX_HCLK	5
+#define ZX279128S_TOP_MATRIX_PCLK	6
+#define ZX279128S_TOP_LSP0_PCLK		7
+#define ZX279128S_TOP_LSP0_25M		8
+#define ZX279128S_TOP_LSP0_32K		9
+#define ZX279128S_TOP_LSP0_100M		10
+#define ZX279128S_TOP_LSP1_PCLK		11
+#define ZX279128S_TOP_LSP1_ACLK		12
+#define ZX279128S_TOP_LSP1_25M		13
+#define ZX279128S_TOP_LSP1_49M		14
+#define ZX279128S_TOP_LSP1_100M		15
+#define ZX279128S_TOP_IRAM_ACLK		16
+#define ZX279128S_TOP_IROM_ACLK		17
+#define ZX279128S_TOP_SYS_CTRL_PCLK	18
+#define ZX279128S_TOP_USB_ACLK		19
+#define ZX279128S_TOP_USB_SUSPEND	20
+#define ZX279128S_TOP_USB_REF		21
+
+
+/* Low speed peripherals 0 */
+#define ZX279128S_LSP0_UART0_WCLK	0
+#define ZX279128S_LSP0_UART0_PCLK	1
+#define ZX279128S_LSP0_UART1_WCLK	2
+#define ZX279128S_LSP0_UART1_PCLK	3
+#define ZX279128S_LSP0_SPI_WCLK		4
+#define ZX279128S_LSP0_SPI_PCLK		5
+#define ZX279128S_LSP0_GPIO_PCLK	6
+
+/* Low speed peripherals 1 */
+#define ZX279128S_LSP1_MDIO_WCLK	0
+#define ZX279128S_LSP1_MDIO_PCLK	1
+
+/* Top CRM 2 */
+#define ZX279128S_CRM2_PCIE0_CLK	0
+#define ZX279128S_CRM2_PCIE1_CLK	1
+
+#endif
-- 
2.55.0



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

* [PATCH v2 05/24] dt-bindings: reset: Add ZTE ZX279128S reset controller bindings
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (3 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 04/24] dt-bindings: clock: Add ZTE ZX279128S CRM clock bindings Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 06/24] clk: zte: Add ZX279128S CRM clock and reset driver Navid Ghahremani
                   ` (19 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Philipp Zabel,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, linux-kernel,
	devicetree

Define logical reset indices (0, 1, 2...) for the topcrm and crm2
reset controllers on ZTE ZX279128S SoCs.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 .../dt-bindings/reset/zte,zx279128s-reset.h   | 26 +++++++++++++++++++
 1 file changed, 26 insertions(+)
 create mode 100644 include/dt-bindings/reset/zte,zx279128s-reset.h

diff --git a/include/dt-bindings/reset/zte,zx279128s-reset.h b/include/dt-bindings/reset/zte,zx279128s-reset.h
new file mode 100644
index 0000000000..5b580bcda8
--- /dev/null
+++ b/include/dt-bindings/reset/zte,zx279128s-reset.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/*
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#ifndef _DT_BINDINGS_RESET_ZTE_ZX279128S_RESET_H
+#define _DT_BINDINGS_RESET_ZTE_ZX279128S_RESET_H
+
+/*
+ * Top CRM logical reset lines. Mapped by drivers/clk/zte/clk-zx279128s.c
+ * to hardware register offsets and bit shifts.
+ */
+#define ZX279128S_RESET_USB_B9		0
+#define ZX279128S_RESET_USB_B10		1
+#define ZX279128S_RESET_USB_B11		2
+#define ZX279128S_RESET_USB_B14		3
+#define ZX279128S_RESET_COUNT		4
+
+/*
+ * Top CRM 2 logical reset lines.
+ */
+#define ZX279128S_CRM2_RESET_PCIE0	0
+#define ZX279128S_CRM2_RESET_PCIE1	1
+#define ZX279128S_CRM2_RESET_COUNT	2
+
+#endif
-- 
2.55.0



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

* [PATCH v2 06/24] clk: zte: Add ZX279128S CRM clock and reset driver
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (4 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 05/24] dt-bindings: reset: Add ZTE ZX279128S reset controller bindings Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14 13:21   ` Philipp Zabel
  2026-10-14 19:39   ` Stefan Dösinger
  2026-10-14  7:20 ` [PATCH v2 07/24] dt-bindings: gpio: Add ZTE ZX279128S GPIO controller Navid Ghahremani
                   ` (18 subsequent siblings)
  24 siblings, 2 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Stephen Boyd,
	Brian Masney, Jerome Brunet, Philipp Zabel, linux-kernel,
	linux-clk

Support the top CRM and low-speed peripheral clock controllers of the
ZTE ZX279128S SoC. Implement custom reset controller ops mapping
logical reset IDs to internal hardware register offsets, and provide
CRM2 clock gating and reset controller support for PCIe.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 drivers/clk/Kconfig             |   1 +
 drivers/clk/Makefile            |   1 +
 drivers/clk/zte/Kconfig         |  14 +
 drivers/clk/zte/Makefile        |   2 +
 drivers/clk/zte/clk-zx279128s.c | 758 ++++++++++++++++++++++++++++++++
 5 files changed, 776 insertions(+)
 create mode 100644 drivers/clk/zte/Kconfig
 create mode 100644 drivers/clk/zte/Makefile
 create mode 100644 drivers/clk/zte/clk-zx279128s.c

diff --git a/drivers/clk/Kconfig b/drivers/clk/Kconfig
index f9592fd9ec..5200de26c0 100644
--- a/drivers/clk/Kconfig
+++ b/drivers/clk/Kconfig
@@ -547,6 +547,7 @@ source "drivers/clk/uniphier/Kconfig"
 source "drivers/clk/visconti/Kconfig"
 source "drivers/clk/x86/Kconfig"
 source "drivers/clk/xilinx/Kconfig"
+source "drivers/clk/zte/Kconfig"
 source "drivers/clk/zynqmp/Kconfig"
 
 # Kunit test cases
diff --git a/drivers/clk/Makefile b/drivers/clk/Makefile
index b18af485d7..07393019c3 100644
--- a/drivers/clk/Makefile
+++ b/drivers/clk/Makefile
@@ -181,5 +181,6 @@ ifeq ($(CONFIG_COMMON_CLK), y)
 obj-$(CONFIG_X86)			+= x86/
 endif
 obj-y					+= xilinx/
+obj-y					+= zte/
 obj-$(CONFIG_ARCH_ZYNQ)			+= zynq/
 obj-$(CONFIG_COMMON_CLK_ZYNQMP)         += zynqmp/
diff --git a/drivers/clk/zte/Kconfig b/drivers/clk/zte/Kconfig
new file mode 100644
index 0000000000..92fd27ea42
--- /dev/null
+++ b/drivers/clk/zte/Kconfig
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config CLK_ZX279128S
+	bool "ZTE zx279128s clock driver"
+	depends on SOC_ZX279128S || COMPILE_TEST
+	depends on OF
+	default SOC_ZX279128S
+	select RESET_CONTROLLER
+	select RESET_SIMPLE
+	help
+	  This driver supports the clocks of the ZTE zx279128s SoC: the
+	  top clock and reset module (CRM) with the PLLs, the CPU, bus and
+	  peripheral group clocks, and the CRPMs of the two groups of low
+	  speed peripherals (UARTs, SPI flash controller, GPIO, MDIO).
+	  Say Y if you build a kernel for a board with this SoC.
diff --git a/drivers/clk/zte/Makefile b/drivers/clk/zte/Makefile
new file mode 100644
index 0000000000..a55ffbfaf1
--- /dev/null
+++ b/drivers/clk/zte/Makefile
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_CLK_ZX279128S)	+= clk-zx279128s.o
diff --git a/drivers/clk/zte/clk-zx279128s.c b/drivers/clk/zte/clk-zx279128s.c
new file mode 100644
index 0000000000..892e0c22e5
--- /dev/null
+++ b/drivers/clk/zte/clk-zx279128s.c
@@ -0,0 +1,758 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ZTE zx279128s clocks: the top CRM and the CRPMs of the two groups of low
+ * speed peripherals
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ *
+ * The boot loader sets up the PLLs and the CPU and bus clocks, and the driver
+ * leaves them as they are: it reads them once and registers their rates. The
+ * gates, and the muxes and dividers of the peripherals, are under its
+ * control.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/io.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/reset-controller.h>
+
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+
+#include <dt-bindings/clock/zte,zx279128s-crm.h>
+#include <dt-bindings/reset/zte,zx279128s-reset.h>
+
+/* Top CRM */
+#define TOP_CLK_SEL		0x0c
+#define  TOP_MATRIX_SEL_SHIFT	15
+#define  TOP_CPU_SEL_SHIFT	13
+#define TOP_CLK_EN		0x14
+#define TOP_PLL_A9		0x18
+#define TOP_PLL_AUDIO		0x28
+#define TOP_USB_EN		0x48
+
+#define TOP_NUM_CLKS		(ZX279128S_TOP_USB_REF + 1)
+
+struct zx_reset_line {
+	u16 reg;
+	u8 bit;
+};
+
+static const struct zx_reset_line zx279128s_top_resets[] = {
+	[ZX279128S_RESET_USB_B9]  = { 0x4c, 9 },
+	[ZX279128S_RESET_USB_B10] = { 0x4c, 10 },
+	[ZX279128S_RESET_USB_B11] = { 0x4c, 11 },
+	[ZX279128S_RESET_USB_B14] = { 0x4c, 14 },
+};
+
+static DEFINE_SPINLOCK(zx_top_lock);
+
+struct zx_reset_data {
+	struct reset_controller_dev rcdev;
+	void __iomem *base;
+	spinlock_t *lock;
+};
+
+static inline struct zx_reset_data *to_zx_reset_data(struct reset_controller_dev *rcdev)
+{
+	return container_of(rcdev, struct zx_reset_data, rcdev);
+}
+
+static int zx_reset_assert(struct reset_controller_dev *rcdev, unsigned long id)
+{
+	struct zx_reset_data *data = to_zx_reset_data(rcdev);
+	const struct zx_reset_line *line;
+	unsigned long flags;
+	u32 val;
+
+	if (id >= ARRAY_SIZE(zx279128s_top_resets))
+		return -EINVAL;
+
+	line = &zx279128s_top_resets[id];
+	spin_lock_irqsave(data->lock, flags);
+	val = readl(data->base + line->reg);
+	val &= ~BIT(line->bit);
+	writel(val, data->base + line->reg);
+	spin_unlock_irqrestore(data->lock, flags);
+	return 0;
+}
+
+static int zx_reset_deassert(struct reset_controller_dev *rcdev, unsigned long id)
+{
+	struct zx_reset_data *data = to_zx_reset_data(rcdev);
+	const struct zx_reset_line *line;
+	unsigned long flags;
+	u32 val;
+
+	if (id >= ARRAY_SIZE(zx279128s_top_resets))
+		return -EINVAL;
+
+	line = &zx279128s_top_resets[id];
+	spin_lock_irqsave(data->lock, flags);
+	val = readl(data->base + line->reg);
+	val |= BIT(line->bit);
+	writel(val, data->base + line->reg);
+	spin_unlock_irqrestore(data->lock, flags);
+	return 0;
+}
+
+static int zx_reset_status(struct reset_controller_dev *rcdev, unsigned long id)
+{
+	struct zx_reset_data *data = to_zx_reset_data(rcdev);
+	const struct zx_reset_line *line;
+	u32 val;
+
+	if (id >= ARRAY_SIZE(zx279128s_top_resets))
+		return -EINVAL;
+
+	line = &zx279128s_top_resets[id];
+	val = readl(data->base + line->reg);
+	return !(val & BIT(line->bit));
+}
+
+static const struct reset_control_ops zx_reset_ops = {
+	.assert = zx_reset_assert,
+	.deassert = zx_reset_deassert,
+	.status = zx_reset_status,
+};
+
+/*
+ * Reset bits are cleared to hold the reset. The registers also hold other
+ * controls, but no other driver writes the reset registers.
+ */
+static int __init zx_top_reset_init(struct device_node *np,
+				    void __iomem *base)
+{
+	struct zx_reset_data *rst;
+	int ret;
+
+	rst = kzalloc_obj(*rst, GFP_KERNEL);
+	if (!rst)
+		return -ENOMEM;
+
+	rst->lock = &zx_top_lock;
+	rst->base = base;
+	rst->rcdev.ops = &zx_reset_ops;
+	rst->rcdev.owner = THIS_MODULE;
+	rst->rcdev.nr_resets = ARRAY_SIZE(zx279128s_top_resets);
+	rst->rcdev.of_node = np;
+
+	ret = reset_controller_register(&rst->rcdev);
+	if (ret)
+		kfree(rst);
+
+	return ret;
+}
+
+/*
+ * A PLL has two registers. The output is
+ * osc * (fbdiv + frac / 2^24) / pd1 / pd2.
+ */
+#define PLL_FBDIV		GENMASK(17, 6)
+#define PLL_PD1			GENMASK(5, 3)
+#define PLL_PD2			GENMASK(2, 0)
+#define PLL_FRAC		GENMASK(23, 0)	/* second register */
+#define PLL_FRAC_BITS		24
+
+/* The LSP CRPMs: one register per peripheral */
+#define LSP_PCLK_EN		0	/* bit of the register clock gate */
+#define LSP_WCLK_EN		1	/* bit of the work clock gate */
+#define LSP_WCLK_SEL_SHIFT	9
+#define LSP_WCLK_DIV_SHIFT	11
+
+/*
+ * Register a PLL as a fixed factor of the crystal, from the setting the boot
+ * loader left in it.
+ */
+static struct clk_hw * __init zx_top_pll(struct device_node *np,
+					 const char *name, void __iomem *reg)
+{
+	u32 cfg0 = readl(reg), cfg1 = readl(reg + 4);
+	unsigned int pd = FIELD_GET(PLL_PD1, cfg0) * FIELD_GET(PLL_PD2, cfg0);
+	u64 mult = ((u64)FIELD_GET(PLL_FBDIV, cfg0) << PLL_FRAC_BITS) |
+		   FIELD_GET(PLL_FRAC, cfg1);
+	u64 div = (u64)pd << PLL_FRAC_BITS;
+	unsigned int shift;
+
+	if (!mult || !pd)
+		return ERR_PTR(-EINVAL);
+
+	/* Fit both into 32 bits: drop trailing zeroes, then fraction bits */
+	shift = min(__ffs64(mult), PLL_FRAC_BITS);
+	mult >>= shift;
+	div >>= shift;
+	while (mult > UINT_MAX) {
+		mult >>= 1;
+		div >>= 1;
+	}
+
+	return clk_hw_register_fixed_factor_fwname(NULL, np, name, "osc", 0,
+						   mult, div);
+}
+
+static struct clk_hw * __init zx_top_gate(void __iomem *reg,
+					  const char *name,
+					  const struct clk_hw *parent, u8 bit,
+					  unsigned long flags)
+{
+	return clk_hw_register_gate_parent_hw(NULL, name, parent, flags, reg,
+					      bit, 0, &zx_top_lock);
+}
+
+
+
+static void __init zx279128s_topcrm_init(struct device_node *np)
+{
+	const struct clk_hw *cpu_parents[4], *matrix_parents[4];
+	struct clk_hw *osc, *lsp, *lsp_500m, *lsp_250m, *lsp_125m, *lsp_100m;
+	struct clk_hw *audio, *audio_32k, *matrix, *hclk, *pclk, *m200, *m20;
+	struct clk_hw_onecell_data *data;
+	struct clk_hw **hws;
+	void __iomem *base, *en;
+	int i;
+
+	base = of_iomap(np, 0);
+	if (!base) {
+		pr_err("%pOF: no registers\n", np);
+		return;
+	}
+
+	data = kzalloc(struct_size(data, hws, TOP_NUM_CLKS), GFP_KERNEL);
+	if (!data)
+		return;
+	data->num = TOP_NUM_CLKS;
+	hws = data->hws;
+
+	hws[ZX279128S_TOP_PLL_A9] = zx_top_pll(np, "pll_a9", base + TOP_PLL_A9);
+	if (IS_ERR(hws[ZX279128S_TOP_PLL_A9]))
+		goto err;
+	osc = clk_hw_get_parent(hws[ZX279128S_TOP_PLL_A9]);
+	if (!osc)
+		goto err;
+
+	/*
+	 * The formula above gives 200 MHz for the LSP PLL, but the vendor
+	 * kernel has it at 1 GHz, with fixed taps for the bus (250 MHz) and
+	 * the peripherals (100 MHz). Until that is measured, it is registered
+	 * with the vendor's rate.
+	 */
+	lsp = clk_hw_register_fixed_factor_parent_hw(NULL, "pll_lsp", osc, 0,
+						     40, 1);
+	if (IS_ERR(lsp))
+		goto err;
+	hws[ZX279128S_TOP_PLL_LSP] = lsp;
+	lsp_500m = clk_hw_register_fixed_factor_parent_hw(NULL, "lsp_500m", lsp,
+							  0, 1, 2);
+	lsp_250m = clk_hw_register_fixed_factor_parent_hw(NULL, "lsp_250m", lsp,
+							  0, 1, 4);
+	lsp_125m = clk_hw_register_fixed_factor_parent_hw(NULL, "lsp_125m", lsp,
+							  0, 1, 8);
+	lsp_100m = clk_hw_register_fixed_factor_parent_hw(NULL, "lsp_100m", lsp,
+							  0, 1, 10);
+	if (IS_ERR(lsp_500m) || IS_ERR(lsp_250m) || IS_ERR(lsp_125m) ||
+	    IS_ERR(lsp_100m))
+		goto err;
+
+	/* 49.152 MHz, and the 32.768 kHz derived from it */
+	audio = zx_top_pll(np, "pll_audio", base + TOP_PLL_AUDIO);
+	if (IS_ERR(audio))
+		goto err;
+	audio_32k = clk_hw_register_fixed_factor_parent_hw(NULL, "audio_32k",
+							   audio, 0, 1, 1500);
+	if (IS_ERR(audio_32k))
+		goto err;
+
+	cpu_parents[0] = osc;
+	cpu_parents[1] = lsp_250m;
+	cpu_parents[2] = lsp_500m;
+	cpu_parents[3] = hws[ZX279128S_TOP_PLL_A9];
+	hws[ZX279128S_TOP_CPU] =
+		clk_hw_register_mux_hws(NULL, "cpu", cpu_parents, 4, 0,
+					base + TOP_CLK_SEL, TOP_CPU_SEL_SHIFT,
+					2, CLK_MUX_READ_ONLY, &zx_top_lock);
+	if (IS_ERR(hws[ZX279128S_TOP_CPU]))
+		goto err;
+	hws[ZX279128S_TOP_A9_PERIPH] =
+		clk_hw_register_fixed_factor_parent_hw(NULL, "a9_periph",
+						       hws[ZX279128S_TOP_CPU],
+						       0, 1, 2);
+
+	/*
+	 * A 200 MHz clock of unknown source; the vendor kernel declares it at
+	 * this rate. It feeds the bus select and, divided, the USB reference.
+	 */
+	m200 = clk_hw_register_fixed_rate(NULL, "clk_200m", NULL, 0,
+					  200000000);
+	if (IS_ERR(m200))
+		goto err;
+	m20 = clk_hw_register_fixed_factor_parent_hw(NULL, "clk_20m", m200, 0,
+						     1, 10);
+	if (IS_ERR(m20))
+		goto err;
+
+	matrix_parents[0] = osc;
+	matrix_parents[1] = m200;
+	matrix_parents[2] = lsp_250m;
+	matrix_parents[3] = lsp_125m;
+	matrix = clk_hw_register_mux_hws(NULL, "matrix_aclk", matrix_parents,
+					 4, 0, base + TOP_CLK_SEL,
+					 TOP_MATRIX_SEL_SHIFT, 2,
+					 CLK_MUX_READ_ONLY, &zx_top_lock);
+	if (IS_ERR(matrix))
+		goto err;
+	hws[ZX279128S_TOP_MATRIX_ACLK] = matrix;
+	hclk = clk_hw_register_fixed_factor_parent_hw(NULL, "matrix_hclk",
+						      matrix, 0, 1, 2);
+	hws[ZX279128S_TOP_MATRIX_HCLK] = hclk;
+	pclk = clk_hw_register_fixed_factor_parent_hw(NULL, "matrix_pclk",
+						      matrix, 0, 1, 2);
+	if (IS_ERR(pclk))
+		goto err;
+	hws[ZX279128S_TOP_MATRIX_PCLK] = pclk;
+
+	en = base + TOP_CLK_EN;
+	hws[ZX279128S_TOP_LSP0_100M] = zx_top_gate(en, "lsp0_100m", lsp_100m,
+						   13, 0);
+	hws[ZX279128S_TOP_LSP0_32K] = zx_top_gate(en, "lsp0_32k", audio_32k,
+						  12, 0);
+	hws[ZX279128S_TOP_LSP0_PCLK] = zx_top_gate(en, "lsp0_pclk", pclk, 11, 0);
+	hws[ZX279128S_TOP_LSP0_25M] = zx_top_gate(en, "lsp0_25m", osc, 9, 0);
+	hws[ZX279128S_TOP_LSP1_ACLK] = zx_top_gate(en, "lsp1_aclk", matrix, 8, 0);
+	hws[ZX279128S_TOP_LSP1_100M] = zx_top_gate(en, "lsp1_100m", lsp_100m,
+						   7, 0);
+	hws[ZX279128S_TOP_LSP1_49M] = zx_top_gate(en, "lsp1_49m", audio, 6, 0);
+	hws[ZX279128S_TOP_LSP1_PCLK] = zx_top_gate(en, "lsp1_pclk", pclk, 5, 0);
+	hws[ZX279128S_TOP_LSP1_25M] = zx_top_gate(en, "lsp1_25m", osc, 4, 0);
+	/* the parked second CPU waits in the IRAM */
+	hws[ZX279128S_TOP_IRAM_ACLK] = zx_top_gate(en, "iram_aclk", matrix, 2,
+						   CLK_IS_CRITICAL);
+	hws[ZX279128S_TOP_IROM_ACLK] = zx_top_gate(en, "irom_aclk", matrix, 1,
+						   CLK_IS_CRITICAL);
+	hws[ZX279128S_TOP_SYS_CTRL_PCLK] = zx_top_gate(en, "sys_ctrl_pclk", pclk,
+						       0, CLK_IS_CRITICAL);
+
+	/* The USB 3.0 controller's bus, suspend and reference clocks */
+	en = base + TOP_USB_EN;
+	hws[ZX279128S_TOP_USB_ACLK] = zx_top_gate(en, "usb_aclk", matrix, 21, 0);
+	hws[ZX279128S_TOP_USB_SUSPEND] = zx_top_gate(en, "usb_suspend",
+						     audio_32k, 20, 0);
+	hws[ZX279128S_TOP_USB_REF] = zx_top_gate(en, "usb_ref", m20, 19, 0);
+
+	for (i = 0; i < TOP_NUM_CLKS; i++)
+		if (IS_ERR_OR_NULL(hws[i]))
+			goto err;
+
+	if (of_clk_add_hw_provider(np, of_clk_hw_onecell_get, data))
+		goto err;
+
+	if (zx_top_reset_init(np, base))
+		pr_err("%pOF: failed to register the resets\n", np);
+	return;
+
+err:
+	/* Without its timer and bus clocks the system won't boot anyway */
+	pr_err("%pOF: failed to register the clocks\n", np);
+}
+
+CLK_OF_DECLARE_DRIVER(zx279128s_topcrm, "zte,zx279128s-topcrm",
+	       zx279128s_topcrm_init);
+
+/*
+ * A peripheral of an LSP group: its work clock, optionally behind a mux of
+ * two inputs or a divider, and its register clock.
+ */
+struct zx_lsp_periph {
+	const char *name;
+	unsigned int reg;
+	int wclk;			/* index of the work clock, or -1 */
+	int pclk;			/* index of the register clock */
+	const char *parents[2];		/* the mux inputs, or one parent */
+	u8 div_width;			/* 0: no divider */
+};
+
+struct zx_lsp_data {
+	const struct zx_lsp_periph *periphs;
+	unsigned int num_periphs;
+	unsigned int num_clks;
+	bool aclk;			/* the group has an AXI clock */
+};
+
+/* The work clock: an optional mux or divider, then the gate */
+static struct clk_hw *zx_lsp_wclk(struct device *dev, void __iomem *reg,
+				  spinlock_t *lock,
+				  const struct zx_lsp_periph *p)
+{
+	struct clk_parent_data pd[2] = {
+		{ .fw_name = p->parents[0] },
+		{ .fw_name = p->parents[1] },
+	};
+	struct clk_hw *src = NULL;
+	const char *name;
+
+	if (p->parents[1]) {
+		name = devm_kasprintf(dev, GFP_KERNEL, "%s_wsel", p->name);
+		if (!name)
+			return ERR_PTR(-ENOMEM);
+		src = devm_clk_hw_register_mux_parent_data_table(dev, name, pd,
+								 2, 0, reg,
+								 LSP_WCLK_SEL_SHIFT,
+								 1, 0, NULL,
+								 lock);
+	} else if (p->div_width) {
+		name = devm_kasprintf(dev, GFP_KERNEL, "%s_wdiv", p->name);
+		if (!name)
+			return ERR_PTR(-ENOMEM);
+		src = __devm_clk_hw_register_divider(dev, NULL, name, NULL,
+						     NULL, pd, 0, reg,
+						     LSP_WCLK_DIV_SHIFT,
+						     p->div_width, 0, NULL,
+						     lock);
+	}
+	if (IS_ERR(src))
+		return src;
+
+	name = devm_kasprintf(dev, GFP_KERNEL, "%s_wclk", p->name);
+	if (!name)
+		return ERR_PTR(-ENOMEM);
+	if (src)
+		return devm_clk_hw_register_gate_parent_hw(dev, name, src,
+							   CLK_SET_RATE_PARENT,
+							   reg, LSP_WCLK_EN, 0,
+							   lock);
+	return devm_clk_hw_register_gate_parent_data(dev, name, pd, 0, reg,
+						     LSP_WCLK_EN, 0, lock);
+}
+
+/* The register clock */
+static struct clk_hw *zx_lsp_pclk(struct device *dev, void __iomem *reg,
+				  spinlock_t *lock,
+				  const struct zx_lsp_periph *p)
+{
+	struct clk_parent_data pclk = { .fw_name = "pclk" };
+	const char *name;
+
+	name = devm_kasprintf(dev, GFP_KERNEL, "%s_pclk", p->name);
+	if (!name)
+		return ERR_PTR(-ENOMEM);
+	return devm_clk_hw_register_gate_parent_data(dev, name, &pclk, 0, reg,
+						     LSP_PCLK_EN, 0, lock);
+}
+
+static int zx279128s_lsp_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	const struct zx_lsp_data *match = device_get_match_data(dev);
+	struct clk_hw_onecell_data *data;
+	void __iomem *base;
+	spinlock_t *lock;	/* serializes the group's clock registers */
+	struct clk *clk;
+	unsigned int i;
+
+	base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(base))
+		return PTR_ERR(base);
+
+	/* The group's own registers need these */
+	clk = devm_clk_get_enabled(dev, "pclk");
+	if (IS_ERR(clk))
+		return dev_err_probe(dev, PTR_ERR(clk), "no register clock\n");
+	if (match->aclk) {
+		clk = devm_clk_get_enabled(dev, "aclk");
+		if (IS_ERR(clk))
+			return dev_err_probe(dev, PTR_ERR(clk),
+					     "no AXI clock\n");
+	}
+
+	lock = devm_kzalloc(dev, sizeof(*lock), GFP_KERNEL);
+	data = devm_kzalloc(dev, struct_size(data, hws, match->num_clks),
+			    GFP_KERNEL);
+	if (!lock || !data)
+		return -ENOMEM;
+	spin_lock_init(lock);
+	data->num = match->num_clks;
+
+	for (i = 0; i < match->num_periphs; i++) {
+		const struct zx_lsp_periph *p = &match->periphs[i];
+		void __iomem *reg = base + p->reg;
+		struct clk_hw *hw;
+
+		if (p->wclk >= 0) {
+			hw = zx_lsp_wclk(dev, reg, lock, p);
+			if (IS_ERR(hw))
+				return dev_err_probe(dev, PTR_ERR(hw),
+						     "failed to register %s\n",
+						     p->name);
+			data->hws[p->wclk] = hw;
+		}
+
+		hw = zx_lsp_pclk(dev, reg, lock, p);
+		if (IS_ERR(hw))
+			return dev_err_probe(dev, PTR_ERR(hw),
+					     "failed to register %s\n", p->name);
+		data->hws[p->pclk] = hw;
+	}
+
+	return devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, data);
+}
+
+static const struct zx_lsp_periph zx_lsp0_periphs[] = {
+	{ "uart0", 0x10, ZX279128S_LSP0_UART0_WCLK, ZX279128S_LSP0_UART0_PCLK,
+	  { "wclk25m", "wclk100m" } },
+	{ "uart1", 0x14, ZX279128S_LSP0_UART1_WCLK, ZX279128S_LSP0_UART1_PCLK,
+	  { "wclk25m", "wclk100m" } },
+	{ "spi", 0x18, ZX279128S_LSP0_SPI_WCLK, ZX279128S_LSP0_SPI_PCLK,
+	  { "wclk100m" }, 6 },
+	{ "gpio", 0x1c, -1, ZX279128S_LSP0_GPIO_PCLK },
+};
+
+static const struct zx_lsp_data zx_lsp0_data = {
+	.periphs = zx_lsp0_periphs,
+	.num_periphs = ARRAY_SIZE(zx_lsp0_periphs),
+	.num_clks = ZX279128S_LSP0_GPIO_PCLK + 1,
+};
+
+static const struct zx_lsp_periph zx_lsp1_periphs[] = {
+	{ "mdio", 0x04, ZX279128S_LSP1_MDIO_WCLK, ZX279128S_LSP1_MDIO_PCLK,
+	  { "wclk100m" }, 7 },
+};
+
+static const struct zx_lsp_data zx_lsp1_data = {
+	.periphs = zx_lsp1_periphs,
+	.num_periphs = ARRAY_SIZE(zx_lsp1_periphs),
+	.num_clks = ZX279128S_LSP1_MDIO_PCLK + 1,
+	.aclk = true,
+};
+
+static const struct of_device_id zx279128s_lsp_of_match[] = {
+	{ .compatible = "zte,zx279128s-lsp0crpm", .data = &zx_lsp0_data },
+	{ .compatible = "zte,zx279128s-lsp1crpm", .data = &zx_lsp1_data },
+	{ }
+};
+
+static struct platform_driver zx279128s_lsp_driver = {
+	.probe = zx279128s_lsp_probe,
+	.driver = {
+		.name = "zx279128s-lspcrpm",
+		.of_match_table = zx279128s_lsp_of_match,
+		.suppress_bind_attrs = true,
+	},
+};
+builtin_platform_driver(zx279128s_lsp_driver);
+
+/* Top CRM 2 */
+#define CRM2_CLK		0x070
+#define CRM2_RST		0x074
+
+struct zx_crm2_gate {
+	struct clk_hw hw;
+	void __iomem *reg;
+	u32 clk_on;
+	u32 clk_off;
+	spinlock_t *lock;
+};
+
+static inline struct zx_crm2_gate *to_zx_crm2_gate(struct clk_hw *hw)
+{
+	return container_of(hw, struct zx_crm2_gate, hw);
+}
+
+static int zx_crm2_clk_enable(struct clk_hw *hw)
+{
+	struct zx_crm2_gate *gate = to_zx_crm2_gate(hw);
+	unsigned long flags;
+	u32 val;
+
+	spin_lock_irqsave(gate->lock, flags);
+	val = readl(gate->reg);
+	val |= gate->clk_on;
+	val &= ~gate->clk_off;
+	writel(val, gate->reg);
+	spin_unlock_irqrestore(gate->lock, flags);
+	return 0;
+}
+
+static void zx_crm2_clk_disable(struct clk_hw *hw)
+{
+	struct zx_crm2_gate *gate = to_zx_crm2_gate(hw);
+	unsigned long flags;
+	u32 val;
+
+	spin_lock_irqsave(gate->lock, flags);
+	val = readl(gate->reg);
+	val &= ~gate->clk_on;
+	writel(val, gate->reg);
+	spin_unlock_irqrestore(gate->lock, flags);
+}
+
+static int zx_crm2_clk_is_enabled(struct clk_hw *hw)
+{
+	struct zx_crm2_gate *gate = to_zx_crm2_gate(hw);
+	u32 val = readl(gate->reg);
+
+	return (val & gate->clk_on) == gate->clk_on;
+}
+
+static const struct clk_ops zx_crm2_clk_ops = {
+	.enable = zx_crm2_clk_enable,
+	.disable = zx_crm2_clk_disable,
+	.is_enabled = zx_crm2_clk_is_enabled,
+};
+
+struct zx_crm2_reset_data {
+	struct reset_controller_dev rcdev;
+	void __iomem *base;
+	spinlock_t *lock;
+};
+
+static const u8 zx_crm2_reset_bits[] = {
+	[ZX279128S_CRM2_RESET_PCIE0] = 0,
+	[ZX279128S_CRM2_RESET_PCIE1] = 3,
+};
+
+static int zx_crm2_reset_assert(struct reset_controller_dev *rcdev,
+				unsigned long id)
+{
+	struct zx_crm2_reset_data *data =
+		container_of(rcdev, struct zx_crm2_reset_data, rcdev);
+	unsigned long flags;
+	u32 val;
+
+	if (id >= ARRAY_SIZE(zx_crm2_reset_bits))
+		return -EINVAL;
+
+	spin_lock_irqsave(data->lock, flags);
+	val = readl(data->base + CRM2_RST);
+	val &= ~BIT(zx_crm2_reset_bits[id]);
+	writel(val, data->base + CRM2_RST);
+	spin_unlock_irqrestore(data->lock, flags);
+	return 0;
+}
+
+static int zx_crm2_reset_deassert(struct reset_controller_dev *rcdev,
+				  unsigned long id)
+{
+	struct zx_crm2_reset_data *data =
+		container_of(rcdev, struct zx_crm2_reset_data, rcdev);
+	unsigned long flags;
+	u32 val;
+
+	if (id >= ARRAY_SIZE(zx_crm2_reset_bits))
+		return -EINVAL;
+
+	spin_lock_irqsave(data->lock, flags);
+	val = readl(data->base + CRM2_RST);
+	val |= BIT(zx_crm2_reset_bits[id]);
+	writel(val, data->base + CRM2_RST);
+	spin_unlock_irqrestore(data->lock, flags);
+	return 0;
+}
+
+static int zx_crm2_reset_status(struct reset_controller_dev *rcdev,
+				unsigned long id)
+{
+	struct zx_crm2_reset_data *data =
+		container_of(rcdev, struct zx_crm2_reset_data, rcdev);
+	u32 val;
+
+	if (id >= ARRAY_SIZE(zx_crm2_reset_bits))
+		return -EINVAL;
+
+	val = readl(data->base + CRM2_RST);
+	return !(val & BIT(zx_crm2_reset_bits[id]));
+}
+
+static const struct reset_control_ops zx_crm2_reset_ops = {
+	.assert = zx_crm2_reset_assert,
+	.deassert = zx_crm2_reset_deassert,
+	.status = zx_crm2_reset_status,
+};
+
+static int zx279128s_crm2_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct clk_hw_onecell_data *clk_data;
+	struct zx_crm2_reset_data *rst_data;
+	struct zx_crm2_gate *gates;
+	struct clk_init_data init0 = { };
+	struct clk_init_data init1 = { };
+	void __iomem *base;
+	spinlock_t *lock;
+	int ret;
+
+	base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(base))
+		return PTR_ERR(base);
+
+	lock = devm_kzalloc(dev, sizeof(*lock), GFP_KERNEL);
+	if (!lock)
+		return -ENOMEM;
+	spin_lock_init(lock);
+
+	clk_data = devm_kzalloc(dev, struct_size(clk_data, hws, 2), GFP_KERNEL);
+	gates = devm_kcalloc(dev, 2, sizeof(*gates), GFP_KERNEL);
+	rst_data = devm_kzalloc(dev, sizeof(*rst_data), GFP_KERNEL);
+	if (!clk_data || !gates || !rst_data)
+		return -ENOMEM;
+
+	clk_data->num = 2;
+
+	/* PCIe0 clock */
+	gates[0].reg = base + CRM2_CLK;
+	gates[0].clk_on = 0xb5;
+	gates[0].clk_off = BIT(8);
+	gates[0].lock = lock;
+	gates[0].hw.init = &init0;
+	init0.name = "crm2_pcie0";
+	init0.ops = &zx_crm2_clk_ops;
+	ret = devm_clk_hw_register(dev, &gates[0].hw);
+	if (ret)
+		return ret;
+	clk_data->hws[ZX279128S_CRM2_PCIE0_CLK] = &gates[0].hw;
+
+	/* PCIe1 clock */
+	gates[1].reg = base + CRM2_CLK;
+	gates[1].clk_on = 0x1aa00;
+	gates[1].clk_off = BIT(17);
+	gates[1].lock = lock;
+	gates[1].hw.init = &init1;
+	init1.name = "crm2_pcie1";
+	init1.ops = &zx_crm2_clk_ops;
+	ret = devm_clk_hw_register(dev, &gates[1].hw);
+	if (ret)
+		return ret;
+	clk_data->hws[ZX279128S_CRM2_PCIE1_CLK] = &gates[1].hw;
+
+	ret = devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, clk_data);
+	if (ret)
+		return ret;
+
+	/* CRM2 Resets */
+	rst_data->base = base;
+	rst_data->lock = lock;
+	rst_data->rcdev.owner = THIS_MODULE;
+	rst_data->rcdev.nr_resets = ARRAY_SIZE(zx_crm2_reset_bits);
+	rst_data->rcdev.ops = &zx_crm2_reset_ops;
+	rst_data->rcdev.of_node = dev->of_node;
+
+	return devm_reset_controller_register(dev, &rst_data->rcdev);
+}
+
+static const struct of_device_id zx279128s_crm2_of_match[] = {
+	{ .compatible = "zte,zx279128s-crm2" },
+	{ }
+};
+
+static struct platform_driver zx279128s_crm2_driver = {
+	.probe = zx279128s_crm2_probe,
+	.driver = {
+		.name = "zx279128s-crm2",
+		.of_match_table = zx279128s_crm2_of_match,
+		.suppress_bind_attrs = true,
+	},
+};
+builtin_platform_driver(zx279128s_crm2_driver);
-- 
2.55.0



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

* [PATCH v2 07/24] dt-bindings: gpio: Add ZTE ZX279128S GPIO controller
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (5 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 06/24] clk: zte: Add ZX279128S CRM clock and reset driver Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 08/24] gpio: Add ZTE ZX279128S GPIO driver Navid Ghahremani
                   ` (17 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Linus Walleij,
	Bartosz Golaszewski, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, linux-gpio, devicetree, linux-kernel

Describe the register-clock consumer and GPIO banks.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 .../bindings/gpio/zte,zx279128s-gpio.yaml     | 53 +++++++++++++++++++
 1 file changed, 53 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/gpio/zte,zx279128s-gpio.yaml

diff --git a/Documentation/devicetree/bindings/gpio/zte,zx279128s-gpio.yaml b/Documentation/devicetree/bindings/gpio/zte,zx279128s-gpio.yaml
new file mode 100644
index 0000000000..132c7126fd
--- /dev/null
+++ b/Documentation/devicetree/bindings/gpio/zte,zx279128s-gpio.yaml
@@ -0,0 +1,53 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/gpio/zte,zx279128s-gpio.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx279128s GPIO controller
+
+maintainers:
+  - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description:
+  The ZTE zx279128s has several identical GPIO banks. Each bank has 16
+  lines and is described by its own node.
+
+properties:
+  compatible:
+    const: zte,zx279128s-gpio
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    maxItems: 1
+
+  gpio-controller: true
+
+  '#gpio-cells':
+    const: 2
+
+  gpio-line-names:
+    maxItems: 16
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - gpio-controller
+  - '#gpio-cells'
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/zte,zx279128s-crm.h>
+
+    gpio@94407000 {
+        compatible = "zte,zx279128s-gpio";
+        reg = <0x94407000 0x40>;
+        clocks = <&lsp0crpm ZX279128S_LSP0_GPIO_PCLK>;
+        gpio-controller;
+        #gpio-cells = <2>;
+    };
-- 
2.55.0



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

* [PATCH v2 08/24] gpio: Add ZTE ZX279128S GPIO driver
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (6 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 07/24] dt-bindings: gpio: Add ZTE ZX279128S GPIO controller Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 09/24] dt-bindings: pinctrl: pinctrl-single: Add ZTE ZX279128S pin mux Navid Ghahremani
                   ` (16 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Linus Walleij,
	Bartosz Golaszewski, linux-kernel, linux-gpio

Expose the six banks using the generic GPIO implementation and enable
the register clock before accessing the bank.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 drivers/gpio/Kconfig          | 12 ++++++
 drivers/gpio/Makefile         |  1 +
 drivers/gpio/gpio-zx279128s.c | 79 +++++++++++++++++++++++++++++++++++
 3 files changed, 92 insertions(+)
 create mode 100644 drivers/gpio/gpio-zx279128s.c

diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index a48586bb8e..3ae51a3c10 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -902,6 +902,18 @@ config GPIO_ZEVIO
 	help
 	  Say yes here to support the GPIO controller in LSI ZEVIO SoCs.
 
+config GPIO_ZX279128S
+	tristate "ZTE zx279128s GPIO support"
+	depends on ARCH_ZTE || COMPILE_TEST
+	select GPIO_GENERIC
+	help
+	  Say yes here to support the GPIO controller of the ZTE zx279128s
+	  SoC. It has several banks of 16 lines each, which boards such as
+	  the ZTE ZXHN H3600 use for LEDs, buttons and reset lines.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called gpio-zx279128s.
+
 config GPIO_ZYNQ
 	tristate "Xilinx Zynq GPIO support"
 	depends on ARCH_ZYNQ || ARCH_ZYNQMP || COMPILE_TEST
diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
index dc9e6d643b..a57ed7ec71 100644
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -223,5 +223,6 @@ obj-$(CONFIG_GPIO_XLP)			+= gpio-xlp.o
 obj-$(CONFIG_GPIO_XRA1403)		+= gpio-xra1403.o
 obj-$(CONFIG_GPIO_XTENSA)		+= gpio-xtensa.o
 obj-$(CONFIG_GPIO_ZEVIO)		+= gpio-zevio.o
+obj-$(CONFIG_GPIO_ZX279128S)		+= gpio-zx279128s.o
 obj-$(CONFIG_GPIO_ZYNQ)			+= gpio-zynq.o
 obj-$(CONFIG_GPIO_ZYNQMP_MODEPIN)	+= gpio-zynqmp-modepin.o
diff --git a/drivers/gpio/gpio-zx279128s.c b/drivers/gpio/gpio-zx279128s.c
new file mode 100644
index 0000000000..71eb6b5d82
--- /dev/null
+++ b/drivers/gpio/gpio-zx279128s.c
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ZTE zx279128s GPIO controller
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#include <linux/clk.h>
+#include <linux/err.h>
+#include <linux/gpio/driver.h>
+#include <linux/gpio/generic.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+
+/*
+ * Each bank has 16 lines, one bit per line in each register. The registers
+ * at 0x04-0x10 and 0x28-0x34 control the interrupts, which are not supported
+ * yet.
+ */
+#define ZX279128S_GPIO_DIR	0x00	/* 1: output */
+#define ZX279128S_GPIO_DATA	0x14	/* line level */
+#define ZX279128S_GPIO_SET	0x18	/* write 1: drive high */
+#define ZX279128S_GPIO_CLEAR	0x1c	/* write 1: drive low */
+
+static int zx279128s_gpio_probe(struct platform_device *pdev)
+{
+	struct gpio_generic_chip_config config = { };
+	struct device *dev = &pdev->dev;
+	struct gpio_generic_chip *chip;
+	void __iomem *base;
+	struct clk *clk;
+	int ret;
+
+	chip = devm_kzalloc(dev, sizeof(*chip), GFP_KERNEL);
+	if (!chip)
+		return -ENOMEM;
+
+	base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(base))
+		return PTR_ERR(base);
+
+	clk = devm_clk_get_enabled(dev, NULL);
+	if (IS_ERR(clk))
+		return dev_err_probe(dev, PTR_ERR(clk), "failed to get the clock\n");
+
+	config.dev = dev;
+	config.sz = 4;
+	config.dat = base + ZX279128S_GPIO_DATA;
+	config.set = base + ZX279128S_GPIO_SET;
+	config.clr = base + ZX279128S_GPIO_CLEAR;
+	config.dirout = base + ZX279128S_GPIO_DIR;
+
+	ret = gpio_generic_chip_init(chip, &config);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to set up the GPIO chip\n");
+
+	chip->gc.ngpio = 16;
+
+	return devm_gpiochip_add_data(dev, &chip->gc, NULL);
+}
+
+static const struct of_device_id zx279128s_gpio_of_match[] = {
+	{ .compatible = "zte,zx279128s-gpio" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, zx279128s_gpio_of_match);
+
+static struct platform_driver zx279128s_gpio_driver = {
+	.probe = zx279128s_gpio_probe,
+	.driver = {
+		.name = "zx279128s-gpio",
+		.of_match_table = zx279128s_gpio_of_match,
+	},
+};
+module_platform_driver(zx279128s_gpio_driver);
+
+MODULE_AUTHOR("Navid Ghahremani <ghahramani.navid@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx279128s GPIO controller driver");
+MODULE_LICENSE("GPL");
-- 
2.55.0



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

* [PATCH v2 09/24] dt-bindings: pinctrl: pinctrl-single: Add ZTE ZX279128S pin mux
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (7 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 08/24] gpio: Add ZTE ZX279128S GPIO driver Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 10/24] dt-bindings: phy: Add ZTE ZX279128S PCIe PHY binding Navid Ghahremani
                   ` (15 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Linus Walleij,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Tony Lindgren,
	linux-gpio, devicetree, linux-kernel

Use pinctrl-single for the verified PCIe and external-PHY bit selections
without guessing the unverified field layout.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 Documentation/devicetree/bindings/pinctrl/pinctrl-single.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Documentation/devicetree/bindings/pinctrl/pinctrl-single.yaml b/Documentation/devicetree/bindings/pinctrl/pinctrl-single.yaml
index afe7329a1d..bd57b41f1e 100644
--- a/Documentation/devicetree/bindings/pinctrl/pinctrl-single.yaml
+++ b/Documentation/devicetree/bindings/pinctrl/pinctrl-single.yaml
@@ -33,6 +33,7 @@ properties:
               - ti,omap4-padconf
               - ti,omap5-padconf
               - ti,j7200-padconf
+              - zte,zx279128s-pinmux
           - const: pinctrl-single
       - items:
           - enum:
-- 
2.55.0



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

* [PATCH v2 10/24] dt-bindings: phy: Add ZTE ZX279128S PCIe PHY binding
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (8 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 09/24] dt-bindings: pinctrl: pinctrl-single: Add ZTE ZX279128S pin mux Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 11/24] phy: zte: Add ZTE ZX279128S PCIe PHY driver Navid Ghahremani
                   ` (14 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, linux-phy, devicetree,
	linux-kernel

Add Device Tree binding for the PCIe SerDes PHY found inside the
topcrm block of the ZTE ZX279128S SoC.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 .../bindings/phy/zte,zx279128s-pcie-phy.yaml  | 40 +++++++++++++++++++
 1 file changed, 40 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/phy/zte,zx279128s-pcie-phy.yaml

diff --git a/Documentation/devicetree/bindings/phy/zte,zx279128s-pcie-phy.yaml b/Documentation/devicetree/bindings/phy/zte,zx279128s-pcie-phy.yaml
new file mode 100644
index 0000000000..7bfa363294
--- /dev/null
+++ b/Documentation/devicetree/bindings/phy/zte,zx279128s-pcie-phy.yaml
@@ -0,0 +1,40 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/phy/zte,zx279128s-pcie-phy.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE ZX279128S PCIe PHY
+
+maintainers:
+  - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description:
+  PCIe PHY block inside the ZTE ZX279128S Top CRM.
+
+properties:
+  compatible:
+    const: zte,zx279128s-pcie-phy
+
+  reg:
+    description: Port index (0 or 1)
+    items:
+      - enum: [0, 1]
+
+  "#phy-cells":
+    const: 0
+
+required:
+  - compatible
+  - reg
+  - "#phy-cells"
+
+additionalProperties: false
+
+examples:
+  - |
+    phy@0 {
+        compatible = "zte,zx279128s-pcie-phy";
+        reg = <0>;
+        #phy-cells = <0>;
+    };
-- 
2.55.0



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

* [PATCH v2 11/24] phy: zte: Add ZTE ZX279128S PCIe PHY driver
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (9 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 10/24] dt-bindings: phy: Add ZTE ZX279128S PCIe PHY binding Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 12/24] dt-bindings: PCI: Add ZTE ZX279128S host controller Navid Ghahremani
                   ` (13 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Vinod Koul,
	Neil Armstrong, Manivannan Sadhasivam, linux-kernel, linux-phy

Add a generic PHY driver for the PCIe SerDes PHY inside the Top CRM
block of the ZTE ZX279128S SoC. This driver encapsulates PLL power
and analog SerDes calibration for the two PCIe ports, isolating
these hardware details from the PCIe host controller driver.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 drivers/phy/Kconfig              |  12 +++
 drivers/phy/Makefile             |   1 +
 drivers/phy/phy-zx279128s-pcie.c | 156 +++++++++++++++++++++++++++++++
 3 files changed, 169 insertions(+)
 create mode 100644 drivers/phy/phy-zx279128s-pcie.c

diff --git a/drivers/phy/Kconfig b/drivers/phy/Kconfig
index 19f3b7d12b..3518d384be 100644
--- a/drivers/phy/Kconfig
+++ b/drivers/phy/Kconfig
@@ -91,6 +91,18 @@ config PHY_ECONET_PCIE
 	  callback for PCIe GEN1 and GEN2 ports. This PHY is found on
 	  EcoNet SoCs including EN751221 and EN7528.
 
+config PHY_ZX279128S_PCIE
+	tristate "ZTE ZX279128S PCIe PHY driver"
+	depends on ARCH_ZTE || COMPILE_TEST
+	depends on OF
+	select GENERIC_PHY
+	select MFD_SYSCON
+	help
+	  Enable support for the PCIe SerDes PHY found inside the Top CRM
+	  of ZTE ZX279128S SoCs. This driver configures PLL power and analog
+	  calibration for the two PCIe ports.
+	  If you say Y or M here, you will enable PCIe PHY initialization.
+
 config PHY_GOOGLE_USB
 	tristate "Google Tensor SoC USB PHY driver"
 	select GENERIC_PHY
diff --git a/drivers/phy/Makefile b/drivers/phy/Makefile
index d7aa516bcc..1323aa817f 100644
--- a/drivers/phy/Makefile
+++ b/drivers/phy/Makefile
@@ -19,6 +19,7 @@ obj-$(CONFIG_PHY_NXP_TJA1145)		+= phy-nxp-tja1145.o
 obj-$(CONFIG_PHY_PISTACHIO_USB)		+= phy-pistachio-usb.o
 obj-$(CONFIG_PHY_SNPS_EUSB2)		+= phy-snps-eusb2.o
 obj-$(CONFIG_PHY_XGENE)			+= phy-xgene.o
+obj-$(CONFIG_PHY_ZX279128S_PCIE)	+= phy-zx279128s-pcie.o
 
 obj-$(CONFIG_GENERIC_PHY)		+= allwinner/	\
 					   amlogic/	\
diff --git a/drivers/phy/phy-zx279128s-pcie.c b/drivers/phy/phy-zx279128s-pcie.c
new file mode 100644
index 0000000000..33bf0eaac8
--- /dev/null
+++ b/drivers/phy/phy-zx279128s-pcie.c
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ZTE zx279128s PCIe PHY driver
+ *
+ * Controls the analog SerDes and PLL for the two PCIe ports inside the
+ * Top CRM block.
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#define CRM1_PHY_CTRL		0x008
+#define CRM1_MISC		0x024
+#define CRM1_MISC_BIT26		BIT(26)
+#define CRM1_PHY1_CAL0		0x1b0
+#define CRM1_PHY1_CAL1		0x1b4
+
+struct zx_pcie_phy_port {
+	u32 phy_on[3];
+	bool has_cal;
+};
+
+static const struct zx_pcie_phy_port zx_phy_ports[] = {
+	[0] = {
+		.phy_on = { BIT(15), BIT(13), BIT(14) },
+	},
+	[1] = {
+		.phy_on = { BIT(27), BIT(25), BIT(26) },
+		.has_cal = true,
+	},
+};
+
+struct zx_pcie_phy {
+	struct regmap *regmap;
+	const struct zx_pcie_phy_port *port;
+	unsigned int port_id;
+};
+
+static int zx_pcie_phy_init(struct phy *phy)
+{
+	struct zx_pcie_phy *zphy = phy_get_drvdata(phy);
+	const struct zx_pcie_phy_port *port = zphy->port;
+	int i, ret;
+
+	for (i = 0; i < ARRAY_SIZE(port->phy_on); i++) {
+		ret = regmap_write_bits(zphy->regmap, CRM1_PHY_CTRL,
+					port->phy_on[i], port->phy_on[i]);
+		if (ret)
+			return ret;
+		fsleep(50);
+
+		if (i == 0 && port->has_cal) {
+			ret = regmap_write(zphy->regmap, CRM1_PHY1_CAL1, 0x00202d5a);
+			if (ret)
+				return ret;
+			ret = regmap_write(zphy->regmap, CRM1_PHY1_CAL0, 0x0046c24a);
+			if (ret)
+				return ret;
+		}
+	}
+
+	ret = regmap_write_bits(zphy->regmap, CRM1_MISC, CRM1_MISC_BIT26, 0);
+	if (ret)
+		return ret;
+	fsleep(50);
+
+	return 0;
+}
+
+static int zx_pcie_phy_exit(struct phy *phy)
+{
+	struct zx_pcie_phy *zphy = phy_get_drvdata(phy);
+	const struct zx_pcie_phy_port *port = zphy->port;
+	int i, ret;
+
+	for (i = ARRAY_SIZE(port->phy_on) - 1; i >= 0; i--) {
+		ret = regmap_write_bits(zphy->regmap, CRM1_PHY_CTRL,
+					port->phy_on[i], 0);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static const struct phy_ops zx_pcie_phy_ops = {
+	.init = zx_pcie_phy_init,
+	.exit = zx_pcie_phy_exit,
+	.owner = THIS_MODULE,
+};
+
+static int zx_pcie_phy_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct phy_provider *provider;
+	struct zx_pcie_phy *zphy;
+	struct phy *phy;
+	u32 port_id;
+	int ret;
+
+	zphy = devm_kzalloc(dev, sizeof(*zphy), GFP_KERNEL);
+	if (!zphy)
+		return -ENOMEM;
+
+	ret = of_property_read_u32(dev->of_node, "reg", &port_id);
+	if (ret)
+		return dev_err_probe(dev, ret, "missing reg property\n");
+
+	if (port_id >= ARRAY_SIZE(zx_phy_ports))
+		return dev_err_probe(dev, -EINVAL, "invalid port id %u\n", port_id);
+
+	zphy->port_id = port_id;
+	zphy->port = &zx_phy_ports[port_id];
+
+	zphy->regmap = syscon_node_to_regmap(dev->parent->of_node);
+	if (IS_ERR(zphy->regmap))
+		return dev_err_probe(dev, PTR_ERR(zphy->regmap),
+				     "failed to get parent topcrm regmap\n");
+
+	phy = devm_phy_create(dev, dev->of_node, &zx_pcie_phy_ops);
+	if (IS_ERR(phy))
+		return dev_err_probe(dev, PTR_ERR(phy), "failed to create phy\n");
+
+	phy_set_drvdata(phy, zphy);
+
+	provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
+	return PTR_ERR_OR_ZERO(provider);
+}
+
+static const struct of_device_id zx_pcie_phy_of_match[] = {
+	{ .compatible = "zte,zx279128s-pcie-phy" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, zx_pcie_phy_of_match);
+
+static struct platform_driver zx_pcie_phy_driver = {
+	.probe = zx_pcie_phy_probe,
+	.driver = {
+		.name = "zx279128s-pcie-phy",
+		.of_match_table = zx_pcie_phy_of_match,
+		.suppress_bind_attrs = true,
+	},
+};
+builtin_platform_driver(zx_pcie_phy_driver);
+
+MODULE_AUTHOR("Navid Ghahremani <ghahramani.navid@gmail.com>");
+MODULE_DESCRIPTION("ZTE ZX279128S PCIe PHY driver");
+MODULE_LICENSE("GPL");
-- 
2.55.0



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

* [PATCH v2 12/24] dt-bindings: PCI: Add ZTE ZX279128S host controller
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (10 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 11/24] phy: zte: Add ZTE ZX279128S PCIe PHY driver Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 13/24] PCI: dwc: Add ZTE ZX279128S host controller driver Navid Ghahremani
                   ` (12 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Bjorn Helgaas,
	Lorenzo Pieralisi, Krzysztof Wilczyński,
	Manivannan Sadhasivam, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, linux-pci, devicetree, linux-kernel

Add Device Tree binding for the DesignWare PCIe host controller on the
ZTE ZX279128S SoC, using standard clocks, resets, and phys properties.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 .../bindings/pci/zte,zx279128s-pcie.yaml      | 121 ++++++++++++++++++
 1 file changed, 121 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/pci/zte,zx279128s-pcie.yaml

diff --git a/Documentation/devicetree/bindings/pci/zte,zx279128s-pcie.yaml b/Documentation/devicetree/bindings/pci/zte,zx279128s-pcie.yaml
new file mode 100644
index 0000000000..dc0214511f
--- /dev/null
+++ b/Documentation/devicetree/bindings/pci/zte,zx279128s-pcie.yaml
@@ -0,0 +1,121 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/pci/zte,zx279128s-pcie.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx279128s PCIe host controller
+
+maintainers:
+  - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description:
+  The ZTE zx279128s has two PCIe host controllers based on the Synopsys
+  DesignWare PCIe IP, each with one lane. Their clocks, resets and PHYs are
+  controlled through the SoC's Top CRM blocks.
+
+allOf:
+  - $ref: /schemas/pci/pci-host-bridge.yaml#
+  - $ref: /schemas/pci/snps,dw-pcie-common.yaml#
+
+properties:
+  compatible:
+    const: zte,zx279128s-pcie
+
+  reg:
+    items:
+      - description: Data Bus Interface (DBI) registers
+      - description: Controller wrapper registers
+      - description: PCIe configuration space region
+
+  reg-names:
+    items:
+      - const: dbi
+      - const: ctrl
+      - const: config
+
+  interrupts:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: PCIe controller clock
+
+  clock-names:
+    items:
+      - const: pcie
+
+  resets:
+    items:
+      - description: PCIe controller reset
+
+  reset-names:
+    items:
+      - const: core
+
+  phys:
+    maxItems: 1
+
+  phy-names:
+    items:
+      - const: pcie
+
+  reset-gpios:
+    maxItems: 1
+    description: Optional board GPIO reset line for downstream endpoints.
+
+  enable-gpios:
+    maxItems: 1
+    description: Optional second board GPIO enable line driven together with reset-gpios.
+
+required:
+  - reg
+  - reg-names
+  - interrupts
+  - '#interrupt-cells'
+  - interrupt-map
+  - interrupt-map-mask
+  - clocks
+  - clock-names
+  - resets
+  - reset-names
+  - phys
+  - phy-names
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/zte,zx279128s-crm.h>
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/interrupt-controller/arm-gic.h>
+    #include <dt-bindings/reset/zte,zx279128s-reset.h>
+
+    pcie@f000000 {
+        compatible = "zte,zx279128s-pcie";
+        reg = <0x0f000000 0x4000>,
+              <0x09500000 0x1000>,
+              <0x1c000000 0x200000>;
+        reg-names = "dbi", "ctrl", "config";
+        #address-cells = <3>;
+        #size-cells = <2>;
+        device_type = "pci";
+        bus-range = <0x00 0xff>;
+        ranges = <0x82000000 0 0x10000000 0x10000000 0 0x08000000>;
+        interrupts = <GIC_SPI 63 IRQ_TYPE_LEVEL_HIGH>;
+        #interrupt-cells = <1>;
+        interrupt-map-mask = <0 0 0 7>;
+        interrupt-map = <0 0 0 1 &gic GIC_SPI 63 IRQ_TYPE_LEVEL_HIGH>,
+                        <0 0 0 2 &gic GIC_SPI 63 IRQ_TYPE_LEVEL_HIGH>,
+                        <0 0 0 3 &gic GIC_SPI 63 IRQ_TYPE_LEVEL_HIGH>,
+                        <0 0 0 4 &gic GIC_SPI 63 IRQ_TYPE_LEVEL_HIGH>;
+        linux,pci-domain = <0>;
+        clocks = <&crm2 ZX279128S_CRM2_PCIE0_CLK>;
+        clock-names = "pcie";
+        resets = <&crm2 ZX279128S_CRM2_RESET_PCIE0>;
+        reset-names = "core";
+        phys = <&pcie0_phy>;
+        phy-names = "pcie";
+        reset-gpios = <&gpio3 5 GPIO_ACTIVE_LOW>;
+        enable-gpios = <&gpio3 7 GPIO_ACTIVE_HIGH>;
+    };
-- 
2.55.0



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

* [PATCH v2 13/24] PCI: dwc: Add ZTE ZX279128S host controller driver
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (11 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 12/24] dt-bindings: PCI: Add ZTE ZX279128S host controller Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14 13:19   ` Bjorn Helgaas
  2026-10-14  7:20 ` [PATCH v2 14/24] dt-bindings: net: Add ZTE ZX279128S MDIO controller Navid Ghahremani
                   ` (11 subsequent siblings)
  24 siblings, 1 reply; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Manivannan Sadhasivam, Rob Herring,
	Bjorn Helgaas, Philipp Zabel, linux-kernel, linux-pci

Support the two Synopsys DesignWare PCIe host controllers of the
ZTE ZX279128S SoC. The driver manages the controller clocks via CCF,
asserts/deasserts resets via the Reset framework, and manages the
analog SerDes and PLL via the Generic PHY framework.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 drivers/pci/controller/dwc/Kconfig          |  13 +
 drivers/pci/controller/dwc/Makefile         |   1 +
 drivers/pci/controller/dwc/pcie-zx279128s.c | 273 ++++++++++++++++++++
 3 files changed, 287 insertions(+)
 create mode 100644 drivers/pci/controller/dwc/pcie-zx279128s.c

diff --git a/drivers/pci/controller/dwc/Kconfig b/drivers/pci/controller/dwc/Kconfig
index dcfbe7e229..3a4008be84 100644
--- a/drivers/pci/controller/dwc/Kconfig
+++ b/drivers/pci/controller/dwc/Kconfig
@@ -577,4 +577,17 @@ config PCIE_ULTRARISC
 	  By default, this symbol is enabled when ARCH_ULTRARISC is active,
 	  requiring no further configuration on that platform.
 
+config PCIE_ZX279128S
+	bool "ZTE zx279128s PCIe controller"
+	depends on ARCH_ZTE || COMPILE_TEST
+	depends on PCI_MSI
+	select GENERIC_PHY
+	select PCIE_DW_HOST
+	select RESET_CONTROLLER
+	help
+	  Say Y here to enable support for the two PCIe host controllers of
+	  the ZTE zx279128s SoC. They are based on the Synopsys DesignWare
+	  PCIe IP. On the ZTE ZXHN H3600 they connect the MT7915 Wi-Fi chip.
+	  The driver only supports legacy INTx interrupts.
+
 endmenu
diff --git a/drivers/pci/controller/dwc/Makefile b/drivers/pci/controller/dwc/Makefile
index d4f88cfc62..379198ebd6 100644
--- a/drivers/pci/controller/dwc/Makefile
+++ b/drivers/pci/controller/dwc/Makefile
@@ -35,6 +35,7 @@ obj-$(CONFIG_PCIE_TEGRA194) += pcie-tegra194.o
 obj-$(CONFIG_PCIE_UNIPHIER) += pcie-uniphier.o
 obj-$(CONFIG_PCIE_UNIPHIER_EP) += pcie-uniphier-ep.o
 obj-$(CONFIG_PCIE_VISCONTI_HOST) += pcie-visconti.o
+obj-$(CONFIG_PCIE_ZX279128S) += pcie-zx279128s.o
 obj-$(CONFIG_PCIE_RCAR_GEN4) += pcie-rcar-gen4.o
 obj-$(CONFIG_PCIE_SPACEMIT_K1) += pcie-spacemit-k1.o
 obj-$(CONFIG_PCIE_STM32_HOST) += pcie-stm32.o
diff --git a/drivers/pci/controller/dwc/pcie-zx279128s.c b/drivers/pci/controller/dwc/pcie-zx279128s.c
new file mode 100644
index 0000000000..592a7f7c4e
--- /dev/null
+++ b/drivers/pci/controller/dwc/pcie-zx279128s.c
@@ -0,0 +1,273 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ZTE zx279128s PCIe host controller
+ *
+ * The SoC has two DesignWare PCIe controllers with one lane each. Their
+ * clocks, resets and PHYs are managed through the CCF, Reset and Generic PHY
+ * frameworks.
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/init.h>
+#include <linux/io.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/pci.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+
+#include "pcie-designware.h"
+
+/* Controller wrapper ("ctrl" registers) */
+#define CTRL_CFG		0x000
+#define CTRL_CFG_EN		BIT(5)
+#define CTRL_STATUS		0x0dc
+#define CTRL_STATUS_LTSSM	GENMASK(22, 17)
+#define CTRL_LTSSM_L0		0x11
+
+struct zx279128s_pcie {
+	struct dw_pcie pci;
+	void __iomem *ctrl;
+	struct clk *clk;
+	struct reset_control *rst;
+	struct phy *phy;
+	struct gpio_desc *reset;
+	struct gpio_desc *enable;
+};
+
+#define to_zx279128s_pcie(x)	container_of(x, struct zx279128s_pcie, pci)
+
+/* The board reset lines are shared by both controllers, see host_init */
+static DEFINE_MUTEX(zx279128s_board_lock);
+static bool zx279128s_board_released;
+
+/* Limit the link to 2.5 GT/s and retrain it, as the vendor firmware does */
+static void zx279128s_pcie_force_gen1(struct dw_pcie *pci)
+{
+	u8 cap = dw_pcie_find_capability(pci, PCI_CAP_ID_EXP);
+	u32 val;
+	u16 val16;
+
+	if (WARN_ON(!cap))
+		return;
+
+	dw_pcie_dbi_ro_wr_en(pci);
+
+	/*
+	 * With L0s/L1 enabled the H3600 endpoints stop responding, so do not
+	 * advertise those states or clock power management until their wakeup
+	 * sequence is understood.
+	 */
+	val = dw_pcie_readl_dbi(pci, cap + PCI_EXP_LNKCAP);
+	val &= ~(PCI_EXP_LNKCAP_SLS | PCI_EXP_LNKCAP_ASPMS |
+		 PCI_EXP_LNKCAP_CLKPM);
+	val |= PCI_EXP_LNKCAP_SLS_2_5GB;
+	dw_pcie_writel_dbi(pci, cap + PCI_EXP_LNKCAP, val);
+
+	val16 = dw_pcie_readw_dbi(pci, cap + PCI_EXP_LNKCTL2);
+	val16 &= ~PCI_EXP_LNKCTL2_TLS;
+	val16 |= PCI_EXP_LNKCTL2_TLS_2_5GT;
+	dw_pcie_writew_dbi(pci, cap + PCI_EXP_LNKCTL2, val16);
+
+	val16 = dw_pcie_readw_dbi(pci, cap + PCI_EXP_LNKCTL);
+	val16 &= ~(PCI_EXP_LNKCTL_ASPMC | PCI_EXP_LNKCTL_CLKREQ_EN);
+	dw_pcie_writew_dbi(pci, cap + PCI_EXP_LNKCTL, val16 | PCI_EXP_LNKCTL_RL);
+
+	dw_pcie_dbi_ro_wr_dis(pci);
+}
+
+static int zx279128s_pcie_host_init(struct dw_pcie_rp *pp)
+{
+	struct dw_pcie *pci = to_dw_pcie_from_pp(pp);
+	struct zx279128s_pcie *pcie = to_zx279128s_pcie(pci);
+	int ret;
+
+	/*
+	 * The board lines reset a device that can sit behind both controllers
+	 * (the MT7915 of the H3600 uses both). The first controller to start
+	 * resets and releases it; the second must not reset it again.
+	 */
+	mutex_lock(&zx279128s_board_lock);
+	if (!zx279128s_board_released && (pcie->reset || pcie->enable)) {
+		ret = gpiod_direction_output(pcie->reset, 1);
+		if (ret)
+			goto err_unlock;
+		ret = gpiod_direction_output(pcie->enable, 0);
+		if (ret)
+			goto err_unlock;
+		msleep(100);
+		ret = gpiod_set_value_cansleep(pcie->reset, 0);
+		if (ret)
+			goto err_unlock;
+		ret = gpiod_set_value_cansleep(pcie->enable, 1);
+		if (ret)
+			goto err_unlock;
+		msleep(200);
+		zx279128s_board_released = true;
+	}
+
+	ret = phy_init(pcie->phy);
+	if (ret)
+		goto err_unlock;
+
+	ret = phy_power_on(pcie->phy);
+	if (ret)
+		goto err_phy_exit;
+
+	ret = reset_control_assert(pcie->rst);
+	if (ret)
+		goto err_phy_off;
+	fsleep(50);
+	ret = reset_control_deassert(pcie->rst);
+	if (ret)
+		goto err_phy_off;
+
+	mutex_unlock(&zx279128s_board_lock);
+
+	/* Enable the controller, which starts link training */
+	writel(readl(pcie->ctrl + CTRL_CFG) | CTRL_CFG_EN, pcie->ctrl + CTRL_CFG);
+
+	zx279128s_pcie_force_gen1(pci);
+
+	return 0;
+
+err_phy_off:
+	phy_power_off(pcie->phy);
+err_phy_exit:
+	phy_exit(pcie->phy);
+err_unlock:
+	mutex_unlock(&zx279128s_board_lock);
+	return dev_err_probe(pci->dev, ret, "failed to initialize PCIe\n");
+}
+
+static void zx279128s_pcie_host_deinit(struct dw_pcie_rp *pp)
+{
+	struct dw_pcie *pci = to_dw_pcie_from_pp(pp);
+	struct zx279128s_pcie *pcie = to_zx279128s_pcie(pci);
+
+	mutex_lock(&zx279128s_board_lock);
+	reset_control_assert(pcie->rst);
+	phy_power_off(pcie->phy);
+	phy_exit(pcie->phy);
+	mutex_unlock(&zx279128s_board_lock);
+}
+
+/*
+ * The Wi-Fi on the H3600 uses legacy INTx interrupts. Providing msi_init
+ * keeps the DWC core from setting up its internal MSI controller, which has
+ * not been tested on this SoC.
+ */
+static int zx279128s_pcie_msi_init(struct dw_pcie_rp *pp)
+{
+	return 0;
+}
+
+static const struct dw_pcie_host_ops zx279128s_pcie_host_ops = {
+	.init = zx279128s_pcie_host_init,
+	.deinit = zx279128s_pcie_host_deinit,
+	.msi_init = zx279128s_pcie_msi_init,
+};
+
+static int zx279128s_pcie_start_link(struct dw_pcie *pci)
+{
+	zx279128s_pcie_force_gen1(pci);
+
+	return 0;
+}
+
+static bool zx279128s_pcie_link_up(struct dw_pcie *pci)
+{
+	struct zx279128s_pcie *pcie = to_zx279128s_pcie(pci);
+	u32 val = readl(pcie->ctrl + CTRL_STATUS);
+
+	return FIELD_GET(CTRL_STATUS_LTSSM, val) == CTRL_LTSSM_L0;
+}
+
+static const struct dw_pcie_ops zx279128s_pcie_ops = {
+	.link_up = zx279128s_pcie_link_up,
+	.start_link = zx279128s_pcie_start_link,
+};
+
+static int zx279128s_pcie_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct zx279128s_pcie *pcie;
+	struct resource *res;
+
+	pcie = devm_kzalloc(dev, sizeof(*pcie), GFP_KERNEL);
+	if (!pcie)
+		return -ENOMEM;
+
+	pcie->pci.dev = dev;
+	pcie->pci.ops = &zx279128s_pcie_ops;
+	pcie->pci.pp.ops = &zx279128s_pcie_host_ops;
+
+	res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "ctrl");
+	pcie->ctrl = devm_ioremap_resource(dev, res);
+	if (IS_ERR(pcie->ctrl))
+		return PTR_ERR(pcie->ctrl);
+
+	pcie->clk = devm_clk_get_enabled(dev, "pcie");
+	if (IS_ERR(pcie->clk))
+		return dev_err_probe(dev, PTR_ERR(pcie->clk),
+				     "failed to get pcie clock\n");
+
+	pcie->rst = devm_reset_control_get_exclusive(dev, "core");
+	if (IS_ERR(pcie->rst))
+		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\n");
+
+	pcie->phy = devm_phy_get(dev, "pcie");
+	if (IS_ERR(pcie->phy))
+		return dev_err_probe(dev, PTR_ERR(pcie->phy),
+				     "failed to get pcie phy\n");
+
+	/*
+	 * Optional board lines to reset and enable endpoints. If already
+	 * claimed by another controller, treat -EBUSY as already managed.
+	 */
+	pcie->reset = devm_gpiod_get_optional(dev, "reset", GPIOD_ASIS);
+	if (IS_ERR(pcie->reset)) {
+		if (PTR_ERR(pcie->reset) == -EBUSY)
+			pcie->reset = NULL;
+		else
+			return dev_err_probe(dev, PTR_ERR(pcie->reset),
+					     "failed to get the reset GPIO\n");
+	}
+
+	pcie->enable = devm_gpiod_get_optional(dev, "enable", GPIOD_ASIS);
+	if (IS_ERR(pcie->enable)) {
+		if (PTR_ERR(pcie->enable) == -EBUSY)
+			pcie->enable = NULL;
+		else
+			return dev_err_probe(dev, PTR_ERR(pcie->enable),
+					     "failed to get the enable GPIO\n");
+	}
+
+	return dw_pcie_host_init(&pcie->pci.pp);
+}
+
+static const struct of_device_id zx279128s_pcie_of_match[] = {
+	{ .compatible = "zte,zx279128s-pcie" },
+	{ }
+};
+
+static struct platform_driver zx279128s_pcie_driver = {
+	.probe = zx279128s_pcie_probe,
+	.driver = {
+		.name = "zx279128s-pcie",
+		.of_match_table = zx279128s_pcie_of_match,
+		.suppress_bind_attrs = true,
+	},
+};
+builtin_platform_driver(zx279128s_pcie_driver);
-- 
2.55.0



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

* [PATCH v2 14/24] dt-bindings: net: Add ZTE ZX279128S MDIO controller
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (12 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 13/24] PCI: dwc: Add ZTE ZX279128S host controller driver Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 15/24] net: mdio: " Navid Ghahremani
                   ` (10 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Andrew Lunn,
	David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, netdev,
	devicetree, linux-kernel

Describe the MDIO register block and its work/register clocks. External-
PHY pin selections use pinctrl rather than direct syscon writes.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 .../bindings/net/zte,zx279128s-mdio.yaml      | 65 +++++++++++++++++++
 1 file changed, 65 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/net/zte,zx279128s-mdio.yaml

diff --git a/Documentation/devicetree/bindings/net/zte,zx279128s-mdio.yaml b/Documentation/devicetree/bindings/net/zte,zx279128s-mdio.yaml
new file mode 100644
index 0000000000..fb12414ab3
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/zte,zx279128s-mdio.yaml
@@ -0,0 +1,65 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/zte,zx279128s-mdio.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx279128s MDIO controller
+
+maintainers:
+  - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description:
+  The MDIO controller of the ZTE zx279128s reaches the PHYs of the integrated
+  switch and an optional external PHY. On boards with an external PHY, the
+  default pin state connects the interface to it.
+
+allOf:
+  - $ref: mdio.yaml#
+
+properties:
+  compatible:
+    const: zte,zx279128s-mdio
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: work clock, the source of the management clock (MDC)
+      - description: register (APB) clock
+
+  clock-names:
+    items:
+      - const: wclk
+      - const: pclk
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - clock-names
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/zte,zx279128s-crm.h>
+
+    mdio@9a101000 {
+        compatible = "zte,zx279128s-mdio";
+        reg = <0x9a101000 0x18>;
+        clocks = <&lsp1crpm ZX279128S_LSP1_MDIO_WCLK>,
+                 <&lsp1crpm ZX279128S_LSP1_MDIO_PCLK>;
+        clock-names = "wclk", "pclk";
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        ethernet-phy@8 {
+            reg = <8>;
+        };
+
+        ethernet-phy@a {
+            reg = <10>;
+        };
+    };
-- 
2.55.0



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

* [PATCH v2 15/24] net: mdio: Add ZTE ZX279128S MDIO controller
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (13 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 14/24] dt-bindings: net: Add ZTE ZX279128S MDIO controller Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14 12:13   ` Andrew Lunn
  2026-10-14  7:20 ` [PATCH v2 16/24] net: phy: Add Sanechips ZX5201 PHY support Navid Ghahremani
                   ` (9 subsequent siblings)
  24 siblings, 1 reply; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Andrew Lunn,
	Heiner Kallweit, Russell King, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-kernel, netdev

Provide bounded Clause 22 transactions and managed clock and MDIO-bus
registration.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 drivers/net/mdio/Kconfig          |  11 +++
 drivers/net/mdio/Makefile         |   1 +
 drivers/net/mdio/mdio-zx279128s.c | 157 ++++++++++++++++++++++++++++++
 3 files changed, 169 insertions(+)
 create mode 100644 drivers/net/mdio/mdio-zx279128s.c

diff --git a/drivers/net/mdio/Kconfig b/drivers/net/mdio/Kconfig
index a05229838c..59812a082a 100644
--- a/drivers/net/mdio/Kconfig
+++ b/drivers/net/mdio/Kconfig
@@ -199,6 +199,17 @@ config MDIO_THUNDER
 	  ThunderX SoCs when the MDIO bus device appears as a PCI
 	  device.
 
+config MDIO_ZX279128S
+	tristate "ZTE zx279128s MDIO bus controller"
+	depends on ARCH_ZTE || COMPILE_TEST
+	depends on HAS_IOMEM && OF_MDIO
+	help
+	  This driver supports the MDIO controller of the ZTE zx279128s
+	  SoC. It reaches the PHYs of the integrated switch and, on boards
+	  such as the ZTE ZXHN H3600, an external PHY.
+
+	  To compile this driver as a module, choose M here.
+
 menu "MDIO Multiplexers"
 
 config MDIO_BUS_MUX
diff --git a/drivers/net/mdio/Makefile b/drivers/net/mdio/Makefile
index 0485867460..ce21e1496c 100644
--- a/drivers/net/mdio/Makefile
+++ b/drivers/net/mdio/Makefile
@@ -26,6 +26,7 @@ obj-$(CONFIG_MDIO_REGMAP)		+= mdio-regmap.o
 obj-$(CONFIG_MDIO_SUN4I)		+= mdio-sun4i.o
 obj-$(CONFIG_MDIO_THUNDER)		+= mdio-thunder.o
 obj-$(CONFIG_MDIO_XGENE)		+= mdio-xgene.o
+obj-$(CONFIG_MDIO_ZX279128S)		+= mdio-zx279128s.o
 
 obj-$(CONFIG_MDIO_BUS_MUX)		+= mdio-mux.o
 obj-$(CONFIG_MDIO_BUS_MUX_BCM6368)	+= mdio-mux-bcm6368.o
diff --git a/drivers/net/mdio/mdio-zx279128s.c b/drivers/net/mdio/mdio-zx279128s.c
new file mode 100644
index 0000000000..fa0aab5b28
--- /dev/null
+++ b/drivers/net/mdio/mdio-zx279128s.c
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ZTE zx279128s MDIO controller
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/err.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/of_mdio.h>
+#include <linux/phy.h>
+#include <linux/platform_device.h>
+
+#define ZX_MDIO_DATA_WR		0x04
+#define ZX_MDIO_DATA_RD		0x08
+#define ZX_MDIO_CFG		0x0c
+#define ZX_MDIO_STATUS		0x10
+#define  ZX_MDIO_STATUS_DONE	BIT(0)
+#define ZX_MDIO_CMD		0x14
+#define  ZX_MDIO_CMD_REG	GENMASK(4, 0)
+#define  ZX_MDIO_CMD_ADDR	GENMASK(9, 5)
+#define  ZX_MDIO_CMD_WR		BIT(10)
+#define  ZX_MDIO_CMD_RD		BIT(11)
+#define  ZX_MDIO_CMD_CLK_DIV	BIT(12)
+#define  ZX_MDIO_CMD_BIT13	BIT(13)
+#define  ZX_MDIO_CMD_START	BIT(14)
+#define  ZX_MDIO_CMD_BIT15	BIT(15)
+
+#define ZX_MDIO_POLL_US		10
+#define ZX_MDIO_TIMEOUT_US	2000
+
+struct zx279128s_mdio {
+	void __iomem *base;
+};
+
+static int zx279128s_mdio_xfer(struct mii_bus *bus, u32 op, int addr,
+			       int regnum, u16 data)
+{
+	struct zx279128s_mdio *priv = bus->priv;
+	void __iomem *base = priv->base;
+	u32 cmd, val;
+	int ret;
+
+	/* Clear the previous command and its status */
+	writel(readl(base + ZX_MDIO_CMD) & ~ZX_MDIO_CMD_START,
+	       base + ZX_MDIO_CMD);
+	writel(0, base + ZX_MDIO_STATUS);
+
+	cmd = readl(base + ZX_MDIO_CMD);
+	cmd &= ~(ZX_MDIO_CMD_WR | ZX_MDIO_CMD_RD | ZX_MDIO_CMD_ADDR |
+		 ZX_MDIO_CMD_REG);
+	cmd |= op | FIELD_PREP(ZX_MDIO_CMD_ADDR, addr) |
+	       FIELD_PREP(ZX_MDIO_CMD_REG, regnum);
+
+	if (op == ZX_MDIO_CMD_WR)
+		writel(data, base + ZX_MDIO_DATA_WR);
+	writel(cmd | ZX_MDIO_CMD_START, base + ZX_MDIO_CMD);
+
+	ret = readl_poll_timeout(base + ZX_MDIO_STATUS, val,
+				 val & ZX_MDIO_STATUS_DONE, ZX_MDIO_POLL_US,
+				 ZX_MDIO_TIMEOUT_US);
+	if (ret) {
+		dev_err_ratelimited(&bus->dev, "timeout (addr %d, reg %d)\n",
+				    addr, regnum);
+		return ret;
+	}
+
+	writel(0, base + ZX_MDIO_STATUS);
+	writel(readl(base + ZX_MDIO_CMD) & ~ZX_MDIO_CMD_START,
+	       base + ZX_MDIO_CMD);
+
+	return 0;
+}
+
+static int zx279128s_mdio_read(struct mii_bus *bus, int addr, int regnum)
+{
+	struct zx279128s_mdio *priv = bus->priv;
+	int ret;
+
+	ret = zx279128s_mdio_xfer(bus, ZX_MDIO_CMD_RD, addr, regnum, 0);
+	if (ret)
+		return ret;
+
+	return readl(priv->base + ZX_MDIO_DATA_RD) & 0xffff;
+}
+
+static int zx279128s_mdio_write(struct mii_bus *bus, int addr, int regnum,
+				u16 val)
+{
+	return zx279128s_mdio_xfer(bus, ZX_MDIO_CMD_WR, addr, regnum, val);
+}
+
+static int zx279128s_mdio_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct zx279128s_mdio *priv;
+	struct mii_bus *bus;
+	struct clk *clk;
+	u32 val;
+
+	bus = devm_mdiobus_alloc_size(dev, sizeof(*priv));
+	if (!bus)
+		return -ENOMEM;
+
+	priv = bus->priv;
+	priv->base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(priv->base))
+		return PTR_ERR(priv->base);
+
+	clk = devm_clk_get_enabled(dev, "pclk");
+	if (IS_ERR(clk))
+		return dev_err_probe(dev, PTR_ERR(clk),
+				     "failed to get the register clock\n");
+	clk = devm_clk_get_enabled(dev, "wclk");
+	if (IS_ERR(clk))
+		return dev_err_probe(dev, PTR_ERR(clk),
+				     "failed to get the work clock\n");
+
+	val = readl(priv->base + ZX_MDIO_CMD);
+	val &= ~(ZX_MDIO_CMD_START | ZX_MDIO_CMD_BIT15 | ZX_MDIO_CMD_BIT13);
+	val |= ZX_MDIO_CMD_CLK_DIV;
+	writel(val, priv->base + ZX_MDIO_CMD);
+	writel(0, priv->base + ZX_MDIO_STATUS);
+	writel(0, priv->base + ZX_MDIO_CFG);
+
+	bus->name = "zx279128s-mdio";
+	bus->read = zx279128s_mdio_read;
+	bus->write = zx279128s_mdio_write;
+	snprintf(bus->id, MII_BUS_ID_SIZE, "%s", dev_name(dev));
+	bus->parent = dev;
+
+	return devm_of_mdiobus_register(dev, bus, dev->of_node);
+}
+
+static const struct of_device_id zx279128s_mdio_of_match[] = {
+	{ .compatible = "zte,zx279128s-mdio" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, zx279128s_mdio_of_match);
+
+static struct platform_driver zx279128s_mdio_driver = {
+	.probe = zx279128s_mdio_probe,
+	.driver = {
+		.name = "zx279128s-mdio",
+		.of_match_table = zx279128s_mdio_of_match,
+	},
+};
+module_platform_driver(zx279128s_mdio_driver);
+
+MODULE_AUTHOR("Navid Ghahremani <ghahramani.navid@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx279128s MDIO controller driver");
+MODULE_LICENSE("GPL");
-- 
2.55.0



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

* [PATCH v2 16/24] net: phy: Add Sanechips ZX5201 PHY support
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (14 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 15/24] net: mdio: " Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14 12:37   ` Andrew Lunn
  2026-10-14  7:20 ` [PATCH v2 17/24] dt-bindings: net: Add ZTE ZX279128S Ethernet switch Navid Ghahremani
                   ` (8 subsequent siblings)
  24 siblings, 1 reply; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Andrew Lunn,
	Heiner Kallweit, Russell King, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-kernel, netdev

Initialize the external WAN PHY and its SerDes path; generic PHY support
handles negotiation.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 drivers/net/phy/Kconfig     |   8 ++
 drivers/net/phy/Makefile    |   1 +
 drivers/net/phy/sanechips.c | 152 ++++++++++++++++++++++++++++++++++++
 3 files changed, 161 insertions(+)
 create mode 100644 drivers/net/phy/sanechips.c

diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig
index b4ef927fd4..63620fdfc4 100644
--- a/drivers/net/phy/Kconfig
+++ b/drivers/net/phy/Kconfig
@@ -416,6 +416,14 @@ config ROCKCHIP_PHY
 	help
 	  Currently supports the integrated Ethernet PHY.
 
+config SANECHIPS_PHY
+	tristate "Sanechips Ethernet PHYs"
+	help
+	  Currently supports the Sanechips ZX5201 PHY, which the ZTE ZXHN
+	  H3600 uses for its WAN port. The driver sets the PHY up so that
+	  its SerDes interface passes traffic; everything else is handled
+	  by the generic PHY code.
+
 config SMSC_PHY
 	tristate "SMSC PHYs"
 	select CRC16
diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
index 25c4a3c242..666309f7d0 100644
--- a/drivers/net/phy/Makefile
+++ b/drivers/net/phy/Makefile
@@ -97,6 +97,7 @@ obj-$(CONFIG_QSEMI_PHY)		+= qsemi.o
 obj-$(CONFIG_REALTEK_PHY)	+= realtek/
 obj-$(CONFIG_RENESAS_PHY)	+= uPD60620.o
 obj-$(CONFIG_ROCKCHIP_PHY)	+= rockchip.o
+obj-$(CONFIG_SANECHIPS_PHY)	+= sanechips.o
 obj-$(CONFIG_SMSC_PHY)		+= smsc.o
 obj-$(CONFIG_STE10XP)		+= ste10Xp.o
 obj-$(CONFIG_TERANETICS_PHY)	+= teranetics.o
diff --git a/drivers/net/phy/sanechips.c b/drivers/net/phy/sanechips.c
new file mode 100644
index 0000000000..92d6388f7d
--- /dev/null
+++ b/drivers/net/phy/sanechips.c
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Sanechips ZX5201 Ethernet PHY
+ *
+ * The ZX5201 answers on two MDIO addresses: the copper PHY on its own
+ * address and its MAC-side SerDes interface on the next one. The copper PHY
+ * uses the standard registers and the generic PHY code; this driver only adds
+ * the setup that the PHY needs before it passes traffic. The values come from
+ * the vendor firmware of the ZTE ZXHN H3600, and most of them are not
+ * documented.
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#include <linux/module.h>
+#include <linux/phy.h>
+
+#define PHY_ID_ZX5201		0x84b95011
+
+/* Copper PHY vendor registers */
+#define ZX5201_REG18		18
+#define ZX5201_EXT_ADDR		16	/* extended register access: address */
+#define ZX5201_EXT_DATA		17	/* extended register access: data */
+#define ZX5201_REG29		29
+
+/*
+ * SerDes interface registers, at the PHY's MDIO address + 1. The names are
+ * a best guess from the way the vendor firmware uses them.
+ */
+#define ZX5201_SDS_WDATA_LO	16
+#define ZX5201_SDS_WDATA_HI	17
+#define ZX5201_SDS_CMD		18
+#define ZX5201_SDS_RDATA_LO	20
+#define ZX5201_SDS_RDATA_HI	21
+#define ZX5201_SDS_REG22	22
+#define ZX5201_SDS_REG27	27
+
+static int zx5201_sds_read(struct phy_device *phydev, u32 regnum)
+{
+	return mdiobus_read(phydev->mdio.bus, phydev->mdio.addr + 1, regnum);
+}
+
+static int zx5201_sds_write(struct phy_device *phydev, u32 regnum, u16 val)
+{
+	return mdiobus_write(phydev->mdio.bus, phydev->mdio.addr + 1, regnum,
+			     val);
+}
+
+static int zx5201_ext_write(struct phy_device *phydev, u16 reg, u16 val)
+{
+	int ret;
+
+	ret = phy_write(phydev, ZX5201_EXT_ADDR, reg);
+	if (ret)
+		return ret;
+
+	return phy_write(phydev, ZX5201_EXT_DATA, val);
+}
+
+static int zx5201_config_init(struct phy_device *phydev)
+{
+	static const u16 led_regs[] = { 0xb409, 0xb407, 0xb406, 0xb408 };
+	int ret, lo, hi, i;
+
+	ret = phy_write(phydev, ZX5201_REG18, 0x8402);
+	if (ret)
+		return ret;
+
+	ret = zx5201_sds_write(phydev, ZX5201_SDS_REG22, 0x0a0f);
+	if (ret)
+		return ret;
+
+	ret = zx5201_sds_write(phydev, ZX5201_SDS_REG27, 0x0800);
+	if (ret)
+		return ret;
+
+	ret = phy_write(phydev, ZX5201_REG29, 0x0355);
+	if (ret)
+		return ret;
+
+	ret = zx5201_ext_write(phydev, 0xb62d, 0x0006);
+	if (ret)
+		return ret;
+
+	/*
+	 * This looks like an indirect read-modify-write of SerDes register 4:
+	 * read it, set bits 9-13 of its high half (bits 25-29 of the register)
+	 * to 0b10100, and write it back.
+	 */
+	ret = zx5201_sds_write(phydev, ZX5201_SDS_CMD, 0x0004);
+	if (ret)
+		return ret;
+
+	hi = zx5201_sds_read(phydev, ZX5201_SDS_RDATA_HI);
+	if (hi < 0)
+		return hi;
+
+	lo = zx5201_sds_read(phydev, ZX5201_SDS_RDATA_LO);
+	if (lo < 0)
+		return lo;
+
+	ret = zx5201_sds_write(phydev, ZX5201_SDS_WDATA_HI,
+			       (hi & 0xc1ff) | 0x2800);
+	if (ret)
+		return ret;
+
+	ret = zx5201_sds_write(phydev, ZX5201_SDS_WDATA_LO, lo);
+	if (ret)
+		return ret;
+
+	ret = zx5201_sds_write(phydev, ZX5201_SDS_CMD, 0x0204);
+	if (ret)
+		return ret;
+
+	ret = zx5201_sds_read(phydev, ZX5201_SDS_REG22);
+	if (ret < 0)
+		return ret;
+
+	ret = zx5201_sds_write(phydev, ZX5201_SDS_REG22, (ret & 0xfff3) | 0x0004);
+	if (ret)
+		return ret;
+
+	/* LED control registers, all cleared as the vendor firmware does */
+	for (i = 0; i < ARRAY_SIZE(led_regs); i++) {
+		ret = zx5201_ext_write(phydev, led_regs[i], 0);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static struct phy_driver sanechips_phy_driver[] = {
+	{
+		PHY_ID_MATCH_EXACT(PHY_ID_ZX5201),
+		.name		= "Sanechips ZX5201",
+		.config_init	= zx5201_config_init,
+		.suspend	= genphy_suspend,
+		.resume		= genphy_resume,
+	},
+};
+module_phy_driver(sanechips_phy_driver);
+
+static const struct mdio_device_id __maybe_unused sanechips_phy_tbl[] = {
+	{ PHY_ID_MATCH_EXACT(PHY_ID_ZX5201) },
+	{ }
+};
+MODULE_DEVICE_TABLE(mdio, sanechips_phy_tbl);
+
+MODULE_AUTHOR("Navid Ghahremani <ghahramani.navid@gmail.com>");
+MODULE_DESCRIPTION("Sanechips Ethernet PHY driver");
+MODULE_LICENSE("GPL");
-- 
2.55.0



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

* [PATCH v2 17/24] dt-bindings: net: Add ZTE ZX279128S Ethernet switch
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (15 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 16/24] net: phy: Add Sanechips ZX5201 PHY support Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 18/24] net: ethernet: zte: Add ZX279128S Ethernet switch driver Navid Ghahremani
                   ` (7 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Andrew Lunn,
	David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, netdev,
	devicetree, linux-kernel

Describe the switch, receive interrupt and per-port PHY modes and MAC
addresses.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 .../bindings/net/zte,zx279128s-gmac.yaml      | 125 ++++++++++++++++++
 1 file changed, 125 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/net/zte,zx279128s-gmac.yaml

diff --git a/Documentation/devicetree/bindings/net/zte,zx279128s-gmac.yaml b/Documentation/devicetree/bindings/net/zte,zx279128s-gmac.yaml
new file mode 100644
index 0000000000..fcbdbafa76
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/zte,zx279128s-gmac.yaml
@@ -0,0 +1,125 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/zte,zx279128s-gmac.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx279128s Ethernet switch
+
+maintainers:
+  - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description:
+  The ZTE zx279128s has a five-port gigabit Ethernet switch with a traffic
+  manager, a buffer manager and a DMA engine that connects it to the CPU.
+  Each port is described by a child node of ethernet-ports and becomes its
+  own network interface, with the MAC address given in that node.
+
+properties:
+  compatible:
+    const: zte,zx279128s-gmac
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    maxItems: 1
+    description: The traffic manager's interrupt, which signals frames
+      received for the CPU.
+
+  ethernet-ports:
+    type: object
+    additionalProperties: false
+
+    properties:
+      '#address-cells':
+        const: 1
+      '#size-cells':
+        const: 0
+
+    patternProperties:
+      "^port@[0-4]$":
+        type: object
+        $ref: /schemas/net/ethernet-controller.yaml#
+        unevaluatedProperties: false
+
+        properties:
+          reg:
+            description: Switch port number
+            maximum: 4
+
+          label: true
+
+          phy-handle: true
+
+          phy-mode:
+            description: internal for the SoC's own PHYs of ports 0-3, gmii
+              for an external PHY.
+            enum: [gmii, internal]
+
+        required:
+          - reg
+          - phy-handle
+          - phy-mode
+
+    required:
+      - '#address-cells'
+      - '#size-cells'
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - ethernet-ports
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+    mdio {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        phy8: ethernet-phy@8 {
+            reg = <8>;
+        };
+
+        phy10: ethernet-phy@a {
+            reg = <10>;
+        };
+    };
+
+    macaddr: nvmem {
+        #nvmem-cell-cells = <1>;
+    };
+
+    ethernet@92000000 {
+        compatible = "zte,zx279128s-gmac";
+        reg = <0x92000000 0x400000>;
+        interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
+
+        ethernet-ports {
+            #address-cells = <1>;
+            #size-cells = <0>;
+
+            port@0 {
+                reg = <0>;
+                label = "lan1";
+                phy-handle = <&phy10>;
+                phy-mode = "internal";
+                nvmem-cells = <&macaddr 0>;
+                nvmem-cell-names = "mac-address";
+            };
+
+            port@4 {
+                reg = <4>;
+                label = "wan";
+                phy-handle = <&phy8>;
+                phy-mode = "gmii";
+                nvmem-cells = <&macaddr 1>;
+                nvmem-cell-names = "mac-address";
+            };
+        };
+    };
-- 
2.55.0



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

* [PATCH v2 18/24] net: ethernet: zte: Add ZX279128S Ethernet switch driver
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (16 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 17/24] dt-bindings: net: Add ZTE ZX279128S Ethernet switch Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14  7:20 ` [PATCH v2 19/24] dt-bindings: spi: Add ZTE ZX family SPI flash controller binding Navid Ghahremani
                   ` (6 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Junyang Han,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Russell King, linux-kernel, netdev

Provide per-port interfaces, phylink, IRQ NAPI, switchdev bridge
forwarding, hardware IPv4 flow offload and verified standard MAC
statistics. Use software fallback for unsupported VLAN and routing
cases.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 drivers/net/ethernet/zte/Kconfig            |   13 +
 drivers/net/ethernet/zte/Makefile           |    3 +
 drivers/net/ethernet/zte/zx279128s-eth.h    |  386 +++++
 drivers/net/ethernet/zte/zx279128s-main.c   | 1502 +++++++++++++++++++
 drivers/net/ethernet/zte/zx279128s-ppe.c    |  805 ++++++++++
 drivers/net/ethernet/zte/zx279128s-switch.c |  368 +++++
 6 files changed, 3077 insertions(+)
 create mode 100644 drivers/net/ethernet/zte/zx279128s-eth.h
 create mode 100644 drivers/net/ethernet/zte/zx279128s-main.c
 create mode 100644 drivers/net/ethernet/zte/zx279128s-ppe.c
 create mode 100644 drivers/net/ethernet/zte/zx279128s-switch.c

diff --git a/drivers/net/ethernet/zte/Kconfig b/drivers/net/ethernet/zte/Kconfig
index b95c2fc7db..20df75486f 100644
--- a/drivers/net/ethernet/zte/Kconfig
+++ b/drivers/net/ethernet/zte/Kconfig
@@ -17,4 +17,17 @@ if NET_VENDOR_ZTE
 
 source "drivers/net/ethernet/zte/dinghai/Kconfig"
 
+config ZX279128S_ETH
+	bool "ZTE zx279128s Ethernet switch support"
+	depends on ARCH_ZTE || COMPILE_TEST
+	depends on OF
+	depends on BRIDGE=y || BRIDGE=n
+	select NET_SWITCHDEV
+	select PHYLINK
+	help
+	  This driver supports the five-port gigabit switch of the ZTE
+	  zx279128s SoC and the DMA engine that connects it to the CPU.
+	  Each switch port becomes its own network interface, such as the
+	  four LAN ports and the WAN port of the ZTE ZXHN H3600.
+
 endif # NET_VENDOR_ZTE
diff --git a/drivers/net/ethernet/zte/Makefile b/drivers/net/ethernet/zte/Makefile
index cd9929b615..09b4f6558d 100644
--- a/drivers/net/ethernet/zte/Makefile
+++ b/drivers/net/ethernet/zte/Makefile
@@ -4,3 +4,6 @@
 #
 
 obj-$(CONFIG_DINGHAI) += dinghai/
+
+obj-$(CONFIG_ZX279128S_ETH) += zx279128s-eth.o
+zx279128s-eth-y := zx279128s-main.o zx279128s-ppe.o zx279128s-switch.o
diff --git a/drivers/net/ethernet/zte/zx279128s-eth.h b/drivers/net/ethernet/zte/zx279128s-eth.h
new file mode 100644
index 0000000000..6c6e187cce
--- /dev/null
+++ b/drivers/net/ethernet/zte/zx279128s-eth.h
@@ -0,0 +1,386 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * ZTE zx279128s Ethernet switch and DMA driver: registers and shared state
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#ifndef ZX279128S_ETH_H
+#define ZX279128S_ETH_H
+
+#include <linux/bitmap.h>
+#include <linux/hrtimer.h>
+#include <linux/if_ether.h>
+#include <linux/mutex.h>
+#include <linux/netdevice.h>
+#include <linux/notifier.h>
+#include <linux/phylink.h>
+#include <linux/rhashtable.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+#include <linux/workqueue.h>
+
+#define DRV_NAME		"zx279128s-eth"
+
+/*
+ * Register offsets from the start of the block (0x92000000). The names follow
+ * the vendor modules where they are known. Registers that only appear in the
+ * vendor's init sequences, with no known meaning, keep their offsets.
+ */
+
+/* Global control */
+#define ZX_GLB_RESET		0x000008	/* write 0, then all ones */
+#define ZX_GLB_CLK_CTRL		0x040018
+#define  ZX_GLB_CLK_CTRL_BITS	GENMASK(1, 0)	/* cleared by the vendor code */
+#define ZX_GLB_PORT_CLK_EN	0x04001c	/* bit n: clock of MAC port n */
+
+/* Switch core */
+#define ZX_SW_PORT_FILTER(p)	(0x1c0060 + 4 * (p))
+#define  ZX_SW_PORT_FILTER_BITS	GENMASK(25, 23)	/* cleared for every port */
+#define ZX_SW_FWD_CTRL		0x1cc000
+#define  ZX_SW_FWD_CTRL_ON	17
+
+/* Switch port attributes (stock tm.ko "spa") */
+#define ZX_SPA_UP_REG_PKT_EN	0x1d4000
+#define ZX_SPA_DN_REG_PKT_EN	0x1d4040
+#define ZX_SPA_TRAP_DMAC	0x1d41a0	/* 8 bytes per entry */
+#define  ZX_SPA_TRAP_DMAC_NUM	2		/* entries the driver uses */
+#define ZX_SPA_TRAP_PROTO	0x1d41c0
+#define ZX_SPA_TRAP_ETH_TYPE	0x1d41c4	/* 4 x 16 bits, big end first */
+#define ZX_SPA_PORT_EN(i)	(0x1d428c + 4 * (i))
+#define ZX_SPA_ONU_MAC		0x1d4120	/* 16 x 8 bytes: our MACs, routed */
+#define  ZX_SPA_ONU_MAC_NUM	16
+#define ZX_SPA_REG_PKT_TCP_ACK	BIT(4)		/* word 2: TCP without payload */
+
+/* MAC ports 0-4 */
+#define ZX_MAC_BASE		0x200000
+#define ZX_PORT_STRIDE		0x040000
+#define ZX_MAC_PORT(p)		(ZX_MAC_BASE + (p) * ZX_PORT_STRIDE)
+#define ZX_MAC_CTRL		0x000
+#define  ZX_MAC_CTRL_EN		GENMASK(1, 0)	/* receive and transmit */
+#define  ZX_MAC_CTRL_FD		BIT(13)		/* full duplex */
+#define  ZX_MAC_CTRL_100	BIT(14)		/* 100 Mbit/s, with _MII */
+#define  ZX_MAC_CTRL_MII	BIT(15)		/* 10 or 100 Mbit/s */
+
+/* MAC port counters: 32 bits, not cleared by reading */
+#define ZX_MAC_TX_HIST(i)	(0x724 + 4 * (i))	/* frame size bins */
+#define ZX_MAC_TX_MCAST		0x740
+#define ZX_MAC_TX_BCAST		0x744
+#define ZX_MAC_TX_OCTETS	0x764
+#define ZX_MAC_TX_FRAMES	0x768
+#define ZX_MAC_RX_OCTETS	0x784
+#define ZX_MAC_RX_BCAST		0x78c
+#define ZX_MAC_RX_MCAST		0x790
+#define ZX_MAC_RX_HIST(i)	(0x7ac + 4 * (i))
+#define ZX_MAC_RX_UCAST		0x7c4
+#define ZX_MAC_HIST_BINS	6	/* 64, 65-127, ... 1024 bytes and up */
+
+/* Traffic manager */
+#define ZX_TM_BASE		0x340000
+#define ZX_TM_CTRL		(ZX_TM_BASE + 0x00)
+#define  ZX_TM_CTRL_BIT6	BIT(6)		/* set by the vendor code */
+#define ZX_TM_CONFIG_START	(ZX_TM_BASE + 0x04)
+#define ZX_TM_TAB0_ADDR		(ZX_TM_BASE + 0xe8)
+#define ZX_TM_TAB1_ADDR		(ZX_TM_BASE + 0xec)
+#define ZX_TM_BUF_F0		(ZX_TM_BASE + 0xf0)
+#define ZX_TM_BUF_START		(ZX_TM_BASE + 0xf4)
+#define ZX_TM_BUF_END		(ZX_TM_BASE + 0xf8)
+#define ZX_TM_TAB_LIMIT		(ZX_TM_BASE + 0xfc)
+#define ZX_TM_INT_STATUS	(ZX_TM_BASE + 0x100)
+#define ZX_TM_INT_MASK		(ZX_TM_BASE + 0x104)	/* 1: masked */
+#define  ZX_TM_INT_RX		GENMASK_U32(1, 0)		/* frames for the CPU */
+
+/* Traffic manager tables, reached through an indirect access window */
+#define ZX_TM_IND_CMD		(ZX_TM_BASE + 0x4014)
+#define  ZX_TM_IND_INDEX	GENMASK(21, 0)
+#define  ZX_TM_IND_TABLE	GENMASK(26, 22)
+#define  ZX_TM_IND_READ		BIT(27)
+#define ZX_TM_IND_STATUS	(ZX_TM_BASE + 0x4018)
+#define  ZX_TM_IND_READY	BIT(0)
+#define ZX_TM_IND_DATA(i)	(ZX_TM_BASE + 0x401c + 4 * (i))
+#define ZX_TM_TABLE_QUEUE	0
+#define ZX_TM_TABLE_QUEUE_USE	1
+#define ZX_TM_TABLE_RED		2
+#define ZX_TM_TABLE_SCHED	4
+
+/* Release of consumed RX slots */
+#define ZX_RX_REL_CTRL		(ZX_TM_BASE + 0x4064)
+#define  ZX_RX_REL_BUSY		BIT(0)
+#define ZX_RX_REL_DATA		(ZX_TM_BASE + 0x4068)
+#define  ZX_RX_REL_QUEUE	GENMASK(2, 0)
+#define  ZX_RX_REL_CLASS	BIT(3)
+#define  ZX_RX_REL_COUNT	GENMASK(13, 4)
+
+/* Buffer management unit */
+#define ZX_BMU_BASE		(ZX_TM_BASE + 0x8000)
+#define ZX_BMU_CTRL		(ZX_BMU_BASE + 0x00)
+#define  ZX_BMU_CTRL_EN		BIT(0)
+#define ZX_BMU_CFG1		(ZX_BMU_BASE + 0x04)
+#define ZX_BMU_CFG2		(ZX_BMU_BASE + 0x08)
+#define ZX_BMU_BUF_ID		(ZX_BMU_BASE + 0x0c)
+#define  ZX_BMU_BUF_ID_VALID	BIT(31)
+#define  ZX_BMU_BUF_ID_BP	GENMASK(15, 0)
+#define ZX_BMU_BUF_FREE		(ZX_BMU_BASE + 0x10)
+#define ZX_BMU_BUF_ALLOC	(ZX_BMU_BASE + 0x14)
+#define  ZX_BMU_BUF_ALLOC_REQ	BIT(0)
+#define  ZX_BMU_BUF_ALLOC_BUSY	GENMASK(1, 0)
+#define ZX_BMU_FREE_STATUS	(ZX_BMU_BASE + 0xdc)
+#define  ZX_BMU_FREE_SLOTS	GENMASK(8, 3)
+#define ZX_BMU_THRES1		(ZX_BMU_BASE + 0x48)
+#define ZX_BMU_THRES2		(ZX_BMU_BASE + 0x4c)
+#define ZX_BMU_LIMIT1		(ZX_BMU_BASE + 0x58)
+#define ZX_BMU_LIMIT2		(ZX_BMU_BASE + 0x5c)
+
+/* DMA engine between the switch and the CPU */
+#define ZX_DMA_BASE		(ZX_TM_BASE + 0x10000)
+#define ZX_DMA_CTRL		(ZX_DMA_BASE + 0x00)
+#define  ZX_DMA_CTRL_EN		GENMASK(19, 16)
+#define  ZX_DMA_CTRL_BIT21	BIT(21)		/* always set, as in the vendor code */
+#define ZX_DMA_RX_IRQ_TIMER	(ZX_DMA_BASE + 0x30)	/* in 20 ns ticks */
+#define ZX_DMA_RX_IRQ_FRAMES	(ZX_DMA_BASE + 0x34)
+#define ZX_DMA_RING_CFG		(ZX_DMA_BASE + 0x3c)
+#define ZX_DMA_AREA50		(ZX_DMA_BASE + 0x50)	/* 1 MiB area, use unknown */
+#define ZX_DMA_TX_RING		(ZX_DMA_BASE + 0x60)	/* ring of frames to send */
+#define ZX_DMA_TX_START		(ZX_DMA_BASE + 0x64)
+#define ZX_DMA_RX_QUEUE_CNT(q)	(ZX_DMA_BASE + 0x100 + ((q) * 4))
+
+/* Packet processor */
+#define ZX_PP_RESET		0x380000
+
+/* Bridge block (sbrg), ports numbered with ZX_BRPORT() */
+#define ZX_SBRG_PORT_CTRL	0x388004
+#define ZX_SBRG_FLUSH_PORTS	GENMASK(15, 8)	/* drop the entries of these */
+#define ZX_SBRG_FLUSH		BIT(16)
+#define ZX_SBRG_IND_CMD		0x388014	/* 27: read, 26-22: memory, 11-0: entry */
+#define ZX_SBRG_IND_READ	BIT(27)
+#define ZX_SBRG_IND_DONE	0x388018
+#define ZX_SBRG_IND_DATA(i)	(0x38801c + 4 * (i))
+#define ZX_SBRG_TABLE_SEL	0x388184	/* source table: 4 RAMs of 1024/256/512 */
+#define ZX_SBRG_LEARN		0x3881c0	/* ports that learn source MACs */
+#define ZX_SBRG_EGRESS(bp)	(0x3883c0 + 4 * (bp))	/* ports bp may send to */
+#define ZX_SBRG_MEM_VLAN	4		/* 1 word per VID */
+#define ZX_SBRG_VLAN_VALID	BIT(0)
+#define ZX_SBRG_VLAN_UNTAG(bp)	(1 << (2 * (bp) + 1))
+
+/* Packet processor tables, reached through an indirect access window:
+ * command (index | table << 22 | read << 27), status bit 0 = idle, data.
+ * The classifier (CLA) has 17 data words, the packet modifier (PM) 8.
+ */
+#define ZX_CLA_BASE		0x38c000
+#define ZX_PM_BASE		0x39c000
+#define ZX_PP_IND_CMD		0x14
+#define ZX_PP_IND_STATUS	0x18
+#define ZX_PP_IND_DATA		0x1c
+#define ZX_PM_IND_DATA_HI	0x100	/* PM data words 4-7 */
+#define ZX_PP_IND_READ		BIT(27)
+
+#define ZX_CLA_EXTRA_INDEX	0	/* header fields each packet type extracts */
+#define ZX_CLA_EXTRA_RULE	1
+#define ZX_CLA_HASH0		2	/* 256 flow entries, CRC-32 slot */
+#define ZX_CLA_HASH1		3	/* 128 flow entries, CRC-32C slot */
+#define ZX_CLA_AGING		8	/* hit flags, bank 0 then bank 1 */
+#define ZX_PM_FLOW		0	/* rewrite of a flow */
+#define ZX_PM_NEXT_HOP		1	/* new destination IP and MAC */
+#define ZX_PM_CMD		3
+#define ZX_PM_SUB		6
+#define ZX_PM_SRC_MAC		12	/* source MAC of each subnet */
+#define ZX_PP_SNAT_IP(s)	(0x3a0400 + (s) * 4)	/* source IP of each subnet */
+
+#define ZX_PPE_HASH0_SIZE	256
+#define ZX_PPE_HASH_SIZE	(256 + 128)
+/*
+ * The rewrite index in a classifier entry has 7 bits (ZX_CLA_W0_FLOW), and
+ * the stock firmware never used more than 125, so only 127 flows (1-127) can
+ * be offloaded at a time. More would reuse the rewrite of another flow.
+ */
+#define ZX_PPE_FLOWS		128
+#define ZX_PPE_NEXT_HOPS	512
+#define ZX_PPE_SUBNETS		16
+/* IPv4 TCP/UDP: extract index 9 and its rule; hashed with this rule id */
+#define ZX_PPE_V4_INDEX		9
+#define ZX_PPE_V4_RULE		0x98
+
+/* Word 0 of a classifier hash entry */
+#define ZX_CLA_W0_FLOW		GENMASK(31, 25)	/* packet modifier rewrite index */
+#define ZX_CLA_W0_FWD		BIT(24)		/* forward to the egress port */
+#define ZX_CLA_W0_EGRESS	GENMASK(15, 12)	/* bridge port, ZX_BRPORT() */
+#define ZX_CLA_W0_BITS		0x44		/* as in every stock entry */
+
+/* Bridge ports: 0 is the PON side, 1-5 are the switch ports 0-4 */
+#define ZX_BRPORT(p)		((p) + 1)
+
+/* DMA buffer pool: 4096 buffers x 2048 bytes = 8 MiB */
+#define ZX_NUM_BUFFERS		4096
+#define ZX_BUF_SIZE		2048
+#define ZX_BUF_POOL_SIZE	(ZX_NUM_BUFFERS * ZX_BUF_SIZE)
+
+/* Free-index tables (written big-endian u16 per U-Boot convention) */
+#define ZX_TAB0_ENTRIES		ZX_NUM_BUFFERS
+#define ZX_TAB1_ENTRIES		512
+
+/* DMA descriptor rings */
+#define ZX_NUM_TX_DESC		1024
+#define ZX_TX_QUEUE_LIMIT	256
+#define ZX_TX_POLL_US		250	/* see zx_tx_kick() */
+
+/*
+ * The RX interrupt comes after this many frames, or this long after the
+ * first one.  Larger batches cost less per frame: 500 us receive faster
+ * than polling every 1 ms did and still answer sooner.
+ */
+#define ZX_RX_IRQ_FRAMES	64
+#define ZX_RX_IRQ_USECS		500
+#define ZX_NUM_RX_DESC		1024
+#define ZX_DESC_SIZE		16	/* bytes per descriptor */
+
+/* Layout of the 16 MiB DMA arena */
+#define ZX_DMA_ARENA_SIZE	0x1000000
+#define ZX_TAB0_OFFSET		0x800000
+#define ZX_TAB1_OFFSET		0x808000
+#define ZX_RX_OFFSET		0xa00000
+#define ZX_AREA50_OFFSET	0xb00000
+#define ZX_TX_OFFSET		0xc00000
+/* RX ring of queue q at ZX_RX_OFFSET + q * 16 KiB (1024 descriptors) */
+#define ZX_RX_QUEUE_STRIDE	0x4000
+#define ZX_RX_QUEUES		8
+#define ZX_DMA_TX_DONE		(ZX_DMA_BASE + 0x68)
+
+struct zx_eth_adapter;
+
+/* One net_device per switch port: ports 0-3 LAN, port 4 WAN */
+#define ZX_NUM_PORTS		5
+/* TM egress queue of switch port p (as in the vendor driver) */
+#define ZX_PORT_TX_QUEUE(p)	(40 + (p))
+/* MACs the Linux bridge moved away from a switch port, queued for removal */
+#define ZX_SW_FDB_QUEUE		16
+
+/* The MAC counters the driver keeps, see zx_mib_regs[] */
+enum zx_mib {
+	ZX_MIB_TX_OCTETS,
+	ZX_MIB_TX_FRAMES,
+	ZX_MIB_TX_MCAST,
+	ZX_MIB_TX_BCAST,
+	ZX_MIB_TX_HIST,
+	ZX_MIB_RX_OCTETS = ZX_MIB_TX_HIST + ZX_MAC_HIST_BINS,
+	ZX_MIB_RX_UCAST,
+	ZX_MIB_RX_MCAST,
+	ZX_MIB_RX_BCAST,
+	ZX_MIB_RX_HIST,
+	ZX_MIB_NUM = ZX_MIB_RX_HIST + ZX_MAC_HIST_BINS,
+};
+
+struct zx_eth_priv {
+	struct net_device	*netdev;
+	struct zx_eth_adapter	*adapter;
+	int			port;
+	struct phylink		*phylink;
+	struct phylink_config	phylink_config;
+
+	/* MAC counters, summed up into 64 bits (under adapter->mib_lock) */
+	u64			mib[ZX_MIB_NUM];
+	u32			mib_last[ZX_MIB_NUM];	/* last register values */
+};
+
+struct zx_eth_adapter {
+	struct device		*dev;
+	void __iomem		*base;
+	struct napi_struct	napi;
+	struct hrtimer		tx_timer;
+
+	struct net_device	*ports[ZX_NUM_PORTS];
+	int			open_count;
+	int			irq;
+
+	/* DMA arena remains allocated across down/up */
+	void			*buffer_pool;
+	dma_addr_t		buf_pool_dma;
+	void			*tx_ring;
+	dma_addr_t		tx_ring_dma;
+	void			*rx_ring;
+	dma_addr_t		rx_ring_dma;
+	void			*tab0;
+	dma_addr_t		tab0_dma;
+	void			*tab1;
+	dma_addr_t		tab1_dma;
+
+	u32			tx_cur_idx;
+	u32			tx_pending;
+	u32			tx_complete_cnt;
+	u32			tx_done_excess;
+	u32			bmu_free_slots;
+	u32			bmu_alloc_fail;
+	u32			bmu_alloc_late;		/* buffers of timed-out requests */
+	bool			bmu_alloc_pending;
+	u32			bmu_free_fail;
+	u32			rx_cur_idx[ZX_RX_QUEUES];
+	bool			rx_synced[ZX_RX_QUEUES];
+	u32			rx_resync_cnt;
+	u32			rx_alias_cnt;
+	u32			rx_nobuf_cnt;
+	u32			rx_rel_fail;
+	u32			rx_bad_cnt;
+
+	/* TX ring, BMU buffer allocation and the TX counters */
+	spinlock_t		tx_lock;
+	/* BMU free register and bmu_free_slots */
+	spinlock_t		bmu_free_lock;
+
+	/* MAC counters of all ports, read before their 32 bits wrap */
+	struct mutex		mib_lock;
+	struct delayed_work	mib_work;
+
+	/* Hardware flow offload (packet processor NAT) */
+	struct mutex		ppe_lock;
+	bool			ppe_active;
+	int			ppe_users;	/* bound flowtables */
+	struct rhashtable	ppe_flows;
+	DECLARE_BITMAP(ppe_hash_used, ZX_PPE_HASH_SIZE);
+	DECLARE_BITMAP(ppe_flow_used, ZX_PPE_FLOWS);
+	DECLARE_BITMAP(ppe_nh_used, ZX_PPE_NEXT_HOPS);
+	struct {
+		u8	mac[ETH_ALEN];
+		__be32	ip;
+		int	refs;
+	} ppe_subnet[ZX_PPE_SUBNETS];
+	u32			ppe_saved[4];
+	u32			ppe_count;
+	u32			ppe_add_skip;	/* flows the hardware cannot take */
+	u32			ppe_add_fail;	/* no free entry, access timeout */
+
+	/* Hardware LAN switching between the ports of one Linux bridge */
+	spinlock_t		trap_lock;	/* zx_update_traps() */
+	struct mutex		sw_lock;
+	bool			sw_ready;	/* bridge notifications registered */
+	u8			sw_members;	/* bridge ports that switch */
+	struct work_struct	sw_work;
+	spinlock_t		sw_fdb_lock;	/* the fields below */
+	struct net_device	*sw_bridge[ZX_NUM_PORTS];
+	bool			sw_recompute;
+	u8			sw_flush;	/* bridge ports to flush */
+	bool			sw_fdb_overflow;
+	unsigned int		sw_fdb_count;
+	u8			sw_fdb_mac[ZX_SW_FDB_QUEUE][ETH_ALEN];
+	u32			sw_fdb_deleted;
+	struct notifier_block	sw_netdev_nb;
+	struct notifier_block	sw_switchdev_nb;
+	struct notifier_block	sw_switchdev_blocking_nb;
+};
+
+/* zx279128s-main.c */
+bool zx_eth_is_port(const struct net_device *dev);
+void zx_set_trap_dmac(struct zx_eth_adapter *adapter, int idx, const u8 *mac);
+
+/* zx279128s-ppe.c */
+extern const struct rhashtable_params zx_ppe_ht_params;
+void zx_update_traps(struct zx_eth_adapter *adapter);
+void zx_update_mac_tables(struct zx_eth_adapter *adapter);
+int zx_eth_setup_tc(struct net_device *dev, enum tc_setup_type type,
+		    void *type_data);
+
+/* zx279128s-switch.c */
+void zx_sw_init(struct zx_eth_adapter *adapter);
+void zx_sw_fini(struct zx_eth_adapter *adapter);
+void zx_sw_work(struct work_struct *work);
+void zx_sw_port_down(struct zx_eth_adapter *adapter, int port);
+
+#endif
diff --git a/drivers/net/ethernet/zte/zx279128s-main.c b/drivers/net/ethernet/zte/zx279128s-main.c
new file mode 100644
index 0000000000..45a0dc1363
--- /dev/null
+++ b/drivers/net/ethernet/zte/zx279128s-main.c
@@ -0,0 +1,1502 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ZTE zx279128s Ethernet switch and DMA driver
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ *
+ * The zx279128s has a five-port gigabit switch with a traffic manager (TM),
+ * a buffer management unit (BMU) and a DMA engine that moves frames between
+ * the switch and the CPU. Every port is its own net_device (lan1-lan4 and wan
+ * on the ZTE ZXHN H3600). Ports in the same Linux bridge switch known unicast
+ * in hardware (zx279128s-switch.c), and established flows can be offloaded to
+ * the packet processor (zx279128s-ppe.c).
+ *
+ * All frames pass through buffers of the BMU pool, which lives in a 16 MiB
+ * DMA arena that is allocated once at probe and kept. Received frames are
+ * copied out of their buffer into an skb, frames to send are copied into a
+ * buffer. The ingress port of a received frame is in its descriptor, and a
+ * frame to send goes to the TM queue of its port. The hardware tables are
+ * programmed from scratch, with the values of the vendor firmware.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/debugfs.h>
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/etherdevice.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_net.h>
+#include <linux/phy.h>
+#include <linux/platform_device.h>
+#include <linux/rtnetlink.h>
+#include <linux/seq_file.h>
+
+#include "zx279128s-eth.h"
+
+/* ---- BMU helpers --------------------------------------------------- */
+
+/*
+ * Allocate a buffer from the BMU (caller holds tx_lock).  A request that
+ * does not complete in time stays with the BMU, which hands out its
+ * buffer later: keep it pending and collect that buffer on the next call,
+ * as the vendor driver does.  Starting a new request instead lost one
+ * buffer for good with every timeout.
+ */
+static int zx_bmu_alloc_bp(struct zx_eth_adapter *adapter)
+{
+	void __iomem *base = adapter->base;
+	u32 val;
+	int ret;
+
+	if (adapter->bmu_alloc_pending) {
+		if (readl(base + ZX_BMU_BUF_ALLOC) & ZX_BMU_BUF_ALLOC_REQ) {
+			adapter->bmu_alloc_fail++;
+			return -EBUSY;
+		}
+		adapter->bmu_alloc_pending = false;
+		val = readl(base + ZX_BMU_BUF_ID);
+		if (val & ZX_BMU_BUF_ID_VALID) {
+			adapter->bmu_alloc_late++;
+			return FIELD_GET(ZX_BMU_BUF_ID_BP, val);
+		}
+	}
+
+	val = readl(base + ZX_BMU_BUF_ALLOC);
+	writel(val | ZX_BMU_BUF_ALLOC_REQ, base + ZX_BMU_BUF_ALLOC);
+	ret = readl_poll_timeout_atomic(base + ZX_BMU_BUF_ALLOC, val,
+					!(val & ZX_BMU_BUF_ALLOC_BUSY), 1, 1000);
+	if (!ret) {
+		val = readl(base + ZX_BMU_BUF_ID);
+		if (val & ZX_BMU_BUF_ID_VALID)
+			return FIELD_GET(ZX_BMU_BUF_ID_BP, val);
+		ret = -ENOSPC;
+	}
+
+	adapter->bmu_alloc_pending = true;
+	adapter->bmu_alloc_fail++;
+	return ret;
+}
+
+static int zx_bmu_free_bp(struct zx_eth_adapter *adapter, u16 bp)
+{
+	u32 val;
+	int ret = 0;
+
+	spin_lock_bh(&adapter->bmu_free_lock);
+	if (!adapter->bmu_free_slots) {
+		ret = readl_poll_timeout_atomic(adapter->base + ZX_BMU_FREE_STATUS,
+						val, val & ZX_BMU_FREE_SLOTS, 1, 5000);
+		if (ret) {
+			adapter->bmu_free_fail++;
+			goto out;
+		}
+		adapter->bmu_free_slots = FIELD_GET(ZX_BMU_FREE_SLOTS, val);
+	}
+
+	adapter->bmu_free_slots--;
+	writel(bp, adapter->base + ZX_BMU_BUF_FREE);
+out:
+	spin_unlock_bh(&adapter->bmu_free_lock);
+	return ret;
+}
+
+/* ---- RX descriptor release ----------------------------------------- */
+
+/*
+ * Hand @n consumed slots of RX queue @q back to the hardware with one
+ * command, as the vendor driver does (count in bits 13:4, ring B), instead
+ * of two register polls and two writes per frame.
+ *
+ * The traffic manager counts each frame it hands to the CPU against the
+ * class in descriptor byte 6 bit 0, and a release only returns slots of the
+ * class it names.  Releasing a class-1 slot as class 0 leaves the frame
+ * counted in the CPU queue for good: after 1024 frames the queue's reserved
+ * space is used up.  So count the slots per class and release each class
+ * separately, as the vendor driver does.
+ */
+static int zx_rx_release(struct zx_eth_adapter *adapter, int q, int class,
+			 u32 n)
+{
+	u32 val;
+	int ret;
+
+	ret = readl_poll_timeout_atomic(adapter->base + ZX_RX_REL_CTRL,
+					val, !(val & ZX_RX_REL_BUSY), 1, 5000);
+	if (ret)
+		return ret;
+	writel(FIELD_PREP(ZX_RX_REL_COUNT, n) | FIELD_PREP(ZX_RX_REL_CLASS, class) |
+	       FIELD_PREP(ZX_RX_REL_QUEUE, q), adapter->base + ZX_RX_REL_DATA);
+	writel(ZX_RX_REL_BUSY, adapter->base + ZX_RX_REL_CTRL);
+	return 0;
+}
+
+/*
+ * Clear a consumed descriptor.  All cleared descriptors must be visible
+ * before their slots are released: with a full ring the hardware refills a
+ * slot as soon as it is released, and clearing it afterwards would wipe
+ * the new descriptor.
+ */
+static void zx_rx_clear_desc(__le32 *desc)
+{
+	desc[0] = 0; desc[1] = 0; desc[2] = 0; desc[3] = 0;
+}
+
+/* Count a consumed slot for its release class, then clear it. */
+static void zx_rx_consume(__le32 *desc, u32 rel[2])
+{
+	rel[((u8 *)desc)[6] & 1]++;
+	zx_rx_clear_desc(desc);
+}
+
+/* ---- MAC port helpers ---------------------------------------------- */
+
+static void zx_port_set_speed(struct zx_eth_adapter *adapter, int port,
+			      int speed, int duplex)
+{
+	void __iomem *reg = adapter->base + ZX_MAC_PORT(port) + ZX_MAC_CTRL;
+	u32 val = readl(reg);
+
+	if (speed == SPEED_1000) {
+		val &= ~(ZX_MAC_CTRL_MII | ZX_MAC_CTRL_100);
+		val |= ZX_MAC_CTRL_FD;
+	} else {
+		if (duplex == DUPLEX_FULL) {
+			val |= ZX_MAC_CTRL_MII | ZX_MAC_CTRL_FD;
+		} else {
+			val &= ~ZX_MAC_CTRL_FD;
+			val |= ZX_MAC_CTRL_MII;
+		}
+		if (speed == SPEED_100)
+			val |= ZX_MAC_CTRL_100;
+		else
+			val &= ~ZX_MAC_CTRL_100;
+	}
+	writel(val, reg);
+}
+
+static void zx_port_enable(struct zx_eth_adapter *adapter, int port, bool enable)
+{
+	void __iomem *reg = adapter->base + ZX_MAC_PORT(port) + ZX_MAC_CTRL;
+	u32 val = readl(reg);
+
+	if (enable)
+		val |= ZX_MAC_CTRL_EN;
+	else
+		val &= ~ZX_MAC_CTRL_EN;
+	writel(val, reg);
+}
+
+/* ---- PHY link callbacks -------------------------------------------- */
+
+static struct zx_eth_priv *zx_config_to_priv(struct phylink_config *config)
+{
+	return netdev_priv(to_net_dev(config->dev));
+}
+
+static void zx_mac_config(struct phylink_config *config, unsigned int mode,
+			  const struct phylink_link_state *state)
+{
+	/* the MAC follows the link in zx_mac_link_up() only */
+}
+
+static void zx_mac_link_down(struct phylink_config *config, unsigned int mode,
+			     phy_interface_t interface)
+{
+	struct zx_eth_priv *priv = zx_config_to_priv(config);
+
+	zx_port_enable(priv->adapter, priv->port, false);
+	zx_sw_port_down(priv->adapter, priv->port);
+}
+
+static void zx_mac_link_up(struct phylink_config *config,
+			   struct phy_device *phy, unsigned int mode,
+			   phy_interface_t interface, int speed, int duplex,
+			   bool tx_pause, bool rx_pause)
+{
+	struct zx_eth_priv *priv = zx_config_to_priv(config);
+
+	zx_port_set_speed(priv->adapter, priv->port, speed, duplex);
+	zx_port_enable(priv->adapter, priv->port, true);
+}
+
+static const struct phylink_mac_ops zx_phylink_mac_ops = {
+	.mac_config	= zx_mac_config,
+	.mac_link_down	= zx_mac_link_down,
+	.mac_link_up	= zx_mac_link_up,
+};
+
+/*
+ * Frames addressed to one of our MAC addresses go to the CPU, whatever
+ * their type (the ethertype traps below cover the common ones).
+ */
+void zx_set_trap_dmac(struct zx_eth_adapter *adapter, int idx, const u8 *mac)
+{
+	writel(mac[2] << 24 | mac[3] << 16 | mac[4] << 8 | mac[5],
+	       adapter->base + ZX_SPA_TRAP_DMAC + idx * 8);
+	writel(mac[0] << 8 | mac[1], adapter->base + ZX_SPA_TRAP_DMAC + idx * 8 + 4);
+}
+
+static void zx_dma_enable(struct zx_eth_adapter *adapter, bool enable)
+{
+	u32 val = readl(adapter->base + ZX_DMA_CTRL);
+
+	val &= ~ZX_DMA_CTRL_EN;
+	val |= ZX_DMA_CTRL_BIT21 | (enable ? ZX_DMA_CTRL_EN : 0);
+	writel(val, adapter->base + ZX_DMA_CTRL);
+	readl(adapter->base + ZX_DMA_CTRL);
+}
+
+static int zx_tm_write(struct zx_eth_adapter *adapter, u32 table, u32 index,
+		       const u32 data[4])
+{
+	void __iomem *base = adapter->base;
+	u32 val;
+	int i, ret;
+
+	ret = readl_poll_timeout(base + ZX_TM_IND_STATUS, val,
+				 val & ZX_TM_IND_READY, 1, 1000);
+	if (ret)
+		return ret;
+	writel(FIELD_PREP(ZX_TM_IND_INDEX, index) |
+	       FIELD_PREP(ZX_TM_IND_TABLE, table), base + ZX_TM_IND_CMD);
+	for (i = 3; i >= 0; i--)
+		writel(data[i], base + ZX_TM_IND_DATA(i));
+	return 0;
+}
+
+static int zx_tm_read0(struct zx_eth_adapter *adapter, u32 table, u32 index,
+		       u32 *data)
+{
+	void __iomem *base = adapter->base;
+	u32 val;
+	int ret;
+
+	ret = readl_poll_timeout(base + ZX_TM_IND_STATUS, val,
+				 val & ZX_TM_IND_READY, 1, 1000);
+	if (ret)
+		return ret;
+	writel(FIELD_PREP(ZX_TM_IND_INDEX, index) |
+	       FIELD_PREP(ZX_TM_IND_TABLE, table) | ZX_TM_IND_READ,
+	       base + ZX_TM_IND_CMD);
+	ret = readl_poll_timeout(base + ZX_TM_IND_STATUS, val,
+				 val & ZX_TM_IND_READY, 1, 1000);
+	if (ret)
+		return ret;
+	*data = readl(base + ZX_TM_IND_DATA(0));
+	return 0;
+}
+
+/* Queue, RED and scheduler tables.  The RED profiles use the vendor
+ * pon_tm_red_init() value.
+ *
+ * Queue word 0 is the reserved space (bits 10:0) plus the shared space
+ * (bits 25:11), in buffers.  Queues 0-15 feed the CPU.  They get 1023
+ * reserved and no shared buffers, as in the stock firmware: a frame stays
+ * counted in its queue until the driver releases its RX slot, so the
+ * traffic manager never has more than 1023 frames in the 1024-slot RX
+ * ring and drops the excess itself.  With more, a burst faster than the
+ * driver drains the ring makes the DMA overwrite slots that were not read
+ * yet, and those frames and their buffers are lost.
+ *
+ * The queue usage in table 1 survives the reset.  U-Boot receives a TFTP
+ * download on CPU queue 0 and never returns the slots, so after a TFTP
+ * boot the queue starts with up to 1024 frames counted that will never be
+ * released, and a 1023 limit would drop every frame.  Raise the limit of
+ * each CPU queue by what is left counted in it.
+ */
+static int zx_tm_init(struct zx_eth_adapter *adapter)
+{
+	static const u32 queue[4] = { 0x00800400, 0, 0, 0 };
+	static const u32 red[4] = { 0x00200020, 0, 0, 0 };
+	static const u32 scheduler[4] = {
+		0xff803fff, 0x0100ff80, 0x00100200, 32
+	};
+	u32 cpu_queue[4] = { 0, 0, 0, 0 };
+	u32 used, limit;
+	int i, ret;
+
+	for (i = 0; i < 16; i++) {
+		ret = zx_tm_read0(adapter, ZX_TM_TABLE_QUEUE_USE, i, &used);
+		if (ret)
+			return ret;
+		used = (used & 0x7ff) + ((used >> 11) & 0x7fff);
+		if (used)
+			dev_dbg(adapter->dev,
+				"CPU queue %d: %u frames left counted by the boot loader\n",
+				i, used);
+		limit = 1023 + used;
+		cpu_queue[0] = min(limit, 0x7ffU) | (limit - min(limit, 0x7ffU)) << 11;
+		ret = zx_tm_write(adapter, ZX_TM_TABLE_QUEUE, i, cpu_queue);
+		if (ret)
+			return ret;
+	}
+	for (i = 16; i < 400; i++) {
+		ret = zx_tm_write(adapter, ZX_TM_TABLE_QUEUE, i, queue);
+		if (ret)
+			return ret;
+	}
+	for (i = 0; i < 384; i++) {
+		ret = zx_tm_write(adapter, ZX_TM_TABLE_RED, i, red);
+		if (ret)
+			return ret;
+		ret = zx_tm_write(adapter, ZX_TM_TABLE_SCHED, i, scheduler);
+		if (ret)
+			return ret;
+	}
+	return 0;
+}
+
+/* ---- Hardware initialisation --------------------------------------- */
+
+static void zx_pp_init(struct zx_eth_adapter *adapter)
+{
+	void __iomem *base = adapter->base;
+
+	/* Mode-2 TX goes through the packet processor bridge.  The bootloader
+	 * initializes it for TFTP, but not when booting directly from NAND.
+	 * Initialize it ourselves, while DMA and the MACs are quiesced.  These
+	 * are the H3600 bootloader's PP reset, bridge and classifier settings.
+	 */
+	writel(2, base + ZX_PP_RESET);
+	usleep_range(1000, 2000);
+
+	writel(0x000200ff, base + ZX_SBRG_PORT_CTRL);
+	/* Stock firmware value.  With the reset value 0x00010000 the bridge
+	 * steered IPv6 unicast arriving on WAN to a queue the driver never
+	 * services, and each frame kept a BMU buffer: about one leaked
+	 * buffer per background WAN frame on an IPv6 network.
+	 */
+	writel(0x0000dfdf, base + 0x388008);
+	/* The bridge would forward a frame between ports only through VLAN
+	 * table entries, which the driver does not program, so it drops what
+	 * the traps leave.  Frames between two LAN ports must reach the Linux
+	 * bridge instead.  Turn off source address learning, so that every
+	 * unicast destination is unknown, and send unknown unicast arriving on
+	 * a switch port (bridge ports 1-5) to the CPU.  Without this, lifting
+	 * the IPv4 trap for flow offload loses IPv4 between two LAN hosts.
+	 */
+	writel(0x00000000, base + ZX_SBRG_LEARN);
+	writel(0xff5ffdff, base + 0x388340);
+	writel(0x0000003e, base + 0x388344);
+	writel(0x0000003f, base + 0x388380);
+	writel(0xaaaaaaaa, base + 0x38863c);
+	writel(0x00005555, base + 0x3881c4);
+	writel(0x000bf874, base + 0x388188);
+	writel(0x000000ff, base + 0x3882c0);
+	writel(0x0000ffff, base + 0x388300);
+	writel(0x0000003e, base + 0x388304);
+
+	writel(0x0000309a, base + 0x38c080);
+	writel(0, base + 0x38c088);
+	writel(1, base + 0x38c0cc);
+	writel(0, base + 0x3a0010);
+	writel(0, base + 0x3a0014);
+}
+
+/* Reset the switch and leave its ports and DMA stopped */
+static void zx_hw_reset(struct zx_eth_adapter *adapter)
+{
+	void __iomem *base = adapter->base;
+	int i;
+
+	/* Master reset pulse */
+	writel(0, base + ZX_GLB_RESET);
+	fsleep(10);
+	writel(0xffffffff, base + ZX_GLB_RESET);
+
+	/* Quiesce all 5 ports */
+	for (i = 0; i < ZX_NUM_PORTS; i++)
+		zx_port_enable(adapter, i, false);
+	zx_dma_enable(adapter, false);
+	writel(0, base + ZX_BMU_CTRL);
+}
+
+static int zx_hw_init(struct zx_eth_adapter *adapter)
+{
+	void __iomem *base = adapter->base;
+	static const int port_to_sw_idx[8] = { 0, 1, 2, 3, 4, -1, 5, 6 };
+	__be16 *tab;
+	u32 val;
+	int i, ret;
+
+	/* 0. Reset, interrupts off until a port is opened */
+	zx_hw_reset(adapter);
+	writel(~0, base + ZX_TM_INT_MASK);
+	msleep(20);
+
+	/* 1. Clock gating: enable the clocks of MAC ports 0-4 */
+	writel(readl(base + ZX_GLB_CLK_CTRL) & ~ZX_GLB_CLK_CTRL_BITS,
+	       base + ZX_GLB_CLK_CTRL);
+	writel(GENMASK(ZX_NUM_PORTS - 1, 0), base + ZX_GLB_PORT_CLK_EN);
+	zx_pp_init(adapter);
+
+	/* 2. Free-buffer index tables */
+	tab = adapter->tab0;
+	for (i = 0; i < ZX_TAB0_ENTRIES; i++)
+		tab[i] = cpu_to_be16(i);
+
+	tab = adapter->tab1;
+	for (i = 0; i < ZX_TAB1_ENTRIES; i++)
+		tab[i] = cpu_to_be16(i);
+
+	dma_wmb();
+	ret = zx_tm_init(adapter);
+	if (ret)
+		return dev_err_probe(adapter->dev, ret, "TM table setup timed out\n");
+
+	/* 3. Traffic Manager buffer pool */
+	writel((u32)adapter->tab0_dma, base + ZX_TM_TAB0_ADDR);
+	writel((u32)adapter->tab1_dma, base + ZX_TM_TAB1_ADDR);
+	writel(0x08000800, base + ZX_TM_TAB_LIMIT);
+	writel((u32)adapter->buf_pool_dma, base + ZX_TM_BUF_START);
+	writel((u32)adapter->buf_pool_dma + ZX_BUF_POOL_SIZE, base + ZX_TM_BUF_END);
+	writel((u32)adapter->rx_ring_dma, base + ZX_TM_BUF_F0);
+	writel(16, base + ZX_TM_CONFIG_START);
+
+	/* DMA and TM control values from the vendor pon_tm_dma_init() and
+	 * the stock firmware, programmed before the BMU is configured.
+	 */
+	writel(1,      base + ZX_DMA_BASE + 0x28);
+	writel(1,      base + ZX_DMA_BASE + 0x2c);
+	writel(ZX_RX_IRQ_USECS * 50, base + ZX_DMA_RX_IRQ_TIMER);
+	writel(ZX_RX_IRQ_FRAMES, base + ZX_DMA_RX_IRQ_FRAMES);
+	writel(0x7f,   base + ZX_DMA_BASE + 0x04);
+	writel(32,     base + ZX_DMA_BASE + 0x20);
+	writel(32,     base + ZX_DMA_BASE + 0x24);
+	/* The bootloader leaves 0x00131213 here, the stock firmware runs with
+	 * 0x00131217.
+	 */
+	writel(0x00131217, base + ZX_DMA_BASE + 0x388);
+	writel(readl(base + ZX_TM_CTRL) | ZX_TM_CTRL_BIT6, base + ZX_TM_CTRL);
+
+	/* 4. Buffer Management Unit */
+	writel(0,          base + ZX_BMU_CTRL);
+	writel(0x0104c040, base + ZX_BMU_CFG1);
+	writel(0x0104c040, base + ZX_BMU_CFG2);
+	/* Pool size in units of 32 buffers.  With a 2048-buffer pool the BMU
+	 * never fills its external free list (BPPE) after a cold NAND boot and
+	 * runs on its ~64-entry internal cache only.  4096 works.
+	 */
+	writel(ZX_NUM_BUFFERS / 32 - 1, base + ZX_BMU_LIMIT1);
+	writel(15,         base + ZX_BMU_LIMIT2);
+	writel(ZX_NUM_BUFFERS << 16, base + ZX_BMU_THRES1);
+	writel(0x2000000,  base + ZX_BMU_THRES2);
+	writel(0,          base + ZX_BMU_BUF_ALLOC);
+	writel(ZX_BMU_CTRL_EN, base + ZX_BMU_CTRL);
+
+	writel(17, base + 0x3a00e0);
+
+	/* 5. DMA descriptor rings */
+	memset(adapter->tx_ring, 0, ZX_NUM_TX_DESC * ZX_DESC_SIZE);
+	memset(adapter->rx_ring, 0, ZX_RX_QUEUES * ZX_RX_QUEUE_STRIDE);
+	adapter->tx_cur_idx = 0;
+	for (i = 0; i < ZX_RX_QUEUES; i++) {
+		adapter->rx_cur_idx[i] = 0;
+		adapter->rx_synced[i] = false;
+	}
+	adapter->tx_pending = 0;
+	adapter->tx_complete_cnt = 0;
+	adapter->tx_done_excess = 0;
+	adapter->bmu_free_slots = 0;
+
+	writel((u32)adapter->buf_pool_dma + ZX_AREA50_OFFSET, base + ZX_DMA_AREA50);
+	writel((u32)adapter->tx_ring_dma, base + ZX_DMA_TX_RING);
+	writel(0x00400040,             base + ZX_DMA_RING_CFG);
+	readl(base + ZX_DMA_TX_DONE);
+
+	dma_wmb();
+
+	/* 6. Switch: enable forwarding */
+	writel(ZX_SW_FWD_CTRL_ON, base + ZX_SW_FWD_CTRL);
+
+	for (i = 0; i < 8; i++) {
+		int idx = port_to_sw_idx[i];
+
+		if (idx >= 0)
+			writel(1, base + ZX_SPA_PORT_EN(idx));
+	}
+
+	/*
+	 * Enable every class of packets the switch can hand to the CPU, up
+	 * and down direction, as the stock firmware does.  With only a few
+	 * classes enabled, IPv6 neighbour discovery never reached the CPU.
+	 */
+	writel(0xffffffff, base + ZX_SPA_UP_REG_PKT_EN);
+	writel(0xffffffff, base + ZX_SPA_UP_REG_PKT_EN + 4);
+	writel(0xffffffff, base + ZX_SPA_DN_REG_PKT_EN);
+	writel(0xffffffff, base + ZX_SPA_DN_REG_PKT_EN + 4);
+
+	/*
+	 * The switch never forwards from port to port and hands the CPU only
+	 * the frames a trap matches; the Linux bridge forwards between the
+	 * port netdevs.  So trap the common ethertypes whatever their
+	 * destination (IPv4, IPv6, ARP, VLAN tagged).  This also covers what
+	 * the IPv4 protocol trap (TCP, UDP, ICMP) did, so clear that one.
+	 */
+	writel(ETH_P_IP << 16 | ETH_P_IPV6, base + ZX_SPA_TRAP_ETH_TYPE);
+	writel(ETH_P_ARP << 16 | ETH_P_8021Q, base + ZX_SPA_TRAP_ETH_TYPE + 4);
+	writel(0, base + ZX_SPA_TRAP_PROTO);
+
+	/* Clear port filter bits for ports 0-4 */
+	for (i = 0; i < ZX_NUM_PORTS; i++) {
+		val = readl(base + ZX_SW_PORT_FILTER(i));
+		val &= ~ZX_SW_PORT_FILTER_BITS;
+		writel(val, base + ZX_SW_PORT_FILTER(i));
+	}
+
+	/* 7. MAC ports 0-4 */
+	for (i = 0; i < ZX_NUM_PORTS; i++) {
+		void __iomem *pb = base + ZX_MAC_PORT(i);
+
+		writel(0x00bbe000, pb + ZX_MAC_CTRL);
+		writel(0x80000001, pb + 0x08);
+		writel(0x0000fffe, pb + 0x04);
+		writel(0x00011200, pb + 0xe0);
+		writel(50,         pb + 0xd00);
+		writel(168,        pb + 0xd30);
+		writel(0x00300002, pb + 0x70);
+		writel(0x4000,     pb + 0xb4);
+		writel(0x0010ff11, pb + 0xb00);
+
+		zx_port_set_speed(adapter, i, SPEED_1000, DUPLEX_FULL);
+		zx_port_enable(adapter, i, false);
+	}
+
+	return 0;
+}
+
+/*
+ * No interrupt reports sent frames.  They are reclaimed on every transmit
+ * and in the NAPI poll; while a full ring keeps the queues stopped, a timer
+ * runs the poll until they can be woken again.  Called with tx_lock held.
+ */
+static void zx_tx_kick(struct zx_eth_adapter *adapter)
+{
+	if (!hrtimer_active(&adapter->tx_timer))
+		hrtimer_start(&adapter->tx_timer, us_to_ktime(ZX_TX_POLL_US),
+			      HRTIMER_MODE_REL);
+}
+
+static void zx_tx_reclaim(struct zx_eth_adapter *adapter)
+{
+	u32 done = readl(adapter->base + ZX_DMA_TX_DONE) & 0xffff;
+	u32 n = min(done, adapter->tx_pending);
+
+	if (unlikely(done > adapter->tx_pending))
+		adapter->tx_done_excess += done - adapter->tx_pending;
+	adapter->tx_complete_cnt += n;
+	adapter->tx_pending -= n;
+	if (adapter->tx_pending < ZX_TX_QUEUE_LIMIT) {
+		int p;
+
+		for (p = 0; p < ZX_NUM_PORTS; p++)
+			if (adapter->ports[p] && netif_queue_stopped(adapter->ports[p]))
+				netif_wake_queue(adapter->ports[p]);
+	}
+}
+
+/* ---- TX path ------------------------------------------------------- */
+
+static netdev_tx_t zx_eth_xmit(struct sk_buff *skb, struct net_device *netdev)
+{
+	struct zx_eth_priv *priv = netdev_priv(netdev);
+	struct zx_eth_adapter *adapter = priv->adapter;
+	void __iomem *base = adapter->base;
+	unsigned int len = skb->len;
+	__le32 *desc;
+	u32 tx_idx;
+	u8 *buf_ptr;
+	int bp;
+
+	if (len > 1536) {
+		dev_kfree_skb_any(skb);
+		dev_dstats_tx_dropped(netdev);
+		return NETDEV_TX_OK;
+	}
+
+	spin_lock_bh(&adapter->tx_lock);
+	zx_tx_reclaim(adapter);
+	/*
+	 * The ports share the ring.  The queues are stopped as soon as it is
+	 * full, but another port's transmit may already be waiting for the
+	 * lock: give its frame back to the stack.
+	 */
+	if (adapter->tx_pending >= ZX_TX_QUEUE_LIMIT) {
+		netif_stop_queue(netdev);
+		zx_tx_kick(adapter);
+		spin_unlock_bh(&adapter->tx_lock);
+		return NETDEV_TX_BUSY;
+	}
+	if (skb_put_padto(skb, ETH_ZLEN)) {
+		spin_unlock_bh(&adapter->tx_lock);
+		dev_dstats_tx_dropped(netdev);
+		return NETDEV_TX_OK;
+	}
+	len = skb->len;
+	bp = zx_bmu_alloc_bp(adapter);
+	if (bp < 0 || bp >= ZX_NUM_BUFFERS) {
+		spin_unlock_bh(&adapter->tx_lock);
+		dev_dstats_tx_dropped(netdev);
+		dev_kfree_skb_any(skb);
+		return NETDEV_TX_OK;
+	}
+
+	buf_ptr = (u8 *)adapter->buffer_pool + ((u32)bp * ZX_BUF_SIZE) + 16;
+	if (skb_copy_bits(skb, 0, buf_ptr, len)) {
+		zx_bmu_free_bp(adapter, bp);
+		spin_unlock_bh(&adapter->tx_lock);
+		dev_dstats_tx_dropped(netdev);
+		dev_kfree_skb_any(skb);
+		return NETDEV_TX_OK;
+	}
+
+	tx_idx = adapter->tx_cur_idx % ZX_NUM_TX_DESC;
+	desc = (__le32 *)adapter->tx_ring + tx_idx * (ZX_DESC_SIZE / sizeof(*desc));
+
+	/*
+	 * Directed egress to the port's TM queue.  The switch does not
+	 * forward between ports: the Linux bridge does, one netdev per port.
+	 */
+	desc[0] = cpu_to_le32(0x80 | (ZX_PORT_TX_QUEUE(priv->port) << 20));
+	desc[3] = cpu_to_le32(0x3 | ((len & 0x3fff) << 2));
+	desc[1] = cpu_to_le32(0x10000);
+	desc[2] = cpu_to_le32(0x21000000 | ((len & 0x3fff) << 9));
+
+	((u8 *)desc)[7] = (u8)((bp & 0x7f) << 1);
+	((u8 *)desc)[8] = (u8)(bp >> 7);
+
+	dma_wmb();
+	writel(1, base + ZX_DMA_TX_START);
+
+	adapter->tx_cur_idx++;
+	adapter->tx_pending++;
+	if (adapter->tx_pending >= ZX_TX_QUEUE_LIMIT) {
+		int p;
+
+		for (p = 0; p < ZX_NUM_PORTS; p++)
+			if (adapter->ports[p])
+				netif_stop_queue(adapter->ports[p]);
+		zx_tx_kick(adapter);
+	}
+	spin_unlock_bh(&adapter->tx_lock);
+	dev_dstats_tx_add(netdev, len);
+	dev_kfree_skb_any(skb);
+	return NETDEV_TX_OK;
+}
+
+/* ---- RX path ------------------------------------------------------- */
+
+static bool zx_is_own_addr(struct zx_eth_adapter *adapter, const u8 *addr)
+{
+	int i;
+
+	for (i = 0; i < ZX_NUM_PORTS; i++)
+		if (adapter->ports[i] &&
+		    ether_addr_equal(adapter->ports[i]->dev_addr, addr))
+			return true;
+	return false;
+}
+
+static int zx_eth_rx(struct zx_eth_adapter *adapter, int budget)
+{
+	int work_done = 0, q;
+
+	for (q = ZX_RX_QUEUES - 1; q >= 0 && work_done < budget; q--) {
+		u32 avail = readl(adapter->base + ZX_DMA_RX_QUEUE_CNT(q)) & 0xffff;
+		u32 rel[2] = { 0, 0 };
+
+		while (avail && work_done < budget) {
+			u32 idx = adapter->rx_cur_idx[q] % ZX_NUM_RX_DESC;
+			__le32 *desc = adapter->rx_ring + q * ZX_RX_QUEUE_STRIDE +
+				       idx * ZX_DESC_SIZE;
+			u16 bp, len;
+			int port;
+			struct sk_buff *skb;
+			struct net_device *dest_dev;
+
+			dma_rmb();
+			bp = (((u8 *)desc)[7] >> 1) | ((u16)((u8 *)desc)[8] << 7);
+			len = (le32_to_cpu(desc[3]) >> 2) & 0x3fff;
+			port = (((u8 *)desc)[6] >> 3) ? ((((u8 *)desc)[6] >> 3) - 1) : 0;
+
+			if (bp >= ZX_NUM_BUFFERS || len < ETH_HLEN || len > 1536) {
+				u32 scan, idx2, found = 0;
+
+				if (adapter->rx_synced[q]) {
+					/*
+					 * The hardware counted this slot but it
+					 * holds no frame: it can leave a slot
+					 * empty (e.g. when it had no buffer).
+					 * Consume it as the vendor driver does.
+					 * Waiting for it would leave our index
+					 * behind the hardware's for good.
+					 */
+					adapter->rx_bad_cnt++;
+					if (net_ratelimit())
+						dev_warn(adapter->dev,
+							 "rxq%d: skipping slot %u (%08x %08x %08x %08x), %u pending\n",
+							 q, idx, le32_to_cpu(desc[0]),
+							 le32_to_cpu(desc[1]),
+							 le32_to_cpu(desc[2]),
+							 le32_to_cpu(desc[3]), avail);
+					zx_rx_consume(desc, rel);
+					adapter->rx_cur_idx[q]++;
+					work_done++;
+					avail--;
+					if (bp && bp < ZX_NUM_BUFFERS &&
+					    zx_bmu_free_bp(adapter, bp))
+						adapter->bmu_free_fail++;
+					continue;
+				}
+
+				for (scan = 1; scan < ZX_NUM_RX_DESC; scan++) {
+					__le32 *d2;
+					u16 bp2, len2;
+
+					idx2 = (idx + scan) % ZX_NUM_RX_DESC;
+					d2 = adapter->rx_ring + q * ZX_RX_QUEUE_STRIDE +
+					     idx2 * ZX_DESC_SIZE;
+					dma_rmb();
+					bp2 = (((u8 *)d2)[7] >> 1) |
+					      ((u16)((u8 *)d2)[8] << 7);
+					len2 = (le32_to_cpu(d2[3]) >> 2) & 0x3fff;
+					if (bp2 < ZX_NUM_BUFFERS &&
+					    len2 >= ETH_HLEN && len2 <= 1536) {
+						found = 1;
+						break;
+					}
+				}
+				if (!found)
+					break;
+				adapter->rx_cur_idx[q] = idx2;
+				adapter->rx_resync_cnt++;
+				continue;
+			}
+			adapter->rx_synced[q] = true;
+
+			/* Aliasing detector: frames from one of our own addresses */
+			if (len >= ETH_HLEN) {
+				u8 *d = adapter->buffer_pool + bp * ZX_BUF_SIZE + 16;
+
+				if (zx_is_own_addr(adapter, d + ETH_ALEN)) {
+					adapter->rx_alias_cnt++;
+					zx_rx_consume(desc, rel);
+					adapter->rx_cur_idx[q]++;
+					work_done++;
+					avail--;
+					/*
+					 * A frame of our own that the switch
+					 * sent back still has an RX buffer of
+					 * its own: return it, or every such
+					 * frame leaks one BMU buffer.
+					 */
+					if (zx_bmu_free_bp(adapter, bp))
+						adapter->bmu_free_fail++;
+					continue;
+				}
+			}
+
+			/* Deliver to the netdev of the ingress port */
+			dest_dev = port < ZX_NUM_PORTS ? adapter->ports[port] : NULL;
+
+			skb = napi_alloc_skb(&adapter->napi, len);
+			if (skb && dest_dev && (dest_dev->flags & IFF_UP)) {
+				dma_rmb();
+				memcpy(skb_put(skb, len), adapter->buffer_pool +
+				       bp * ZX_BUF_SIZE + 16, len);
+				skb->dev = dest_dev;
+				skb->protocol = eth_type_trans(skb, dest_dev);
+				napi_gro_receive(&adapter->napi, skb);
+				dev_dstats_rx_add(dest_dev, len);
+			} else {
+				if (skb)
+					dev_kfree_skb_any(skb);
+				if (dest_dev)
+					dev_dstats_rx_dropped(dest_dev);
+			}
+
+			zx_rx_consume(desc, rel);
+			adapter->rx_cur_idx[q]++;
+			work_done++;
+			avail--;
+			if (zx_bmu_free_bp(adapter, bp))
+				adapter->bmu_free_fail++;
+		}
+		if (rel[0] || rel[1])
+			dma_wmb();
+		if (rel[0] && zx_rx_release(adapter, q, 0, rel[0]))
+			adapter->rx_rel_fail++;
+		if (rel[1] && zx_rx_release(adapter, q, 1, rel[1]))
+			adapter->rx_rel_fail++;
+	}
+	return work_done;
+}
+
+/* ---- interrupt and NAPI poll -------------------------------------- */
+
+static irqreturn_t zx_eth_isr(int irq, void *dev_id)
+{
+	struct zx_eth_adapter *adapter = dev_id;
+
+	if (!(readl(adapter->base + ZX_TM_INT_STATUS) & ZX_TM_INT_RX))
+		return IRQ_NONE;
+
+	writel(~0, adapter->base + ZX_TM_INT_MASK);
+	napi_schedule(&adapter->napi);
+	return IRQ_HANDLED;
+}
+
+static int zx_eth_poll(struct napi_struct *napi, int budget)
+{
+	struct zx_eth_adapter *adapter =
+		container_of(napi, struct zx_eth_adapter, napi);
+	int work_done;
+
+	spin_lock_bh(&adapter->tx_lock);
+	zx_tx_reclaim(adapter);
+	if (adapter->tx_pending >= ZX_TX_QUEUE_LIMIT)
+		zx_tx_kick(adapter);
+	spin_unlock_bh(&adapter->tx_lock);
+	work_done = budget ? zx_eth_rx(adapter, budget) : 0;
+
+	if (work_done < budget && napi_complete_done(napi, work_done))
+		writel(~ZX_TM_INT_RX, adapter->base + ZX_TM_INT_MASK);
+
+	return work_done;
+}
+
+static enum hrtimer_restart zx_eth_tx_timer(struct hrtimer *timer)
+{
+	struct zx_eth_adapter *adapter =
+		container_of(timer, struct zx_eth_adapter, tx_timer);
+
+	napi_schedule(&adapter->napi);
+	return HRTIMER_NORESTART;
+}
+
+/* ---- ndo_open / ndo_stop ------------------------------------------- */
+
+static int zx_eth_open(struct net_device *netdev)
+{
+	struct zx_eth_priv *priv = netdev_priv(netdev);
+	struct zx_eth_adapter *adapter = priv->adapter;
+
+	if (adapter->open_count == 0) {
+		napi_enable(&adapter->napi);
+		writel(~ZX_TM_INT_RX, adapter->base + ZX_TM_INT_MASK);
+	}
+	adapter->open_count++;
+
+	phylink_start(priv->phylink);
+	netif_start_queue(netdev);
+	return 0;
+}
+
+static int zx_eth_stop(struct net_device *netdev)
+{
+	struct zx_eth_priv *priv = netdev_priv(netdev);
+	struct zx_eth_adapter *adapter = priv->adapter;
+
+	netif_tx_disable(netdev);
+	phylink_stop(priv->phylink);
+	adapter->open_count--;
+
+	if (adapter->open_count == 0) {
+		writel(~0, adapter->base + ZX_TM_INT_MASK);
+		hrtimer_cancel(&adapter->tx_timer);
+		napi_disable(&adapter->napi);
+		/* a last poll may have unmasked it again */
+		writel(~0, adapter->base + ZX_TM_INT_MASK);
+		synchronize_irq(adapter->irq);
+	}
+
+	return 0;
+}
+
+/* ---- net_device_ops ------------------------------------------------ */
+
+static int zx_eth_set_mac_address(struct net_device *netdev, void *p)
+{
+	struct zx_eth_priv *priv = netdev_priv(netdev);
+	struct zx_eth_adapter *adapter = priv->adapter;
+	int ret;
+
+	ret = eth_mac_addr(netdev, p);
+	if (ret)
+		return ret;
+
+	mutex_lock(&adapter->ppe_lock);
+	zx_update_mac_tables(adapter);
+	mutex_unlock(&adapter->ppe_lock);
+	return 0;
+}
+
+static int zx_eth_ioctl(struct net_device *netdev, struct ifreq *ifr, int cmd)
+{
+	struct zx_eth_priv *priv = netdev_priv(netdev);
+
+	return phylink_mii_ioctl(priv->phylink, ifr, cmd);
+}
+
+static const struct net_device_ops zx_netdev_ops = {
+	.ndo_open		= zx_eth_open,
+	.ndo_stop		= zx_eth_stop,
+	.ndo_start_xmit		= zx_eth_xmit,
+	.ndo_set_mac_address	= zx_eth_set_mac_address,
+	.ndo_validate_addr	= eth_validate_addr,
+	.ndo_eth_ioctl		= zx_eth_ioctl,
+	.ndo_setup_tc		= zx_eth_setup_tc,
+};
+
+bool zx_eth_is_port(const struct net_device *dev)
+{
+	return dev->netdev_ops == &zx_netdev_ops;
+}
+
+/* ---- MAC counters -------------------------------------------------- */
+
+/*
+ * The offsets are from the vendor's statistics table, and the vendor driver
+ * picks the same counters.  The MACs have more (errors, collisions, pause
+ * frames), but these are the ones checked against known traffic.
+ */
+static const u16 zx_mib_regs[ZX_MIB_NUM] = {
+	[ZX_MIB_TX_OCTETS]	= ZX_MAC_TX_OCTETS,
+	[ZX_MIB_TX_FRAMES]	= ZX_MAC_TX_FRAMES,
+	[ZX_MIB_TX_MCAST]	= ZX_MAC_TX_MCAST,
+	[ZX_MIB_TX_BCAST]	= ZX_MAC_TX_BCAST,
+	[ZX_MIB_TX_HIST + 0]	= ZX_MAC_TX_HIST(0),
+	[ZX_MIB_TX_HIST + 1]	= ZX_MAC_TX_HIST(1),
+	[ZX_MIB_TX_HIST + 2]	= ZX_MAC_TX_HIST(2),
+	[ZX_MIB_TX_HIST + 3]	= ZX_MAC_TX_HIST(3),
+	[ZX_MIB_TX_HIST + 4]	= ZX_MAC_TX_HIST(4),
+	[ZX_MIB_TX_HIST + 5]	= ZX_MAC_TX_HIST(5),
+	[ZX_MIB_RX_OCTETS]	= ZX_MAC_RX_OCTETS,
+	[ZX_MIB_RX_UCAST]	= ZX_MAC_RX_UCAST,
+	[ZX_MIB_RX_MCAST]	= ZX_MAC_RX_MCAST,
+	[ZX_MIB_RX_BCAST]	= ZX_MAC_RX_BCAST,
+	[ZX_MIB_RX_HIST + 0]	= ZX_MAC_RX_HIST(0),
+	[ZX_MIB_RX_HIST + 1]	= ZX_MAC_RX_HIST(1),
+	[ZX_MIB_RX_HIST + 2]	= ZX_MAC_RX_HIST(2),
+	[ZX_MIB_RX_HIST + 3]	= ZX_MAC_RX_HIST(3),
+	[ZX_MIB_RX_HIST + 4]	= ZX_MAC_RX_HIST(4),
+	[ZX_MIB_RX_HIST + 5]	= ZX_MAC_RX_HIST(5),
+};
+
+/*
+ * The counters are 32 bits wide and keep running.  At 1 Gbit/s the byte
+ * counters wrap after 34 s, so they are read every few seconds, and what
+ * they grew by since the last read is added to 64-bit sums.
+ */
+#define ZX_MIB_INTERVAL		(5 * HZ)
+
+static void zx_mib_update(struct zx_eth_priv *priv)
+{
+	void __iomem *base = priv->adapter->base + ZX_MAC_PORT(priv->port);
+	int i;
+
+	lockdep_assert_held(&priv->adapter->mib_lock);
+
+	for (i = 0; i < ZX_MIB_NUM; i++) {
+		u32 val = readl(base + zx_mib_regs[i]);
+
+		priv->mib[i] += val - priv->mib_last[i];	/* modulo 2^32 */
+		priv->mib_last[i] = val;
+	}
+}
+
+static void zx_mib_work(struct work_struct *work)
+{
+	struct zx_eth_adapter *adapter =
+		container_of(to_delayed_work(work), struct zx_eth_adapter,
+			     mib_work);
+	int i;
+
+	mutex_lock(&adapter->mib_lock);
+	for (i = 0; i < ZX_NUM_PORTS; i++)
+		if (adapter->ports[i])
+			zx_mib_update(netdev_priv(adapter->ports[i]));
+	mutex_unlock(&adapter->mib_lock);
+
+	schedule_delayed_work(&adapter->mib_work, ZX_MIB_INTERVAL);
+}
+
+/* ---- ethtool_ops --------------------------------------------------- */
+
+static int zx_eth_get_link_ksettings(struct net_device *netdev,
+				     struct ethtool_link_ksettings *cmd)
+{
+	struct zx_eth_priv *priv = netdev_priv(netdev);
+
+	return phylink_ethtool_ksettings_get(priv->phylink, cmd);
+}
+
+static int zx_eth_set_link_ksettings(struct net_device *netdev,
+				     const struct ethtool_link_ksettings *cmd)
+{
+	struct zx_eth_priv *priv = netdev_priv(netdev);
+
+	return phylink_ethtool_ksettings_set(priv->phylink, cmd);
+}
+
+static int zx_eth_nway_reset(struct net_device *netdev)
+{
+	struct zx_eth_priv *priv = netdev_priv(netdev);
+
+	return phylink_ethtool_nway_reset(priv->phylink);
+}
+
+static void zx_eth_get_eth_mac_stats(struct net_device *netdev,
+				     struct ethtool_eth_mac_stats *mac_stats)
+{
+	struct zx_eth_priv *priv = netdev_priv(netdev);
+	const u64 *mib = priv->mib;
+
+	mutex_lock(&priv->adapter->mib_lock);
+	zx_mib_update(priv);
+
+	mac_stats->FramesTransmittedOK = mib[ZX_MIB_TX_FRAMES];
+	mac_stats->OctetsTransmittedOK = mib[ZX_MIB_TX_OCTETS];
+	mac_stats->MulticastFramesXmittedOK = mib[ZX_MIB_TX_MCAST];
+	mac_stats->BroadcastFramesXmittedOK = mib[ZX_MIB_TX_BCAST];
+	/* there is a total, but the vendor driver adds up these three */
+	mac_stats->FramesReceivedOK = mib[ZX_MIB_RX_UCAST] +
+				      mib[ZX_MIB_RX_MCAST] +
+				      mib[ZX_MIB_RX_BCAST];
+	mac_stats->OctetsReceivedOK = mib[ZX_MIB_RX_OCTETS];
+	mac_stats->MulticastFramesReceivedOK = mib[ZX_MIB_RX_MCAST];
+	mac_stats->BroadcastFramesReceivedOK = mib[ZX_MIB_RX_BCAST];
+
+	mutex_unlock(&priv->adapter->mib_lock);
+}
+
+/* The last bin also counts 1522-byte (VLAN-tagged) frames */
+static const struct ethtool_rmon_hist_range zx_rmon_ranges[] = {
+	{   64,   64 },
+	{   65,  127 },
+	{  128,  255 },
+	{  256,  511 },
+	{  512, 1023 },
+	{ 1024, 1522 },
+	{}
+};
+
+static void zx_eth_get_rmon_stats(struct net_device *netdev,
+				  struct ethtool_rmon_stats *rmon_stats,
+				  const struct ethtool_rmon_hist_range **ranges)
+{
+	struct zx_eth_priv *priv = netdev_priv(netdev);
+	int i;
+
+	BUILD_BUG_ON(ARRAY_SIZE(zx_rmon_ranges) != ZX_MAC_HIST_BINS + 1);
+
+	mutex_lock(&priv->adapter->mib_lock);
+	zx_mib_update(priv);
+	for (i = 0; i < ZX_MAC_HIST_BINS; i++) {
+		rmon_stats->hist[i] = priv->mib[ZX_MIB_RX_HIST + i];
+		rmon_stats->hist_tx[i] = priv->mib[ZX_MIB_TX_HIST + i];
+	}
+	mutex_unlock(&priv->adapter->mib_lock);
+
+	*ranges = zx_rmon_ranges;
+}
+
+static const struct ethtool_ops zx_ethtool_ops = {
+	.get_link		= ethtool_op_get_link,
+	.get_link_ksettings	= zx_eth_get_link_ksettings,
+	.set_link_ksettings	= zx_eth_set_link_ksettings,
+	.nway_reset		= zx_eth_nway_reset,
+	.get_eth_mac_stats	= zx_eth_get_eth_mac_stats,
+	.get_rmon_stats		= zx_eth_get_rmon_stats,
+};
+
+/* ---- debugfs ------------------------------------------------------- */
+
+/* Driver counters and the main DMA, BMU and switch registers */
+static int zx_eth_dma_state_show(struct seq_file *s, void *data)
+{
+	struct zx_eth_adapter *adapter = dev_get_drvdata(s->private);
+	static const u32 regs[] = {
+		ZX_TM_TAB0_ADDR, ZX_TM_TAB1_ADDR, ZX_TM_BUF_F0,
+		ZX_TM_BUF_START, ZX_TM_BUF_END, ZX_BMU_CTRL,
+		ZX_BMU_FREE_STATUS,
+		ZX_DMA_CTRL, ZX_DMA_RING_CFG,
+		ZX_DMA_AREA50, ZX_DMA_TX_RING,
+		ZX_DMA_TX_START,
+		ZX_RX_REL_CTRL, ZX_RX_REL_DATA,
+		ZX_MAC_PORT(0), ZX_MAC_PORT(1), ZX_MAC_PORT(2),
+		ZX_MAC_PORT(3), ZX_MAC_PORT(4),
+		ZX_SW_FWD_CTRL,
+		ZX_SPA_UP_REG_PKT_EN, ZX_SPA_UP_REG_PKT_EN + 4,
+		ZX_SPA_DN_REG_PKT_EN, ZX_SPA_DN_REG_PKT_EN + 4,
+		0x1d4080, 0x1d4084,
+		0x1d40c0, 0x1d40c4,
+		0x1d4100, 0x1d4104,
+		ZX_SPA_TRAP_PROTO,
+		ZX_SPA_PORT_EN(0), ZX_SPA_PORT_EN(1), ZX_SPA_PORT_EN(2),
+		ZX_SPA_PORT_EN(3), ZX_SPA_PORT_EN(4), ZX_SPA_PORT_EN(6),
+	};
+	int i;
+
+	rtnl_lock();
+	seq_printf(s, "arena=%pad open_count=%d\n",
+		   &adapter->buf_pool_dma, adapter->open_count);
+	seq_printf(s, "tx_submitted=%u tx_completed=%u tx_pending=%u tx_done_excess=%u rx_resync=%u\n",
+		   adapter->tx_cur_idx, adapter->tx_complete_cnt,
+		   adapter->tx_pending, adapter->tx_done_excess,
+		   adapter->rx_resync_cnt);
+	seq_printf(s, "rx_alias=%u rx_nobuf=%u rx_rel_fail=%u rx_bad=%u\n",
+		   adapter->rx_alias_cnt, adapter->rx_nobuf_cnt,
+		   adapter->rx_rel_fail, adapter->rx_bad_cnt);
+	seq_printf(s, "ppe_active=%u ppe_flows=%u ppe_add_skip=%u ppe_add_fail=%u\n",
+		   adapter->ppe_active, adapter->ppe_count,
+		   adapter->ppe_add_skip, adapter->ppe_add_fail);
+	seq_printf(s, "sw_members=%#x sw_fdb_deleted=%u\n",
+		   adapter->sw_members,
+		   adapter->sw_fdb_deleted);
+	seq_printf(s, "bmu_alloc_fail=%u bmu_alloc_late=%u bmu_free_fail=%u bmu_free_slots=%u\n",
+		   adapter->bmu_alloc_fail, adapter->bmu_alloc_late,
+		   adapter->bmu_free_fail, adapter->bmu_free_slots);
+
+	for (i = 0; i < 0x100; i += 4)
+		seq_printf(s, "bmu+%03x=%08x\n", i,
+			   readl(adapter->base + ZX_BMU_BASE + i));
+	/* Reading 0x68 (TX done) would clear the count of sent frames */
+	for (i = 0; i < 0x120; i += 4) {
+		if (i == 0x68)
+			continue;
+		seq_printf(s, "dma+%03x=%08x\n", i,
+			   readl(adapter->base + ZX_DMA_BASE + i));
+	}
+
+	for (i = 0; i < ARRAY_SIZE(regs); i++)
+		seq_printf(s, "%06x=%08x\n", regs[i],
+			   readl(adapter->base + regs[i]));
+
+	for (i = 0; i < ZX_RX_QUEUES; i++)
+		seq_printf(s, "rxq%d count=%u consumed=%u\n", i,
+			   readl(adapter->base + ZX_DMA_RX_QUEUE_CNT(i)) & 0xffff,
+			   READ_ONCE(adapter->rx_cur_idx[i]));
+	rtnl_unlock();
+
+	return 0;
+}
+
+/*
+ * The driver cannot be unbound (suppress_bind_attrs) and has no remove
+ * function, so the directory stays until reboot.
+ */
+static void zx_eth_debugfs_init(struct zx_eth_adapter *adapter)
+{
+	struct dentry *dir = debugfs_create_dir(dev_name(adapter->dev), NULL);
+
+	debugfs_create_devm_seqfile(adapter->dev, "dma_state", dir,
+				    zx_eth_dma_state_show);
+}
+
+static int zx_eth_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct zx_eth_adapter *adapter;
+	struct device_node *np;
+	u8 addrs[ZX_NUM_PORTS][ETH_ALEN];
+	unsigned long have_addr = 0;
+	struct device_node *ports_np;
+	int i, ret;
+
+	ports_np = of_get_child_by_name(dev->of_node, "ethernet-ports");
+	if (!ports_np)
+		return dev_err_probe(dev, -ENODEV, "no ethernet-ports node\n");
+
+	/*
+	 * The MAC addresses can come from nvmem cells on a flash partition
+	 * that is not registered yet.  Get them before the hardware is set
+	 * up, so that deferring the probe leaves no DMA running.
+	 */
+	for_each_available_child_of_node(ports_np, np) {
+		u32 port;
+
+		if (of_property_read_u32(np, "reg", &port) ||
+		    port >= ZX_NUM_PORTS)
+			continue;
+		ret = of_get_mac_address(np, addrs[port]);
+		if (ret == -EPROBE_DEFER) {
+			of_node_put(np);
+			of_node_put(ports_np);
+			return ret;
+		}
+		if (!ret)
+			__set_bit(port, &have_addr);
+	}
+
+	ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
+	if (ret) {
+		of_node_put(ports_np);
+		return dev_err_probe(dev, ret, "failed to set the DMA mask\n");
+	}
+
+	adapter = devm_kzalloc(dev, sizeof(*adapter), GFP_KERNEL);
+	if (!adapter) {
+		of_node_put(ports_np);
+		return -ENOMEM;
+	}
+
+	adapter->irq = platform_get_irq(pdev, 0);
+	if (adapter->irq < 0) {
+		of_node_put(ports_np);
+		return adapter->irq;
+	}
+
+	adapter->dev = dev;
+	spin_lock_init(&adapter->tx_lock);
+	spin_lock_init(&adapter->bmu_free_lock);
+	mutex_init(&adapter->ppe_lock);
+	spin_lock_init(&adapter->trap_lock);
+	mutex_init(&adapter->sw_lock);
+	spin_lock_init(&adapter->sw_fdb_lock);
+	INIT_WORK(&adapter->sw_work, zx_sw_work);
+	mutex_init(&adapter->mib_lock);
+	INIT_DELAYED_WORK(&adapter->mib_work, zx_mib_work);
+	ret = rhashtable_init(&adapter->ppe_flows, &zx_ppe_ht_params);
+	if (ret) {
+		of_node_put(ports_np);
+		return ret;
+	}
+
+	adapter->base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(adapter->base)) {
+		ret = PTR_ERR(adapter->base);
+		goto err_rhash;
+	}
+
+	adapter->buffer_pool = dma_alloc_coherent(dev, ZX_DMA_ARENA_SIZE,
+						  &adapter->buf_pool_dma, GFP_KERNEL);
+	if (!adapter->buffer_pool) {
+		ret = -ENOMEM;
+		goto err_rhash;
+	}
+	adapter->tab0 = adapter->buffer_pool + ZX_TAB0_OFFSET;
+	adapter->tab0_dma = adapter->buf_pool_dma + ZX_TAB0_OFFSET;
+	adapter->tab1 = adapter->buffer_pool + ZX_TAB1_OFFSET;
+	adapter->tab1_dma = adapter->buf_pool_dma + ZX_TAB1_OFFSET;
+	adapter->rx_ring = adapter->buffer_pool + ZX_RX_OFFSET;
+	adapter->rx_ring_dma = adapter->buf_pool_dma + ZX_RX_OFFSET;
+	adapter->tx_ring = adapter->buffer_pool + ZX_TX_OFFSET;
+	adapter->tx_ring_dma = adapter->buf_pool_dma + ZX_TX_OFFSET;
+
+	ret = zx_hw_init(adapter);
+	if (ret)
+		goto err_dma;
+	zx_dma_enable(adapter, true);
+
+	/* One netdev per port, named by the port's label */
+	for_each_available_child_of_node(ports_np, np) {
+		struct net_device *netdev;
+		struct zx_eth_priv *priv;
+		phy_interface_t interface;
+		const char *label;
+		u32 port;
+
+		if (of_property_read_u32(np, "reg", &port) || port >= ZX_NUM_PORTS ||
+		    adapter->ports[port]) {
+			dev_warn(dev, "%pOF: bad or duplicate reg\n", np);
+			continue;
+		}
+
+		netdev = devm_alloc_etherdev(dev, sizeof(struct zx_eth_priv));
+		if (!netdev) {
+			ret = -ENOMEM;
+			of_node_put(np);
+			goto err_disconnect;
+		}
+		SET_NETDEV_DEV(netdev, dev);
+		if (!of_property_read_string(np, "label", &label)) {
+			strscpy(netdev->name, label, IFNAMSIZ);
+			netdev->name_assign_type = NET_NAME_PREDICTABLE;
+		}
+		if (test_bit(port, &have_addr))
+			eth_hw_addr_set(netdev, addrs[port]);
+		else
+			eth_hw_addr_random(netdev);
+		netdev->priv_flags |= IFF_LIVE_ADDR_CHANGE;
+		netdev->netdev_ops = &zx_netdev_ops;
+		netdev->ethtool_ops = &zx_ethtool_ops;
+		netdev->pcpu_stat_type = NETDEV_PCPU_STAT_DSTATS;
+		netdev->hw_features |= NETIF_F_HW_TC;
+		netdev->features |= NETIF_F_HW_TC;
+		netdev->min_mtu    = 68;
+		netdev->max_mtu    = 1500;
+		priv = netdev_priv(netdev);
+		priv->netdev  = netdev;
+		priv->adapter = adapter;
+		priv->port    = port;
+		adapter->ports[port] = netdev;
+
+		ret = of_get_phy_mode(np, &interface);
+		if (ret) {
+			dev_err_probe(dev, ret, "%pOF: no valid phy-mode\n", np);
+			of_node_put(np);
+			goto err_disconnect;
+		}
+		priv->phylink_config.dev = &netdev->dev;
+		priv->phylink_config.type = PHYLINK_NETDEV;
+		priv->phylink_config.mac_capabilities = MAC_10 | MAC_100 |
+							MAC_1000FD;
+		__set_bit(PHY_INTERFACE_MODE_INTERNAL,
+			  priv->phylink_config.supported_interfaces);
+		__set_bit(PHY_INTERFACE_MODE_GMII,
+			  priv->phylink_config.supported_interfaces);
+		priv->phylink = phylink_create(&priv->phylink_config,
+					       of_fwnode_handle(np), interface,
+					       &zx_phylink_mac_ops);
+		if (IS_ERR(priv->phylink)) {
+			ret = dev_err_probe(dev, PTR_ERR(priv->phylink),
+					    "port %u: phylink\n", port);
+			priv->phylink = NULL;
+			of_node_put(np);
+			goto err_disconnect;
+		}
+		ret = phylink_of_phy_connect(priv->phylink, np, 0);
+		if (ret)
+			dev_warn(dev, "port %u: PHY connect failed: %d\n", port, ret);
+	}
+	of_node_put(ports_np);
+	ports_np = NULL;
+
+	/* The MAC counters start from zero, whatever the registers hold */
+	mutex_lock(&adapter->mib_lock);
+	for (i = 0; i < ZX_NUM_PORTS; i++) {
+		struct zx_eth_priv *priv;
+
+		if (!adapter->ports[i])
+			continue;
+		priv = netdev_priv(adapter->ports[i]);
+		zx_mib_update(priv);
+		memset(priv->mib, 0, sizeof(priv->mib));
+	}
+	mutex_unlock(&adapter->mib_lock);
+
+	for (i = 0; i < ZX_NUM_PORTS; i++)
+		if (adapter->ports[i])
+			break;
+	if (i == ZX_NUM_PORTS) {
+		ret = dev_err_probe(dev, -ENODEV, "no usable port\n");
+		goto err_disconnect;
+	}
+
+	mutex_lock(&adapter->ppe_lock);
+	zx_update_mac_tables(adapter);
+	mutex_unlock(&adapter->ppe_lock);
+
+	/* One NAPI context serves all ports */
+	netif_napi_add(adapter->ports[i], &adapter->napi, zx_eth_poll);
+	hrtimer_setup(&adapter->tx_timer, zx_eth_tx_timer,
+		      CLOCK_MONOTONIC, HRTIMER_MODE_REL);
+
+	/* zx_hw_init() masked the interrupt, a port's open enables it */
+	ret = devm_request_irq(dev, adapter->irq, zx_eth_isr, 0, dev_name(dev),
+			       adapter);
+	if (ret)
+		goto err_napi;
+
+	for (i = 0; i < ZX_NUM_PORTS; i++) {
+		if (!adapter->ports[i])
+			continue;
+		ret = register_netdev(adapter->ports[i]);
+		if (ret) {
+			dev_err(dev, "port %d: register_netdev failed: %d\n", i, ret);
+			while (--i >= 0)
+				if (adapter->ports[i])
+					unregister_netdev(adapter->ports[i]);
+			goto err_napi;
+		}
+	}
+
+	platform_set_drvdata(pdev, adapter);
+
+	zx_sw_init(adapter);
+	schedule_delayed_work(&adapter->mib_work, ZX_MIB_INTERVAL);
+
+	zx_eth_debugfs_init(adapter);
+	return 0;
+
+err_napi:
+	netif_napi_del(&adapter->napi);
+err_disconnect:
+	for (i = 0; i < ZX_NUM_PORTS; i++) {
+		struct zx_eth_priv *priv;
+
+		if (!adapter->ports[i])
+			continue;
+		priv = netdev_priv(adapter->ports[i]);
+		if (priv->phylink) {
+			phylink_disconnect_phy(priv->phylink);
+			phylink_destroy(priv->phylink);
+		}
+	}
+	of_node_put(ports_np);
+	zx_dma_enable(adapter, false);
+err_dma:
+	dma_free_coherent(dev, ZX_DMA_ARENA_SIZE, adapter->buffer_pool,
+			  adapter->buf_pool_dma);
+err_rhash:
+	rhashtable_destroy(&adapter->ppe_flows);
+	return ret;
+}
+
+/* ---- module glue --------------------------------------------------- */
+
+/*
+ * The switch writes received frames, and frames of offloaded flows, into
+ * the buffer pool without the CPU.  Stop it before a reboot or kexec: after
+ * kexec, that memory belongs to the next kernel.
+ */
+static void zx_eth_shutdown(struct platform_device *pdev)
+{
+	struct zx_eth_adapter *adapter = platform_get_drvdata(pdev);
+	int i;
+
+	if (!adapter)
+		return;
+
+	zx_sw_fini(adapter);
+
+	rtnl_lock();
+	for (i = 0; i < ZX_NUM_PORTS; i++) {
+		if (!adapter->ports[i])
+			continue;
+		netif_device_detach(adapter->ports[i]);
+		dev_close(adapter->ports[i]);
+	}
+	rtnl_unlock();
+	cancel_work_sync(&adapter->sw_work);
+	cancel_delayed_work_sync(&adapter->mib_work);
+
+	zx_hw_reset(adapter);
+}
+
+static const struct of_device_id zx_eth_dt_ids[] = {
+	{ .compatible = "zte,zx279128s-gmac" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, zx_eth_dt_ids);
+
+static struct platform_driver zx_eth_driver = {
+	.probe  = zx_eth_probe,
+	.shutdown = zx_eth_shutdown,
+	.driver = {
+		.name           = DRV_NAME,
+		.of_match_table = zx_eth_dt_ids,
+		.suppress_bind_attrs = true,
+	},
+};
+module_platform_driver(zx_eth_driver);
+
+MODULE_AUTHOR("Navid Ghahremani <ghahramani.navid@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx279128s Ethernet switch and DMA driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/ethernet/zte/zx279128s-ppe.c b/drivers/net/ethernet/zte/zx279128s-ppe.c
new file mode 100644
index 0000000000..ff19d2cc67
--- /dev/null
+++ b/drivers/net/ethernet/zte/zx279128s-ppe.c
@@ -0,0 +1,805 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ZTE zx279128s Ethernet switch: hardware flow offload
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/etherdevice.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/ip.h>
+#include <linux/rhashtable.h>
+#include <linux/slab.h>
+#include <net/flow_offload.h>
+
+#include "zx279128s-eth.h"
+
+/* ---- Hardware flow offload ------------------------------------------ */
+
+/*
+ * The packet processor forwards established IPv4 TCP/UDP flows without the
+ * CPU: the classifier looks the 5-tuple up in two hash banks, the packet
+ * modifier rewrites MACs, addresses, ports and TTL, and the entry sends
+ * the frame straight to the egress port.  A miss goes to the CPU as usual.
+ * Formats reverse engineered from the stock firmware's NPU tables.
+ *
+ * The WAN is a plain switch port here, so the hardware treats both
+ * directions of a connection as upstream: both use the upstream key
+ * header, and each direction has its own flow (rewrite) index.
+ */
+
+struct zx_ppe_entry {
+	struct rhash_head	node;
+	unsigned long		cookie;
+	u16			hash;	/* 0-255 bank 0, 256-383 bank 1 */
+	u16			flow;
+	u16			nh;
+	u8			subnet;
+};
+
+const struct rhashtable_params zx_ppe_ht_params = {
+	.head_offset		= offsetof(struct zx_ppe_entry, node),
+	.key_offset		= offsetof(struct zx_ppe_entry, cookie),
+	.key_len		= sizeof(unsigned long),
+	.automatic_shrinking	= true,
+};
+
+static u32 zx_pp_data_reg(u32 blk, int i)
+{
+	if (blk == ZX_PM_BASE && i >= 4)
+		return ZX_PM_IND_DATA_HI + (i - 4) * 4;
+	return ZX_PP_IND_DATA + i * 4;
+}
+
+static int zx_pp_wait(struct zx_eth_adapter *adapter, u32 blk)
+{
+	u32 val;
+
+	return readl_poll_timeout(adapter->base + blk + ZX_PP_IND_STATUS, val,
+				  val & 1, 0, 1000);
+}
+
+/* Write @n data words of an entry; the hardware takes them when word 0 is
+ * written, so it goes last (as in the stock driver).
+ */
+static int zx_pp_write(struct zx_eth_adapter *adapter, u32 blk, u32 table,
+		       u32 index, const u32 *data, int n)
+{
+	int words = blk == ZX_CLA_BASE ? 17 : 8;
+	int i, ret;
+
+	ret = zx_pp_wait(adapter, blk);
+	if (ret)
+		return ret;
+	writel(index | table << 22, adapter->base + blk + ZX_PP_IND_CMD);
+	for (i = words - 1; i >= 0; i--)
+		writel(i < n ? data[i] : 0, adapter->base + blk + zx_pp_data_reg(blk, i));
+	/* the status can still read idle right after the command */
+	for (i = 0; i < 64 && (readl(adapter->base + blk + ZX_PP_IND_STATUS) & 1); i++)
+		;
+	return zx_pp_wait(adapter, blk);
+}
+
+static int zx_pp_read_word0(struct zx_eth_adapter *adapter, u32 blk, u32 table,
+			    u32 index, u32 *val)
+{
+	int i, ret;
+
+	ret = zx_pp_wait(adapter, blk);
+	if (ret)
+		return ret;
+	writel(index | table << 22 | ZX_PP_IND_READ, adapter->base + blk + ZX_PP_IND_CMD);
+	for (i = 0; i < 64 && (readl(adapter->base + blk + ZX_PP_IND_STATUS) & 1); i++)
+		;
+	ret = zx_pp_wait(adapter, blk);
+	if (!ret)
+		*val = readl(adapter->base + blk + ZX_PP_IND_DATA);
+	return ret;
+}
+
+/* MAC in the layout of the switch's MAC registers: bytes 2-5, then 0-1 */
+static void zx_mac_words(const u8 *mac, u32 *w)
+{
+	w[0] = mac[2] << 24 | mac[3] << 16 | mac[4] << 8 | mac[5];
+	w[1] = mac[0] << 8 | mac[1];
+}
+
+/* Set @width bits at bit @pos of a little-endian bit string */
+static void zx_ppe_put_bits(u8 *buf, int pos, int width, u32 val)
+{
+	int i;
+
+	for (i = 0; i < width; i++, pos++)
+		if (val & BIT(i))
+			buf[pos / 8] |= BIT(pos % 8);
+}
+
+/* MSB-first CRC over the key, last byte first, no init or final xor */
+static u32 zx_ppe_crc(u32 poly, const u8 *key, int len)
+{
+	u32 crc = 0;
+	int i, b;
+
+	for (i = len - 1; i >= 0; i--) {
+		crc ^= key[i] << 24;
+		for (b = 0; b < 8; b++)
+			crc = crc & BIT(31) ? crc << 1 ^ poly : crc << 1;
+	}
+	return crc;
+}
+
+struct zx_ppe_tuple {
+	u8	proto;
+	__be32	saddr, daddr;
+	__be16	sport, dport;
+};
+
+static void zx_ppe_halfwords(const struct zx_ppe_tuple *t, u16 hw[7])
+{
+	u32 s = be32_to_cpu(t->saddr), d = be32_to_cpu(t->daddr);
+
+	hw[0] = t->proto;
+	hw[1] = s >> 16;
+	hw[2] = s & 0xffff;
+	hw[3] = d >> 16;
+	hw[4] = d & 0xffff;
+	hw[5] = be16_to_cpu(t->sport);
+	hw[6] = be16_to_cpu(t->dport);
+}
+
+/* Hash slot of a tuple in bank 0 (0-255) and bank 1 (256-383).  The key is
+ * 45 bytes: header (extract rule id at bit 23, direction bit 32 = 0) and
+ * the extracted halfwords from bit 33.
+ */
+static void zx_ppe_slots(const struct zx_ppe_tuple *t, u16 slot[2])
+{
+	u8 key[45] = {};
+	u16 hw[7];
+	int i;
+
+	zx_ppe_halfwords(t, hw);
+	zx_ppe_put_bits(key, 23, 8, ZX_PPE_V4_RULE);
+	for (i = 0; i < 7; i++)
+		zx_ppe_put_bits(key, 33 + 16 * i, 16, hw[i]);
+	slot[0] = zx_ppe_crc(0x04c11db7, key, sizeof(key)) & 0xff;
+	slot[1] = ZX_PPE_HASH0_SIZE + (zx_ppe_crc(0x1edc6f41, key, sizeof(key)) & 0x7f);
+}
+
+/*
+ * Classifier entry: rewrite index, egress port and forward flag, then the
+ * key. Words 1-3 are the same constants in every entry of the stock
+ * firmware, and so is the 0x44 in word 0.
+ */
+static void zx_ppe_hash_entry(const struct zx_ppe_tuple *t, u16 flow, int port,
+			      u32 w[9])
+{
+	u8 key[20] = {};
+	u16 hw[7];
+	int i;
+
+	zx_ppe_halfwords(t, hw);
+	/* valid, extract index 9, upstream */
+	zx_ppe_put_bits(key, 0, 8, 0x40 | ZX_PPE_V4_INDEX);
+	for (i = 0; i < 7; i++)
+		zx_ppe_put_bits(key, 24 + 16 * i, 16, hw[i]);
+	w[0] = FIELD_PREP(ZX_CLA_W0_FLOW, flow) | ZX_CLA_W0_FWD |
+	       FIELD_PREP(ZX_CLA_W0_EGRESS, ZX_BRPORT(port)) | ZX_CLA_W0_BITS;
+	w[1] = 0xfa11c000;
+	w[2] = 0x00000608;
+	w[3] = 0x80000000;
+	for (i = 0; i < 5; i++)
+		w[4 + i] = get_unaligned_le32(key + 4 * i);
+}
+
+struct zx_ppe_rewrite {
+	bool	sip, dip, sport, dport;
+	__be16	new_sport, new_dport;
+	u8	subnet;
+	u16	nh;
+};
+
+/* Packet modifier flow entry (12 bytes, little-endian bit fields) */
+static void zx_ppe_flow_entry(const struct zx_ppe_rewrite *r, u32 w[3])
+{
+	u8 b[12] = {};
+
+	zx_ppe_put_bits(b, 0, 1, 1);			/* replace destination MAC */
+	zx_ppe_put_bits(b, 1, 1, 1);			/* replace source MAC */
+	if (r->dport)
+		zx_ppe_put_bits(b, 2, 16, be16_to_cpu(r->new_dport));
+	if (r->sport)
+		zx_ppe_put_bits(b, 18, 16, be16_to_cpu(r->new_sport));
+	zx_ppe_put_bits(b, 34, 1, 1);			/* decrement TTL */
+	zx_ppe_put_bits(b, 35, 1, 1);			/* update L4 checksum */
+	zx_ppe_put_bits(b, 36, 1, 1);			/* update IP checksum */
+	zx_ppe_put_bits(b, 37, 1, r->dport);
+	zx_ppe_put_bits(b, 38, 1, r->sport);
+	zx_ppe_put_bits(b, 39, 1, r->dip);
+	zx_ppe_put_bits(b, 40, 1, r->sip);
+	zx_ppe_put_bits(b, 41, 4, r->subnet);
+	zx_ppe_put_bits(b, 50, 9, r->nh);
+	w[0] = get_unaligned_le32(b);
+	w[1] = get_unaligned_le32(b + 4);
+	w[2] = get_unaligned_le32(b + 8);
+}
+
+/* Subnet: source MAC and (for SNAT) source IP of rewritten frames */
+static int zx_ppe_subnet_get(struct zx_eth_adapter *adapter, const u8 *mac,
+			     __be32 ip)
+{
+	int i, free = -1;
+	u32 w[2];
+
+	for (i = 0; i < ZX_PPE_SUBNETS; i++) {
+		if (adapter->ppe_subnet[i].refs &&
+		    ether_addr_equal(adapter->ppe_subnet[i].mac, mac) &&
+		    adapter->ppe_subnet[i].ip == ip) {
+			adapter->ppe_subnet[i].refs++;
+			return i;
+		}
+		if (!adapter->ppe_subnet[i].refs && free < 0)
+			free = i;
+	}
+	if (free < 0)
+		return -ENOSPC;
+
+	zx_mac_words(mac, w);
+	if (zx_pp_write(adapter, ZX_PM_BASE, ZX_PM_SRC_MAC, free, w, 2))
+		return -ETIMEDOUT;
+	writel(be32_to_cpu(ip), adapter->base + ZX_PP_SNAT_IP(free));
+	ether_addr_copy(adapter->ppe_subnet[free].mac, mac);
+	adapter->ppe_subnet[free].ip = ip;
+	adapter->ppe_subnet[free].refs = 1;
+	return free;
+}
+
+static void zx_ppe_subnet_put(struct zx_eth_adapter *adapter, int i)
+{
+	adapter->ppe_subnet[i].refs--;
+}
+
+static int zx_ppe_alloc(unsigned long *map, int size, int first)
+{
+	int i = find_next_zero_bit(map, size, first);
+
+	if (i >= size)
+		return -ENOSPC;
+	set_bit(i, map);
+	return i;
+}
+
+/*
+ * Apply one of the MAC rewrites that nf_flow_table_offload.c builds: the
+ * destination MAC as 4 bytes at offset 0 and 2 bytes at offset 4 (mask
+ * 0xffff0000), the source MAC as 2 bytes at offset 4 (mask 0x0000ffff, value
+ * in the upper half) and 4 bytes at offset 8. The masks and values are in
+ * CPU order, so the byte positions below are those of a little-endian CPU,
+ * which the zx279128s is.
+ */
+static void zx_ppe_mangle_eth(const struct flow_action_entry *act, void *eth)
+{
+	void *dest = eth + act->mangle.offset;
+	const void *src = &act->mangle.val;
+
+	if (act->mangle.offset > 8)
+		return;
+	if (act->mangle.mask == 0xffff) {
+		src += 2;
+		dest += 2;
+	}
+	memcpy(dest, src, act->mangle.mask ? 2 : 4);
+}
+
+static int zx_ppe_flow_add(struct zx_eth_adapter *adapter,
+			   struct flow_cls_offload *f)
+{
+	struct flow_rule *rule = flow_cls_offload_flow_rule(f);
+	struct flow_action_entry *act;
+	struct zx_ppe_rewrite rw = {};
+	struct zx_ppe_tuple t = {};
+	struct net_device *odev = NULL;
+	struct zx_ppe_entry *e;
+	struct ethhdr eth = {};
+	__be32 new_saddr, new_daddr;
+	u16 slot[2];
+	u32 w[17];
+	int i, ret, port, subnet, flow, nh;
+
+	if (rhashtable_lookup_fast(&adapter->ppe_flows, &f->cookie, zx_ppe_ht_params))
+		return -EEXIST;
+
+	/* the hardware only sees frames from our switch ports */
+	if (flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_META)) {
+		struct flow_match_meta match;
+
+		flow_rule_match_meta(rule, &match);
+		for (i = 0; i < ZX_NUM_PORTS; i++)
+			if (adapter->ports[i] &&
+			    adapter->ports[i]->ifindex == match.key->ingress_ifindex)
+				break;
+		if (i == ZX_NUM_PORTS)
+			return -EOPNOTSUPP;
+	}
+
+	if (flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_CONTROL)) {
+		struct flow_match_control match;
+
+		flow_rule_match_control(rule, &match);
+		if (match.key->addr_type != FLOW_DISSECTOR_KEY_IPV4_ADDRS)
+			return -EOPNOTSUPP;
+	} else {
+		return -EOPNOTSUPP;
+	}
+
+	if (flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_BASIC)) {
+		struct flow_match_basic match;
+
+		flow_rule_match_basic(rule, &match);
+		if (match.key->n_proto != htons(ETH_P_IP))
+			return -EOPNOTSUPP;
+		t.proto = match.key->ip_proto;
+	}
+	if (t.proto != IPPROTO_TCP && t.proto != IPPROTO_UDP)
+		return -EOPNOTSUPP;
+
+	if (!flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_IPV4_ADDRS) ||
+	    !flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_PORTS))
+		return -EOPNOTSUPP;
+	{
+		struct flow_match_ipv4_addrs addrs;
+		struct flow_match_ports ports;
+
+		flow_rule_match_ipv4_addrs(rule, &addrs);
+		flow_rule_match_ports(rule, &ports);
+		t.saddr = addrs.key->src;
+		t.daddr = addrs.key->dst;
+		t.sport = ports.key->src;
+		t.dport = ports.key->dst;
+	}
+	new_saddr = t.saddr;
+	new_daddr = t.daddr;
+	rw.new_sport = t.sport;
+	rw.new_dport = t.dport;
+
+	flow_action_for_each(i, act, &rule->action) {
+		switch (act->id) {
+		case FLOW_ACTION_MANGLE:
+			switch (act->mangle.htype) {
+			case FLOW_ACT_MANGLE_HDR_TYPE_ETH:
+				zx_ppe_mangle_eth(act, &eth);
+				break;
+			case FLOW_ACT_MANGLE_HDR_TYPE_IP4:
+				/* the value holds the address in network order */
+				if (act->mangle.offset == offsetof(struct iphdr, saddr))
+					new_saddr = (__force __be32)act->mangle.val;
+				else if (act->mangle.offset == offsetof(struct iphdr, daddr))
+					new_daddr = (__force __be32)act->mangle.val;
+				else
+					return -EOPNOTSUPP;
+				break;
+			case FLOW_ACT_MANGLE_HDR_TYPE_TCP:
+			case FLOW_ACT_MANGLE_HDR_TYPE_UDP: {
+				u32 val = be32_to_cpu((__force __be32)act->mangle.val);
+
+				if (act->mangle.offset == 0) {
+					if (act->mangle.mask == ~(__force u32)cpu_to_be32(0xffff))
+						rw.new_dport = cpu_to_be16(val);
+					else
+						rw.new_sport = cpu_to_be16(val >> 16);
+				} else if (act->mangle.offset == 2) {
+					rw.new_dport = cpu_to_be16(val);
+				} else {
+					return -EOPNOTSUPP;
+				}
+				break;
+			}
+			default:
+				return -EOPNOTSUPP;
+			}
+			break;
+		case FLOW_ACTION_CSUM:
+			break;
+		case FLOW_ACTION_REDIRECT:
+			odev = act->dev;
+			break;
+		default:
+			/* VLAN, PPPoE and tunnel encapsulation are not supported */
+			return -EOPNOTSUPP;
+		}
+	}
+
+	if (!odev || !zx_eth_is_port(odev))
+		return -EOPNOTSUPP;
+	port = ((struct zx_eth_priv *)netdev_priv(odev))->port;
+	if (!is_valid_ether_addr(eth.h_source) || !is_valid_ether_addr(eth.h_dest))
+		return -EOPNOTSUPP;
+
+	rw.sip = new_saddr != t.saddr;
+	rw.dip = new_daddr != t.daddr;
+	rw.sport = rw.new_sport != t.sport;
+	rw.dport = rw.new_dport != t.dport;
+
+	zx_ppe_slots(&t, slot);
+	if (!test_bit(slot[0], adapter->ppe_hash_used))
+		slot[1] = slot[0];
+	else if (test_bit(slot[1], adapter->ppe_hash_used))
+		return -EBUSY;
+
+	e = kzalloc_obj(*e);
+	if (!e)
+		return -ENOMEM;
+
+	subnet = zx_ppe_subnet_get(adapter, eth.h_source, rw.sip ? new_saddr : 0);
+	if (subnet < 0) {
+		ret = subnet;
+		goto err_free;
+	}
+	flow = zx_ppe_alloc(adapter->ppe_flow_used, ZX_PPE_FLOWS, 1);
+	if (flow < 0) {
+		ret = flow;
+		goto err_subnet;
+	}
+	nh = zx_ppe_alloc(adapter->ppe_nh_used, ZX_PPE_NEXT_HOPS, 1);
+	if (nh < 0) {
+		ret = nh;
+		goto err_flow;
+	}
+	rw.subnet = subnet;
+	rw.nh = nh;
+
+	/* next hop: new destination IP (used for DNAT) and MAC */
+	w[0] = rw.dip ? be32_to_cpu(new_daddr) : 0;
+	zx_mac_words(eth.h_dest, w + 1);
+	ret = zx_pp_write(adapter, ZX_PM_BASE, ZX_PM_NEXT_HOP, nh, w, 3);
+	if (ret)
+		goto err_nh;
+	zx_ppe_flow_entry(&rw, w);
+	ret = zx_pp_write(adapter, ZX_PM_BASE, ZX_PM_FLOW, flow, w, 3);
+	if (ret)
+		goto err_nh;
+	ret = zx_pp_write(adapter, ZX_CLA_BASE, ZX_CLA_AGING, slot[1], w, 0);
+	if (ret)
+		goto err_nh;
+	zx_ppe_hash_entry(&t, flow, port, w);
+	ret = zx_pp_write(adapter, ZX_CLA_BASE,
+			  slot[1] < ZX_PPE_HASH0_SIZE ? ZX_CLA_HASH0 : ZX_CLA_HASH1,
+			  slot[1] % ZX_PPE_HASH0_SIZE, w, 9);
+	if (ret)
+		goto err_nh;
+
+	e->cookie = f->cookie;
+	e->hash = slot[1];
+	e->flow = flow;
+	e->nh = nh;
+	e->subnet = subnet;
+	ret = rhashtable_insert_fast(&adapter->ppe_flows, &e->node, zx_ppe_ht_params);
+	if (ret)
+		goto err_entry;
+	set_bit(e->hash, adapter->ppe_hash_used);
+	adapter->ppe_count++;
+	return 0;
+
+err_entry:
+	zx_pp_write(adapter, ZX_CLA_BASE,
+		    slot[1] < ZX_PPE_HASH0_SIZE ? ZX_CLA_HASH0 : ZX_CLA_HASH1,
+		    slot[1] % ZX_PPE_HASH0_SIZE, w, 0);
+err_nh:
+	clear_bit(nh, adapter->ppe_nh_used);
+err_flow:
+	clear_bit(flow, adapter->ppe_flow_used);
+err_subnet:
+	zx_ppe_subnet_put(adapter, subnet);
+err_free:
+	kfree(e);
+	return ret;
+}
+
+static void zx_ppe_entry_remove(struct zx_eth_adapter *adapter,
+				struct zx_ppe_entry *e)
+{
+	zx_pp_write(adapter, ZX_CLA_BASE,
+		    e->hash < ZX_PPE_HASH0_SIZE ? ZX_CLA_HASH0 : ZX_CLA_HASH1,
+		    e->hash % ZX_PPE_HASH0_SIZE, NULL, 0);
+	zx_pp_write(adapter, ZX_CLA_BASE, ZX_CLA_AGING, e->hash, NULL, 0);
+	clear_bit(e->hash, adapter->ppe_hash_used);
+	clear_bit(e->flow, adapter->ppe_flow_used);
+	clear_bit(e->nh, adapter->ppe_nh_used);
+	zx_ppe_subnet_put(adapter, e->subnet);
+	adapter->ppe_count--;
+}
+
+static int zx_ppe_flow_del(struct zx_eth_adapter *adapter,
+			   struct flow_cls_offload *f)
+{
+	struct zx_ppe_entry *e;
+
+	e = rhashtable_lookup_fast(&adapter->ppe_flows, &f->cookie, zx_ppe_ht_params);
+	if (!e)
+		return -ENOENT;
+	rhashtable_remove_fast(&adapter->ppe_flows, &e->node, zx_ppe_ht_params);
+	zx_ppe_entry_remove(adapter, e);
+	kfree(e);
+	return 0;
+}
+
+/* No packet counters: report use from the entry's hit flag */
+static int zx_ppe_flow_stats(struct zx_eth_adapter *adapter,
+			     struct flow_cls_offload *f)
+{
+	struct zx_ppe_entry *e;
+	u32 hit;
+
+	e = rhashtable_lookup_fast(&adapter->ppe_flows, &f->cookie, zx_ppe_ht_params);
+	if (!e)
+		return -ENOENT;
+	if (zx_pp_read_word0(adapter, ZX_CLA_BASE, ZX_CLA_AGING, e->hash, &hit))
+		return -ETIMEDOUT;
+	if (hit & 1) {
+		f->stats.lastused = jiffies;
+		zx_pp_write(adapter, ZX_CLA_BASE, ZX_CLA_AGING, e->hash, NULL, 0);
+	}
+	return 0;
+}
+
+/* Values from the stock firmware */
+static const u32 zx_ppe_v4_index[5] = {
+	0x93929190, 0x97969594, 0x9b9a9998, 0x9f9e9d9c,
+	0x00150151,	/* bit 8: look up the hash banks */
+};
+
+static const u32 zx_ppe_v4_rule[16] = {
+	0x22038608, 0x000058a1, 0, 0, 0xf00ff000, 0xffffffff, 0xffffffff,
+	0x0fffffff, 0, 0, 0, 0, 0, 0, 0x00700000, 0x00092492,
+};
+
+static const u32 zx_ppe_sub[2] = { 0xfc000000, 0x00001fff };
+
+/* classifier L3 MTU and actions, packet modifier padding of short frames.
+ * The "other L3 packet" action (0x38c0cc) keeps the value 1 of the base
+ * driver: with the stock value 0, IPv6 addressed to our MACs was dropped
+ * once its trap was lifted (hardware LAN switching).
+ */
+static const u32 zx_ppe_regs[4] = { 0x38c088, 0x38c094, 0x38c098, 0x39c034 };
+static const u32 zx_ppe_reg_vals[4] = { 0x7fff, 4, 0x7fff7fff, 0x3d };
+
+/*
+ * Traps that send frames to the CPU before the flow lookup: the IPv4
+ * ethertype trap, and the registered packet class for TCP segments without
+ * payload (trap reason 0x2c), which would send every pure ACK of an
+ * offloaded connection through the software path.  Both are off while
+ * offload is active.  SYN, FIN and RST still reach the CPU, so conntrack
+ * sees connections open and close.  Hardware LAN switching also needs the
+ * IPv6 trap off.  Frames for the router, broadcast, multicast and unknown
+ * unicast still reach the CPU through the other traps.
+ */
+void zx_update_traps(struct zx_eth_adapter *adapter)
+{
+	void __iomem *base = adapter->base;
+	bool sw, fwd;
+	u32 val;
+
+	spin_lock(&adapter->trap_lock);
+	sw = READ_ONCE(adapter->sw_members);
+	fwd = READ_ONCE(adapter->ppe_active) || sw;
+	writel((fwd ? 0 : ETH_P_IP << 16) | (sw ? 0 : ETH_P_IPV6),
+	       base + ZX_SPA_TRAP_ETH_TYPE);
+	val = readl(base + ZX_SPA_UP_REG_PKT_EN + 8);
+	if (fwd)
+		val &= ~ZX_SPA_REG_PKT_TCP_ACK;
+	else
+		val |= ZX_SPA_REG_PKT_TCP_ACK;
+	writel(val, base + ZX_SPA_UP_REG_PKT_EN + 8);
+	spin_unlock(&adapter->trap_lock);
+}
+
+/*
+ * Frames to the ports' own addresses reach the CPU through the trap table,
+ * or with flow offload active, through the ONU MAC table, which marks them
+ * as routed so that the classifier sees them first.  Addresses beyond the
+ * trap table still reach the CPU as unknown unicast.  Called with ppe_lock
+ * held, after an address or the offload state changed.
+ */
+void zx_update_mac_tables(struct zx_eth_adapter *adapter)
+{
+	static const u8 none[ETH_ALEN];
+	void __iomem *base = adapter->base;
+	const u8 *addrs[ZX_NUM_PORTS];
+	int i, j, n = 0;
+	u32 w[2];
+
+	lockdep_assert_held(&adapter->ppe_lock);
+
+	/* each distinct port address once */
+	for (i = 0; i < ZX_NUM_PORTS; i++) {
+		struct net_device *dev = adapter->ports[i];
+
+		if (!dev)
+			continue;
+		for (j = 0; j < n; j++)
+			if (ether_addr_equal(addrs[j], dev->dev_addr))
+				break;
+		if (j == n)
+			addrs[n++] = dev->dev_addr;
+	}
+
+	for (i = 0; i < ZX_SPA_ONU_MAC_NUM; i++) {
+		zx_mac_words(adapter->ppe_active && i < n ? addrs[i] : none, w);
+		writel(w[0], base + ZX_SPA_ONU_MAC + i * 8);
+		writel(w[1], base + ZX_SPA_ONU_MAC + i * 8 + 4);
+	}
+	for (i = 0; i < ZX_SPA_TRAP_DMAC_NUM; i++)
+		zx_set_trap_dmac(adapter, i,
+				 !adapter->ppe_active && i < n ? addrs[i] : none);
+}
+
+/*
+ * Routed frames must reach the classifier: mark our MACs as routed (ONU
+ * MAC table) and stop trapping IPv4 and frames to our MACs straight to the
+ * CPU.  IPv6, ARP and VLAN tagged frames stay trapped.  Frames the
+ * classifier does not know still go to the CPU.
+ */
+static int zx_ppe_start(struct zx_eth_adapter *adapter)
+{
+	static const u32 cmd = 0x00010000;
+	void __iomem *base = adapter->base;
+	int i, ret;
+
+	for (i = 0; i < ZX_PPE_SUBNETS; i++)
+		adapter->ppe_subnet[i].refs = 0;
+	bitmap_zero(adapter->ppe_hash_used, ZX_PPE_HASH_SIZE);
+	bitmap_zero(adapter->ppe_flow_used, ZX_PPE_FLOWS);
+	bitmap_zero(adapter->ppe_nh_used, ZX_PPE_NEXT_HOPS);
+
+	for (i = 0; i < ARRAY_SIZE(zx_ppe_regs); i++) {
+		adapter->ppe_saved[i] = readl(base + zx_ppe_regs[i]);
+		writel(zx_ppe_reg_vals[i], base + zx_ppe_regs[i]);
+	}
+
+	ret = zx_pp_write(adapter, ZX_PM_BASE, ZX_PM_CMD, 0, &cmd, 1);
+	if (!ret)
+		ret = zx_pp_write(adapter, ZX_PM_BASE, ZX_PM_SUB, 0, zx_ppe_sub, 2);
+	if (!ret)
+		ret = zx_pp_write(adapter, ZX_CLA_BASE, ZX_CLA_EXTRA_RULE,
+				  ZX_PPE_V4_RULE, zx_ppe_v4_rule, 16);
+	if (!ret)
+		ret = zx_pp_write(adapter, ZX_CLA_BASE, ZX_CLA_EXTRA_INDEX,
+				  ZX_PPE_V4_INDEX, zx_ppe_v4_index, 5);
+	if (ret) {
+		for (i = 0; i < ARRAY_SIZE(zx_ppe_regs); i++)
+			writel(adapter->ppe_saved[i], base + zx_ppe_regs[i]);
+		return ret;
+	}
+
+	adapter->ppe_active = true;
+	zx_update_mac_tables(adapter);
+	zx_update_traps(adapter);
+	dev_info(adapter->dev, "hardware flow offload enabled\n");
+	return 0;
+}
+
+static void zx_ppe_flush(void *ptr, void *arg)
+{
+	struct zx_ppe_entry *e = ptr;
+
+	zx_ppe_entry_remove(arg, e);
+	kfree(e);
+}
+
+static void zx_ppe_stop(struct zx_eth_adapter *adapter)
+{
+	void __iomem *base = adapter->base;
+	int i, ret;
+
+	/* back to trapping everything, then drop the tables */
+	adapter->ppe_active = false;
+	zx_update_traps(adapter);
+	zx_update_mac_tables(adapter);
+
+	rhashtable_free_and_destroy(&adapter->ppe_flows, zx_ppe_flush, adapter);
+	ret = rhashtable_init(&adapter->ppe_flows, &zx_ppe_ht_params);
+	if (ret)
+		dev_err(adapter->dev, "failed to reinitialize flow table: %d\n", ret);
+	zx_pp_write(adapter, ZX_CLA_BASE, ZX_CLA_EXTRA_INDEX, ZX_PPE_V4_INDEX, NULL, 0);
+	zx_pp_write(adapter, ZX_CLA_BASE, ZX_CLA_EXTRA_RULE, ZX_PPE_V4_RULE, NULL, 0);
+	for (i = 0; i < ARRAY_SIZE(zx_ppe_regs); i++)
+		writel(adapter->ppe_saved[i], base + zx_ppe_regs[i]);
+	dev_info(adapter->dev, "hardware flow offload disabled\n");
+}
+
+static int zx_ppe_block_cb(enum tc_setup_type type, void *type_data, void *cb_priv)
+{
+	struct zx_eth_adapter *adapter = cb_priv;
+	struct flow_cls_offload *f = type_data;
+	int ret;
+
+	if (type != TC_SETUP_CLSFLOWER)
+		return -EOPNOTSUPP;
+
+	mutex_lock(&adapter->ppe_lock);
+	switch (f->command) {
+	case FLOW_CLS_REPLACE:
+		ret = zx_ppe_flow_add(adapter, f);
+		if (ret == -EOPNOTSUPP)
+			adapter->ppe_add_skip++;
+		else if (ret && ret != -EEXIST)
+			adapter->ppe_add_fail++;
+		break;
+	case FLOW_CLS_DESTROY:
+		ret = zx_ppe_flow_del(adapter, f);
+		break;
+	case FLOW_CLS_STATS:
+		ret = zx_ppe_flow_stats(adapter, f);
+		break;
+	default:
+		ret = -EOPNOTSUPP;
+	}
+	mutex_unlock(&adapter->ppe_lock);
+	return ret;
+}
+
+static LIST_HEAD(zx_ppe_block_cb_list);
+
+/* One callback for the flowtable of all port netdevs, so each flow is
+ * offered once.
+ */
+int zx_eth_setup_tc(struct net_device *dev, enum tc_setup_type type,
+		    void *type_data)
+{
+	struct zx_eth_priv *priv = netdev_priv(dev);
+	struct zx_eth_adapter *adapter = priv->adapter;
+	struct flow_block_offload *f = type_data;
+	struct flow_block_cb *block_cb;
+	int ret = 0;
+
+	if (type != TC_SETUP_FT)
+		return -EOPNOTSUPP;
+	if (f->binder_type != FLOW_BLOCK_BINDER_TYPE_CLSACT_INGRESS)
+		return -EOPNOTSUPP;
+
+	f->driver_block_list = &zx_ppe_block_cb_list;
+	block_cb = flow_block_cb_lookup(f->block, zx_ppe_block_cb, adapter);
+
+	switch (f->command) {
+	case FLOW_BLOCK_BIND:
+		if (block_cb) {
+			flow_block_cb_incref(block_cb);
+			return 0;
+		}
+		block_cb = flow_block_cb_alloc(zx_ppe_block_cb, adapter, adapter, NULL);
+		if (IS_ERR(block_cb))
+			return PTR_ERR(block_cb);
+		/* A ruleset reload binds the new flowtable before it unbinds the
+		 * old one, so keep the hardware on while any flowtable is bound.
+		 */
+		mutex_lock(&adapter->ppe_lock);
+		if (!adapter->ppe_users)
+			ret = zx_ppe_start(adapter);
+		if (!ret)
+			adapter->ppe_users++;
+		mutex_unlock(&adapter->ppe_lock);
+		if (ret) {
+			flow_block_cb_free(block_cb);
+			return ret;
+		}
+		flow_block_cb_incref(block_cb);
+		flow_block_cb_add(block_cb, f);
+		list_add_tail(&block_cb->driver_list, &zx_ppe_block_cb_list);
+		return 0;
+	case FLOW_BLOCK_UNBIND:
+		if (!block_cb)
+			return -ENOENT;
+		if (!flow_block_cb_decref(block_cb)) {
+			flow_block_cb_remove(block_cb, f);
+			list_del(&block_cb->driver_list);
+			mutex_lock(&adapter->ppe_lock);
+			if (!--adapter->ppe_users)
+				zx_ppe_stop(adapter);
+			mutex_unlock(&adapter->ppe_lock);
+		}
+		return 0;
+	default:
+		return -EOPNOTSUPP;
+	}
+}
diff --git a/drivers/net/ethernet/zte/zx279128s-switch.c b/drivers/net/ethernet/zte/zx279128s-switch.c
new file mode 100644
index 0000000000..b76a057f1d
--- /dev/null
+++ b/drivers/net/ethernet/zte/zx279128s-switch.c
@@ -0,0 +1,368 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ZTE zx279128s Ethernet switch: hardware LAN switching
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/etherdevice.h>
+#include <linux/if_bridge.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/rtnetlink.h>
+#include <net/switchdev.h>
+
+#include "zx279128s-eth.h"
+
+/* ---- Hardware LAN switching ---------------------------------------- */
+
+/*
+ * The bridge block forwards a unicast frame from one switch port to another
+ * when its source table knows the destination and both ports are members
+ * of the port VLAN (1).  Normally learning is off and the VLAN table empty,
+ * so every frame goes to the Linux bridge.  Ports of the same Linux bridge
+ * switch between each other in hardware, as long as the bridge does not
+ * filter VLANs and the port forwards, learns, and is neither isolated nor
+ * locked (zx_sw_bridge()); any other port stays on the CPU path:
+ * - VLAN 1 has them as untagged members, each may send only to the other
+ *   ports of its bridge (egress mask 0 for all other ports, so nothing is
+ *   switched to or from a port outside the bridge), and only they learn;
+ * - the IPv4 and IPv6 ethertype traps are off (zx_update_traps()).
+ * Only known unicast is switched.  Frames for the router, broadcast,
+ * multicast and unknown unicast reach the CPU alone, and the Linux bridge
+ * floods them, also to the hosts on its other ports (Wi-Fi).  So received
+ * frames are never marked as forwarded already (offload_fwd_mark).
+ * A host that moves loses its hardware entry: the entries of a port are
+ * dropped when its link goes down, and a MAC is dropped as soon as the
+ * Linux bridge learns it on a port that is not a switch port.
+ */
+static int zx_sbrg_ind(struct zx_eth_adapter *adapter, u32 cmd)
+{
+	u32 val;
+
+	writel(cmd, adapter->base + ZX_SBRG_IND_CMD);
+	return readl_poll_timeout_atomic(adapter->base + ZX_SBRG_IND_DONE, val,
+					 val & 1, 0, 1000);
+}
+
+static void zx_sbrg_flush(struct zx_eth_adapter *adapter, u8 brports)
+{
+	void __iomem *reg = adapter->base + ZX_SBRG_PORT_CTRL;
+	u32 val = readl(reg) & ~(ZX_SBRG_FLUSH_PORTS | ZX_SBRG_FLUSH);
+
+	writel(val | FIELD_PREP(ZX_SBRG_FLUSH_PORTS, brports) | ZX_SBRG_FLUSH, reg);
+	usleep_range(1000, 2000);
+	writel(val, reg);
+}
+
+/* Remove one MAC from the source table: scan the four RAMs */
+static void zx_sbrg_fdb_del(struct zx_eth_adapter *adapter, const u8 *mac)
+{
+	static const int sizes[4] = { 1024, 256, 512, 512 };
+	void __iomem *base = adapter->base;
+	u32 hi = mac[0] << 16 | mac[1] << 8 | mac[2];
+	u32 lo = mac[3] << 24 | mac[4] << 16 | mac[5] << 8;
+	int ram, i;
+
+	for (ram = 0; ram < 4; ram++) {
+		int bank_size = sizes[ram];
+
+		for (i = 0; i < bank_size; i++) {
+			if (zx_sbrg_ind(adapter, ZX_SBRG_IND_READ | ram << 22 | i))
+				return;
+			if (!(readl(base + ZX_SBRG_IND_DATA(2)) & 0xf0) ||
+			    (readl(base + ZX_SBRG_IND_DATA(1)) & 0xffffff) != hi ||
+			    (readl(base + ZX_SBRG_IND_DATA(0)) & 0xffffff00) != lo)
+				continue;
+			writel(0, base + ZX_SBRG_IND_DATA(2));
+			writel(0, base + ZX_SBRG_IND_DATA(1));
+			writel(0, base + ZX_SBRG_IND_DATA(0));
+			if (zx_sbrg_ind(adapter, ram << 22 | i))
+				return;
+			adapter->sw_fdb_deleted++;
+		}
+	}
+}
+
+/* the bridge a port switches in, or NULL if it stays on the CPU path */
+static struct net_device *zx_sw_bridge(struct net_device *dev)
+{
+	struct net_device *br;
+
+	ASSERT_RTNL();
+	if (!dev || !netif_is_bridge_port(dev))
+		return NULL;
+	br = netdev_master_upper_dev_get(dev);
+	if (!br || br_vlan_enabled(br) ||
+	    br_port_get_stp_state(dev) != BR_STATE_FORWARDING ||
+	    !br_port_flag_is_set(dev, BR_LEARNING) ||
+	    br_port_flag_is_set(dev, BR_ISOLATED) ||
+	    br_port_flag_is_set(dev, BR_PORT_LOCKED))
+		return NULL;
+	return br;
+}
+
+static void zx_sw_apply(struct zx_eth_adapter *adapter)
+{
+	struct net_device *br[ZX_NUM_PORTS] = {};
+	void __iomem *base = adapter->base;
+	u8 egress[ZX_NUM_PORTS] = {};
+	u8 members = 0;
+	u32 vlan = 0;
+	int i, j;
+
+	lockdep_assert_held(&adapter->sw_lock);
+
+	rtnl_lock();
+	for (i = 0; adapter->sw_ready && i < ZX_NUM_PORTS; i++)
+		br[i] = zx_sw_bridge(adapter->ports[i]);
+	rtnl_unlock();
+
+	for (i = 0; i < ZX_NUM_PORTS; i++)
+		for (j = 0; j < ZX_NUM_PORTS; j++)
+			if (j != i && br[i] && br[j] == br[i])
+				egress[i] |= BIT(ZX_BRPORT(j));
+	for (i = 0; i < ZX_NUM_PORTS; i++) {
+		if (!egress[i]) {
+			br[i] = NULL;
+			continue;
+		}
+		members |= BIT(ZX_BRPORT(i));
+		vlan |= ZX_SBRG_VLAN_UNTAG(ZX_BRPORT(i));
+	}
+
+	spin_lock_bh(&adapter->sw_fdb_lock);
+	memcpy(adapter->sw_bridge, br, sizeof(br));
+	spin_unlock_bh(&adapter->sw_fdb_lock);
+
+	/* traps back on before the tables go, off once they are in place */
+	if (!members) {
+		WRITE_ONCE(adapter->sw_members, 0);
+		zx_update_traps(adapter);
+	}
+
+	/* no learning while the tables change, then start empty */
+	writel(0, base + ZX_SBRG_LEARN);
+	writel(members ? vlan | ZX_SBRG_VLAN_VALID : 0,
+	       base + ZX_SBRG_IND_DATA(0));
+	if (zx_sbrg_ind(adapter, ZX_SBRG_MEM_VLAN << 22 | 1))
+		dev_warn(adapter->dev, "bridge VLAN table access timed out\n");
+	/* the reset value lets a port send to every other port */
+	for (i = 0; i < ZX_NUM_PORTS; i++)
+		writel(members ? egress[i] : (u8)~BIT(ZX_BRPORT(i)),
+		       base + ZX_SBRG_EGRESS(ZX_BRPORT(i)));
+	zx_sbrg_flush(adapter, 0xff);
+	writel(members, base + ZX_SBRG_LEARN);
+	if (members) {
+		WRITE_ONCE(adapter->sw_members, members);
+		zx_update_traps(adapter);
+	}
+	dev_dbg(adapter->dev, "hardware LAN switching %s (bridge ports %#x)\n",
+		members ? "on" : "off", members);
+}
+
+void zx_sw_work(struct work_struct *work)
+{
+	struct zx_eth_adapter *adapter = container_of(work, struct zx_eth_adapter,
+						      sw_work);
+	u8 macs[ZX_SW_FDB_QUEUE][ETH_ALEN];
+	bool recompute, overflow;
+	unsigned int n, i;
+	u8 flush;
+
+	spin_lock_bh(&adapter->sw_fdb_lock);
+	recompute = adapter->sw_recompute;
+	overflow = adapter->sw_fdb_overflow;
+	flush = adapter->sw_flush;
+	n = adapter->sw_fdb_count;
+	memcpy(macs, adapter->sw_fdb_mac, n * ETH_ALEN);
+	adapter->sw_recompute = false;
+	adapter->sw_fdb_overflow = false;
+	adapter->sw_flush = 0;
+	adapter->sw_fdb_count = 0;
+	spin_unlock_bh(&adapter->sw_fdb_lock);
+
+	mutex_lock(&adapter->sw_lock);
+	if (recompute) {
+		zx_sw_apply(adapter);
+	} else if (READ_ONCE(adapter->sw_members)) {
+		if (overflow)
+			flush = 0xff;
+		if (flush)
+			zx_sbrg_flush(adapter, flush);
+		for (i = 0; !overflow && i < n; i++)
+			zx_sbrg_fdb_del(adapter, macs[i]);
+	}
+	mutex_unlock(&adapter->sw_lock);
+}
+
+void zx_sw_port_down(struct zx_eth_adapter *adapter, int port)
+{
+	if (!READ_ONCE(adapter->sw_members))
+		return;
+	spin_lock_bh(&adapter->sw_fdb_lock);
+	adapter->sw_flush |= BIT(ZX_BRPORT(port));
+	spin_unlock_bh(&adapter->sw_fdb_lock);
+	schedule_work(&adapter->sw_work);
+}
+
+static bool zx_sw_is_port(struct zx_eth_adapter *adapter,
+			  const struct net_device *dev)
+{
+	int i;
+
+	for (i = 0; i < ZX_NUM_PORTS; i++)
+		if (adapter->ports[i] == dev)
+			return true;
+	return false;
+}
+
+static void zx_sw_recompute(struct zx_eth_adapter *adapter)
+{
+	spin_lock_bh(&adapter->sw_fdb_lock);
+	adapter->sw_recompute = true;
+	spin_unlock_bh(&adapter->sw_fdb_lock);
+	schedule_work(&adapter->sw_work);
+}
+
+/* a switch port joined or left a bridge */
+static int zx_sw_netdev_event(struct notifier_block *nb, unsigned long event,
+			      void *ptr)
+{
+	struct zx_eth_adapter *adapter = container_of(nb, struct zx_eth_adapter,
+						      sw_netdev_nb);
+	struct net_device *dev = netdev_notifier_info_to_dev(ptr);
+
+	if (event == NETDEV_CHANGEUPPER && zx_sw_is_port(adapter, dev))
+		zx_sw_recompute(adapter);
+	return NOTIFY_DONE;
+}
+
+static bool zx_sw_dev_check(const struct net_device *dev)
+{
+	return zx_eth_is_port(dev);
+}
+
+/* something that decides whether the port switches (zx_sw_bridge()) */
+static int zx_sw_port_attr_set(struct net_device *dev, const void *ctx,
+			       const struct switchdev_attr *attr,
+			       struct netlink_ext_ack *extack)
+{
+	struct zx_eth_priv *priv = netdev_priv(dev);
+
+	switch (attr->id) {
+	case SWITCHDEV_ATTR_ID_PORT_PRE_BRIDGE_FLAGS:
+		/* the CPU path handles what the switch does not */
+		return 0;
+	case SWITCHDEV_ATTR_ID_PORT_STP_STATE:
+	case SWITCHDEV_ATTR_ID_PORT_BRIDGE_FLAGS:
+	case SWITCHDEV_ATTR_ID_BRIDGE_VLAN_FILTERING:
+		zx_sw_recompute(priv->adapter);
+		return 0;
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int zx_sw_switchdev_blocking_event(struct notifier_block *nb,
+					  unsigned long event, void *ptr)
+{
+	struct net_device *dev = switchdev_notifier_info_to_dev(ptr);
+	int err;
+
+	if (event != SWITCHDEV_PORT_ATTR_SET)
+		return NOTIFY_DONE;
+	err = switchdev_handle_port_attr_set(dev, ptr, zx_sw_dev_check,
+					     zx_sw_port_attr_set);
+	return notifier_from_errno(err);
+}
+
+/* the Linux bridge learned a MAC; if it is on a non-switch port of a
+ * switching bridge (Wi-Fi), the host left its switch port
+ */
+static int zx_sw_switchdev_event(struct notifier_block *nb,
+				 unsigned long event, void *ptr)
+{
+	struct zx_eth_adapter *adapter = container_of(nb, struct zx_eth_adapter,
+						      sw_switchdev_nb);
+	struct net_device *dev = switchdev_notifier_info_to_dev(ptr);
+	struct switchdev_notifier_fdb_info *fdb;
+	struct net_device *br;
+	bool queued = false;
+	int i;
+
+	if (event != SWITCHDEV_FDB_ADD_TO_DEVICE || zx_sw_is_port(adapter, dev))
+		return NOTIFY_DONE;
+	fdb = container_of(ptr, struct switchdev_notifier_fdb_info, info);
+	if (fdb->is_local)
+		return NOTIFY_DONE;
+
+	rcu_read_lock();
+	br = netdev_master_upper_dev_get_rcu(dev);
+	rcu_read_unlock();
+	if (!br)
+		return NOTIFY_DONE;
+
+	spin_lock_bh(&adapter->sw_fdb_lock);
+	for (i = 0; i < ZX_NUM_PORTS; i++) {
+		if (adapter->sw_bridge[i] != br)
+			continue;
+		if (adapter->sw_fdb_count < ZX_SW_FDB_QUEUE)
+			memcpy(adapter->sw_fdb_mac[adapter->sw_fdb_count++],
+			       fdb->addr, ETH_ALEN);
+		else
+			adapter->sw_fdb_overflow = true;
+		queued = true;
+		break;
+	}
+	spin_unlock_bh(&adapter->sw_fdb_lock);
+	if (queued)
+		schedule_work(&adapter->sw_work);
+	return NOTIFY_DONE;
+}
+
+/* Follow the bridges the ports are in; called at the end of probe */
+void zx_sw_init(struct zx_eth_adapter *adapter)
+{
+	int ret;
+
+	adapter->sw_netdev_nb.notifier_call = zx_sw_netdev_event;
+	adapter->sw_switchdev_nb.notifier_call = zx_sw_switchdev_event;
+	adapter->sw_switchdev_blocking_nb.notifier_call =
+		zx_sw_switchdev_blocking_event;
+
+	ret = register_netdevice_notifier(&adapter->sw_netdev_nb);
+	if (ret)
+		goto err_netdev;
+
+	ret = register_switchdev_notifier(&adapter->sw_switchdev_nb);
+	if (ret)
+		goto err_switchdev;
+
+	ret = register_switchdev_blocking_notifier(&adapter->sw_switchdev_blocking_nb);
+	if (ret)
+		goto err_blocking;
+
+	adapter->sw_ready = true;
+	return;
+
+err_blocking:
+	unregister_switchdev_notifier(&adapter->sw_switchdev_nb);
+err_switchdev:
+	unregister_netdevice_notifier(&adapter->sw_netdev_nb);
+err_netdev:
+	dev_warn(adapter->dev,
+		 "no bridge notifications: hardware LAN switching unavailable\n");
+}
+
+void zx_sw_fini(struct zx_eth_adapter *adapter)
+{
+	if (!adapter->sw_ready)
+		return;
+
+	adapter->sw_ready = false;
+	unregister_switchdev_blocking_notifier(&adapter->sw_switchdev_blocking_nb);
+	unregister_switchdev_notifier(&adapter->sw_switchdev_nb);
+	unregister_netdevice_notifier(&adapter->sw_netdev_nb);
+}
-- 
2.55.0



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

* [PATCH v2 19/24] dt-bindings: spi: Add ZTE ZX family SPI flash controller binding
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (17 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 18/24] net: ethernet: zte: Add ZX279128S Ethernet switch driver Navid Ghahremani
@ 2026-10-14  7:20 ` Navid Ghahremani
  2026-10-14 20:33   ` Stefan Dösinger
  2026-10-14  7:21 ` [PATCH v2 20/24] spi: zx: Add ZTE ZX family SPI flash controller driver Navid Ghahremani
                   ` (5 subsequent siblings)
  24 siblings, 1 reply; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:20 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Mark Brown,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, linux-spi,
	devicetree, linux-kernel

Add Device Tree binding for the ZTE ZX family SPI flash controller,
supporting both ZX29 and ZX279128S SoCs.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 .../bindings/spi/zte,zx279128s-spifc.yaml     | 72 +++++++++++++++++++
 1 file changed, 72 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/spi/zte,zx279128s-spifc.yaml

diff --git a/Documentation/devicetree/bindings/spi/zte,zx279128s-spifc.yaml b/Documentation/devicetree/bindings/spi/zte,zx279128s-spifc.yaml
new file mode 100644
index 0000000000..201fe46b2f
--- /dev/null
+++ b/Documentation/devicetree/bindings/spi/zte,zx279128s-spifc.yaml
@@ -0,0 +1,72 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/spi/zte,zx279128s-spifc.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx279128s SPI flash controller
+
+maintainers:
+  - Stefan Dösinger <stefandoesinger@gmail.com>
+  - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description:
+  The SPI flash controller of the ZTE zx279128s runs flash operations (an
+  opcode with optional address, dummy and data phases) on one chip select.
+  The data phase can use one, two or four lines.
+
+allOf:
+  - $ref: spi-controller.yaml#
+
+properties:
+  compatible:
+    enum:
+      - zte,zx29-qspi
+      - zte,zx279128s-spifc
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: work clock, the source of the SPI clock
+      - description: register (APB) clock
+
+  clock-names:
+    items:
+      - const: wclk
+      - const: pclk
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - clock-names
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/zte,zx279128s-crm.h>
+    #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+    spi@94406000 {
+        compatible = "zte,zx279128s-spifc";
+        reg = <0x94406000 0x1000>;
+        interrupts = <GIC_SPI 13 IRQ_TYPE_LEVEL_HIGH>;
+        clocks = <&lsp0crpm ZX279128S_LSP0_SPI_WCLK>,
+                 <&lsp0crpm ZX279128S_LSP0_SPI_PCLK>;
+        clock-names = "wclk", "pclk";
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        flash@0 {
+            compatible = "spi-nand";
+            reg = <0>;
+            spi-max-frequency = <50000000>;
+            spi-rx-bus-width = <2>;
+        };
+    };
-- 
2.55.0



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

* [PATCH v2 20/24] spi: zx: Add ZTE ZX family SPI flash controller driver
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (18 preceding siblings ...)
  2026-10-14  7:20 ` [PATCH v2 19/24] dt-bindings: spi: Add ZTE ZX family SPI flash controller binding Navid Ghahremani
@ 2026-10-14  7:21 ` Navid Ghahremani
  2026-10-14 19:28   ` Stefan Dösinger
  2026-10-14  7:21 ` [PATCH v2 21/24] dt-bindings: usb: Add ZTE ZX279128S DWC3 controller Navid Ghahremani
                   ` (4 subsequent siblings)
  24 siblings, 1 reply; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:21 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Mark Brown,
	Philipp Zabel, linux-kernel, linux-spi

Add support for the SPI flash controller found on ZTE ZX family SoCs
(ZX297520V3, ZX279128S). Implement tuned PIO to avoid redundant FIFO
depth reads, dynamic CCF clock scaling, 4KB operation chunking, and
optional DMA support.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 drivers/spi/Kconfig  |  15 +
 drivers/spi/Makefile |   1 +
 drivers/spi/spi-zx.c | 728 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 744 insertions(+)
 create mode 100644 drivers/spi/spi-zx.c

diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index a0f37e2df7..5ab0cec218 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -1320,6 +1320,21 @@ config SPI_XTENSA_XTFPGA
 	  16 bit words in SPI mode 0, automatically asserting CS on transfer
 	  start and deasserting on end.
 
+config SPI_ZX
+	tristate "ZTE ZX family SPI flash controller"
+	depends on ARCH_ZTE || COMPILE_TEST
+	depends on OF && HAS_IOMEM
+	depends on SPI_MEM
+	help
+	  This enables support for the SPI flash controller found on ZTE ZX
+	  family SoCs (ZX297520V3, ZX279128S, etc.), through which they reach
+	  boot flash (such as SPI NAND on the ZTE ZXHN H3600).
+	  This controller operates flash transactions through a hardware FIFO
+	  or optional DMA engine.
+	  This driver does not support generic SPI. It only implements the
+	  spi-mem interface.
+
+
 config SPI_ZYNQ_QSPI
 	tristate "Xilinx Zynq QSPI controller"
 	depends on ARCH_ZYNQ || COMPILE_TEST
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index a18814fbde..3e9ea1e6fa 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -170,6 +170,7 @@ obj-$(CONFIG_SPI_XCOMM)		+= spi-xcomm.o
 obj-$(CONFIG_SPI_XILINX)		+= spi-xilinx.o
 obj-$(CONFIG_SPI_XLP)			+= spi-xlp.o
 obj-$(CONFIG_SPI_XTENSA_XTFPGA)		+= spi-xtensa-xtfpga.o
+obj-$(CONFIG_SPI_ZX)			+= spi-zx.o
 obj-$(CONFIG_SPI_ZYNQ_QSPI)		+= spi-zynq-qspi.o
 obj-$(CONFIG_SPI_ZYNQMP_GQSPI)		+= spi-zynqmp-gqspi.o
 obj-$(CONFIG_SPI_AMD)			+= spi-amd.o spi-amd-pci.o
diff --git a/drivers/spi/spi-zx.c b/drivers/spi/spi-zx.c
new file mode 100644
index 0000000000..edcad6008d
--- /dev/null
+++ b/drivers/spi/spi-zx.c
@@ -0,0 +1,728 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) ZTE (for HW register defines and initial PIO implementation)
+ * Copyright (C) Stefan Dösinger 2026
+ * Author: Stefan Dösinger <stefandoesinger@gmail.com>
+ *
+ * This device has certain similarities to stm32-qspi, but it isn't as simple as remapping register
+ * offsets.
+ */
+
+//#define DEBUG
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/dmaengine.h>
+#include <linux/dma-mapping.h>
+#include <linux/errno.h>
+#include <linux/gpio/consumer.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/pinctrl/consumer.h>
+#include <linux/pm_runtime.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+#include <linux/sizes.h>
+#include <linux/spi/spi-mem.h>
+
+#define MODULE_NAME "zx-qspi"
+
+struct zx_qspi {
+	struct spi_controller *ctrl;
+	struct reset_control *rstc;
+	phys_addr_t phys_base;
+	void __iomem *io_base;
+	struct clk *pclk, *wclk;
+
+	struct completion dma_completion;
+	struct sg_table sgt;
+	struct dma_async_tx_descriptor *dma_desc;
+	struct dma_chan *cur_dma_ch;
+	unsigned int speed_hz;
+
+	/* TODO: Do I need locks? */
+};
+
+#define ZX_QSPI_FIFO_SIZE			16
+
+/* Register and flag defines are copypasted from ZTE's kernel */
+
+/*
+ * spifc register
+ *
+ * ZX_QSPI_VER_REG: This is a bit of a misnomer. The mmio areas of a lot of hardware on the ZX
+ * platform starts with an identifier. 0x01040000 is the identifier of the SPI and QSPI devices,
+ * so VER_REG will always contain 0x01040000.
+ */
+#define ZX_QSPI_VER_REG				0x0
+#define ZX_QSPI_START_REG			0x4
+#define ZX_QSPI_EN_REG				0x8
+#define ZX_QSPI_CTRL0_REG			0xc
+#define ZX_QSPI_CTRL1_REG			0x10
+#define ZX_QSPI_CTRL2_REG			0x14
+#define ZX_QSPI_BYTE_NUM_REG			0x18
+#define ZX_QSPI_ADDR_REG			0x1c
+#define ZX_QSPI_INS_REG				0x20
+#define ZX_QSPI_TIMING_REG			0x24
+#define ZX_QSPI_INT_EN_REG			0x28
+#define ZX_QSPI_INT_RAW_REG			0x2c
+#define ZX_QSPI_INT_SW_CLR_REG			0x30
+#define ZX_QSPI_SW_REG				0x34
+#define ZX_QSPI_DATA_REG			0x38
+
+/*
+ * spifc start register
+ */
+#define ZX_QSPI_START				BIT(0)
+#define ZX_QSPI_BUSY				BIT(0)
+
+/*
+ * spifc en register
+ */
+#define ZX_QSPI_EN				BIT(0)
+#define ZX_QSPI_EN_BACK				BIT(1)
+
+/*
+ * spifc ctrl0 register
+ *
+ * Guesswork: ZX_QSPI_*XFIFO_THRES are probably 4 bit counts that set at which point an int is
+ * raised. E.g. if ZX_QSPI_TXFIFO_THRES=5, it will raise an int when the RX fifo has 5 32 bit
+ * values ready to read. They also have an effect on DMA. If the THRES flags are set, the burst
+ * size needs to be big enough or the DMA transfer times out.
+ *
+ * I don't know what ZX_QSPI_WDOG_EN does. Likely there is some correlation with INT_WDOG_OVERRUN,
+ * but all I could observe is that setting ZX_QSPI_WDOG_EN breaks write operations with DMA.
+ */
+#define ZX_QSPI_SCLK_PAUSE_CLR_ALLOW		BIT(17)
+#define ZX_QSPI_SCLK_PAUSE_EN			BIT(16)
+#define ZX_QSPI_TXFIFO_CLR			BIT(15)
+#define ZX_QSPI_RXFIFO_CLR			BIT(14)
+#define ZX_QSPI_TXFIFO_THRES			BIT(10)
+#define ZX_QSPI_RXFIFO_THRES			BIT(6)
+#define ZX_QSPI_TX_DMA_EN			BIT(5)
+#define ZX_QSPI_RX_DMA_EN			BIT(4)
+#define ZX_QSPI_WDOG_EN				BIT(3)
+#define ZX_QSPI_SPI_MODE			BIT(1)
+#define ZX_QSPI_WR_PROTECT			BIT(0)
+
+/*
+ * spifc ctrl1 register
+ */
+#define ZX_QSPI_ADDR_TX_EN			BIT(4)
+#define ZX_QSPI_DUMMY_TX_EN			BIT(2)
+#define ZX_QSPI_READ_DATA_EN			BIT(1)
+#define ZX_QSPI_WRITE_DATA_EN			BIT(0)
+
+/*
+ * spifc ctrl2 register
+ */
+#define ZX_QSPI_DUMMY_BYTE_NUM			(12)		/* [12:15} */
+#define ZX_QSPI_DUMMY_BIT_NUM			(8)		/* [8:10] */
+#define ZX_QSPI_ADDR_BYTE_NUM			(5)		/* [5:6] */
+#define ZX_QSPI_ADDR_MULTI_LINE_EN		BIT(4)
+#define ZX_QSPI_DATA_MULTI_LINE_EN		BIT(2)
+#define ZX_QSPI_TRANS_MOD			BIT(0)
+
+#define ADDR_BYTE_WIDTH_8			(0)
+#define ADDR_BYTE_WIDTH_16			(1)
+#define ADDR_BYTE_WIDTH_24			(2)
+#define ADDR_BYTE_WIDTH_32			(3)
+
+/*
+ * spifc timing register
+ */
+#define ZX_QSPI_T_CS_SETUP			11	/* [11:13] */
+#define ZX_QSPI_T_CS_HOLD			6	/* [8:6] */
+#define ZX_QSPI_T_CS_DESEL			0	/* [0:3] */
+
+/*
+ * spifc int_en, int_raw, int_sw_clr register
+ */
+#define ZX_QSPI_INT_TX_BYD_THRES		BIT(7)
+#define ZX_QSPI_INT_RX_BYD_THRES		BIT(6)
+#define ZX_QSPI_INT_TX_UNDERRUN			BIT(5)
+#define ZX_QSPI_INT_RX_OVERRUN			BIT(4)
+#define ZX_QSPI_INT_WDOG_OVERRUN		BIT(2)
+#define ZX_QSPI_INT_FMT_ERR			BIT(1)
+#define ZX_QSPI_INT_CMD_END			BIT(0)
+#define ZX_QSPI_INT_ALL_MASK			GENMASK(7, 0)
+#define ZX_QSPI_INT_EN_MASK			GENMASK(5, 0)
+
+/*
+ * spifc sw register
+ */
+#define ZX_QSPI_TX_FIFO_CNT_MASK		GENMASK(20, 16)	/* [16:20] */
+#define ZX_QSPI_RX_FIFO_CNT_MASK		GENMASK(12, 8)	/* [8:12] */
+#define ZX_QSPI_TX_BYD_THRES			BIT(5)
+#define ZX_QSPI_RX_BYD_THRES			BIT(4)
+#define ZX_QSPI_SCLK_PAUSE_FLAG			BIT(3)
+#define ZX_QSPI_WAIT_FLAG			BIT(2)
+#define ZX_QSPI_FORMAT_ERR			BIT(1)
+
+static irqreturn_t zx_qspi_irq(int irq, void *dev_id)
+{
+	struct zx_qspi *qspi = dev_id;
+	struct device *dev = &qspi->ctrl->dev;
+	u32 sw, raw = 0;
+	/* The RAW_REG register reports the unmodified hardware status while the SW/CLR reg reports
+	 * RAW_REG & INT_EN_REG.
+	 *
+	 * Beware that clearing a FIFO flag here doesn't do much - the RX fifo will still have data
+	 * and the TX fifo will still have room, so it will re-raise the same int again. The
+	 * solution is to mask the RX/TX ints and re-enable them after reading or filling the fifo.
+	 *
+	 * By and large IRQs from this controller are not very helpful. If we're in PIO mode we are
+	 * forced to do a lot of busy waiting anyway because we have to drain/fill FIFOs every 64
+	 * bytes. In ad-hoc benchmarking, doing PIO with IRQ notifications resulted in both slower
+	 * reads as well as slower parallel CPU computation.
+	 *
+	 * In DMA mode we just piggypack on the DMA IRQ.
+	 */
+	sw = readl_relaxed(qspi->io_base + ZX_QSPI_INT_SW_CLR_REG);
+	raw = readl_relaxed(qspi->io_base + ZX_QSPI_INT_RAW_REG);
+
+	dev_dbg(dev, "Got an IRQ, sw %x raw %x\n", sw, raw);
+
+	writel_relaxed(0, qspi->io_base + ZX_QSPI_INT_EN_REG);
+
+	return IRQ_HANDLED;
+}
+
+/* IRQs for PIO aren't worth the bother. The hardware is sloppy with IRQs in larger (>2000 byte)
+ * transfers and in my testing it does not free up measurable amounts of CPU resources. DMA is the
+ * answer to better performance, not tuning PIO.
+ *
+ * To get IRQs for PIO operations set ZX_QSPI_{R/T}XFIFO_THRES to a non-zero value and enable
+ * ZX_QSPI_INT_{R/T}X_BYD_THES in ZX_QSPI_INT_EN_REG. Beware that the TX fifo will report empty
+ * until it is fed with data, so only unmask this interrupt after the pipe is full. Then either
+ * signal completion from zx_qspi_irq when it sees ZX_QSPI_INT_{R/T}X_BYD_THES and command
+ * completion or fill new data from the IRQ handler.
+ */
+static int zx_qspi_read_fifo(struct zx_qspi *qspi, u32 len, u8 *val)
+{
+	struct device *dev = &qspi->ctrl->dev;
+	u32 *p = (uint32_t *)val;
+	u32 fifo, readval;
+	u32 cnt = 0;
+	int ret;
+
+	len = ALIGN(len, 4) >> 2;
+	while (cnt < len) {
+		ret = readl_relaxed_poll_timeout_atomic(qspi->io_base + ZX_QSPI_SW_REG,
+							 fifo, fifo & ZX_QSPI_RX_FIFO_CNT_MASK, 1,
+							 30000);
+		if (ret) {
+			dev_err(dev, "fifo timeout (len:%d stat:%#x)\n",
+				len << 2, fifo);
+			return ret;
+		}
+		fifo = min((fifo & ZX_QSPI_RX_FIFO_CNT_MASK) >> 8, len - cnt);
+		while (fifo) {
+			readval = readl_relaxed(qspi->io_base + ZX_QSPI_DATA_REG);
+			p[cnt++] = readval;
+			fifo--;
+		}
+	}
+
+	return 0;
+}
+
+static int zx_qspi_write_fifo(struct zx_qspi *qspi, u32 len, u8 *val)
+{
+	struct device *dev = &qspi->ctrl->dev;
+	u32 *p = (uint32_t *)val;
+	u32 cnt = 0;
+	u32 fifo;
+	int ret;
+
+	len = ALIGN(len, 4) >> 2;
+	while (cnt < len) {
+		ret = readl_relaxed_poll_timeout_atomic(qspi->io_base + ZX_QSPI_SW_REG,
+							 fifo, fifo & ZX_QSPI_TX_FIFO_CNT_MASK, 1,
+							 30000);
+		if (ret) {
+			dev_err(dev, "fifo timeout (len:%d stat:%#x)\n",
+				len << 2, fifo);
+			return ret;
+		}
+		fifo = min((fifo & ZX_QSPI_TX_FIFO_CNT_MASK) >> 16, len - cnt);
+		while (fifo) {
+			writel_relaxed(p[cnt++], qspi->io_base + ZX_QSPI_DATA_REG);
+			fifo--;
+		}
+	}
+
+	return 0;
+}
+
+static int zx_qspi_tx(struct zx_qspi *qspi,
+		      const struct spi_mem_op *op)
+{
+	int (*tx_fifo)(struct zx_qspi *qspi, u32 len, u8 *val);
+	u32 len = op->data.nbytes;
+	u8 *buf;
+
+	if (!op->data.nbytes)
+		return 0;
+
+	if (op->data.dir == SPI_MEM_DATA_IN) {
+		tx_fifo = zx_qspi_read_fifo;
+		buf = op->data.buf.in;
+	} else {
+		tx_fifo = zx_qspi_write_fifo;
+		buf = (u8 *)op->data.buf.out;
+	}
+
+	return tx_fifo(qspi, len, buf);
+}
+
+static void zx_qspi_callback_result(void *arg,
+				    const struct dmaengine_result *result)
+{
+	struct zx_qspi *qspi = arg;
+	struct device *dev = &qspi->ctrl->dev;
+	struct completion *dma_completion = &qspi->dma_completion;
+
+	if (result->residue)
+		dev_err(dev, "Residue %u not handled\n", result->residue);
+
+	if (result->result)
+		dev_err(dev, "Unexpected result %d\n", result->result);
+
+	complete(dma_completion);
+}
+
+static u32 zx_qspi_setup_dma(struct zx_qspi *qspi, const struct spi_mem_op *op)
+{
+	struct device *dev = &qspi->ctrl->dev;
+	enum dma_transfer_direction dma_dir;
+	u32 ctrl0 = 0;
+	int ret;
+
+	qspi->cur_dma_ch = NULL;
+	qspi->dma_desc = NULL;
+	if (op->data.nbytes < ZX_QSPI_FIFO_SIZE * 4)
+		return 0;
+
+	if (op->data.dir == SPI_MEM_DATA_IN && qspi->ctrl->dma_rx) {
+		dma_dir = DMA_DEV_TO_MEM;
+		ctrl0 |= ZX_QSPI_RX_DMA_EN;
+		qspi->cur_dma_ch = qspi->ctrl->dma_rx;
+	} else if (qspi->ctrl->dma_tx) {
+		dma_dir = DMA_MEM_TO_DEV;
+		ctrl0 |= ZX_QSPI_TX_DMA_EN;
+		qspi->cur_dma_ch = qspi->ctrl->dma_tx;
+	} else {
+		return 0;
+	}
+
+	reinit_completion(&qspi->dma_completion);
+	ret = spi_controller_dma_map_mem_op_data(qspi->ctrl, op, &qspi->sgt);
+	if (ret) {
+		dev_err(dev, "spi_controller_dma_map_mem_op_data = %d\n", ret);
+		return 0;
+	}
+
+	qspi->dma_desc = dmaengine_prep_slave_sg(qspi->cur_dma_ch, qspi->sgt.sgl, qspi->sgt.nents,
+				       dma_dir, DMA_PREP_INTERRUPT);
+	if (!qspi->dma_desc) {
+		dev_err(dev, "dmaengine_prep_slave_sg fail\n");
+		goto out;
+	}
+
+	return ctrl0;
+
+out:
+	spi_controller_dma_unmap_mem_op_data(qspi->ctrl, op, &qspi->sgt);
+	return 0;
+}
+
+static int zx_qspi_set_speed(struct zx_qspi *qspi, unsigned int max_hz)
+{
+	unsigned long rate;
+	long rounded;
+	int ret;
+
+	if (!max_hz)
+		return -EINVAL;
+	if (qspi->speed_hz == max_hz)
+		return 0;
+
+	rounded = clk_round_rate(qspi->wclk, max_hz);
+	if (rounded < 0)
+		return rounded;
+	if (!rounded || rounded > max_hz)
+		return -EINVAL;
+
+	ret = clk_set_rate(qspi->wclk, rounded);
+	if (ret)
+		return ret;
+	rate = clk_get_rate(qspi->wclk);
+	if (!rate || rate > max_hz)
+		return -EINVAL;
+	qspi->speed_hz = rate;
+
+	return 0;
+}
+
+static int zx_qspi_setup(struct spi_device *spi)
+{
+	struct zx_qspi *qspi = spi_controller_get_devdata(spi->controller);
+
+	return zx_qspi_set_speed(qspi, spi->max_speed_hz);
+}
+
+static int zx_qspi_exec_op(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct zx_qspi *qspi = spi_controller_get_devdata(mem->spi->controller);
+	u32 ctrl0, ctrl1 = 0, ctrl2 = 0, txstatus;
+	struct device *dev = &qspi->ctrl->dev;
+	dma_cookie_t cookie;
+	int ret;
+
+	ret = zx_qspi_set_speed(qspi, op->max_freq ?: mem->spi->max_speed_hz);
+	if (ret)
+		return ret;
+
+	dev_dbg(dev, "%s cmd:%#x buswidth:%d.%d.%d.%d addr:%#llx nbytes:%#x\n",
+		__func__, op->cmd.opcode, op->cmd.buswidth, op->addr.buswidth,
+		op->dummy.buswidth, op->data.buswidth,
+		op->addr.val, op->data.nbytes);
+
+	ctrl0 = readl_relaxed(qspi->io_base + ZX_QSPI_CTRL0_REG);
+	dev_dbg(dev, "ZX_QSPI_CTRL0_REG = 0x%08x\n", ctrl0);
+	ctrl0 &= ~(ZX_QSPI_TX_DMA_EN | ZX_QSPI_RX_DMA_EN);
+	ctrl0 |= (ZX_QSPI_RXFIFO_CLR | ZX_QSPI_TXFIFO_CLR);
+	/* What are these for? I don't know, the ZTE driver sets them. Removing them here makes it
+	 * possible to operate with a smaller burst size but presumably comes at a performance cost.
+	 * In simple benchmarking I have not found an impact though.
+	 */
+	ctrl0 |= ZX_QSPI_TXFIFO_THRES | ZX_QSPI_RXFIFO_THRES;
+
+	if (op->addr.nbytes && op->addr.buswidth) {
+		ctrl1 |= ZX_QSPI_ADDR_TX_EN;
+		switch (op->addr.buswidth * op->addr.nbytes) {
+		case 1:
+			ctrl2 |= (ADDR_BYTE_WIDTH_8 << ZX_QSPI_ADDR_BYTE_NUM);
+			break;
+		case 2:
+			ctrl2 |= (ADDR_BYTE_WIDTH_16 << ZX_QSPI_ADDR_BYTE_NUM);
+			break;
+		case 3:
+			ctrl2 |= (ADDR_BYTE_WIDTH_24 << ZX_QSPI_ADDR_BYTE_NUM);
+			break;
+		case 4:
+			ctrl2 |= (ADDR_BYTE_WIDTH_32 << ZX_QSPI_ADDR_BYTE_NUM);
+			break;
+		default:
+			dev_err(dev, "Unexpected addr.buswidth %u\n",
+				op->addr.buswidth * op->addr.nbytes);
+			return -EIO;
+		}
+	}
+
+	ctrl0 |= zx_qspi_setup_dma(qspi, op);
+
+	/* dummy config */
+	if (op->dummy.nbytes && op->dummy.buswidth) {
+		ctrl1 |= ZX_QSPI_DUMMY_TX_EN;
+		ctrl2 |= (0x1 << ZX_QSPI_DUMMY_BYTE_NUM);
+	}
+
+	/* data config */
+	if (op->data.nbytes) {
+		if (op->data.dir == SPI_MEM_DATA_IN)
+			ctrl1 |= ZX_QSPI_READ_DATA_EN;
+		else
+			ctrl1 |= ZX_QSPI_WRITE_DATA_EN;
+
+		if (op->data.buswidth == 0x4)
+			ctrl2 |= ZX_QSPI_DATA_MULTI_LINE_EN | ZX_QSPI_TRANS_MOD;
+		else if (op->data.buswidth == 0x2)
+			ctrl2 |= ZX_QSPI_DATA_MULTI_LINE_EN;
+	}
+
+	writel_relaxed(ctrl0, qspi->io_base + ZX_QSPI_CTRL0_REG);
+	writel_relaxed(ctrl1, qspi->io_base + ZX_QSPI_CTRL1_REG);
+	writel_relaxed(ctrl2, qspi->io_base + ZX_QSPI_CTRL2_REG);
+
+	/* clear int */
+	writel_relaxed(ZX_QSPI_INT_ALL_MASK, qspi->io_base + ZX_QSPI_INT_SW_CLR_REG);
+
+	/* data len */
+	if (op->data.nbytes)
+		writel_relaxed(op->data.nbytes - 1, qspi->io_base + ZX_QSPI_BYTE_NUM_REG);
+
+	/* addr */
+	if (op->addr.nbytes && op->addr.buswidth)
+		writel_relaxed(op->addr.val, qspi->io_base + ZX_QSPI_ADDR_REG);
+
+	/* cmd */
+	writel_relaxed(op->cmd.opcode, qspi->io_base + ZX_QSPI_INS_REG);
+
+	/* spifc start */
+	writel_relaxed(ZX_QSPI_START, qspi->io_base + ZX_QSPI_START_REG);
+
+	if (op->data.nbytes && ctrl0 & (ZX_QSPI_TX_DMA_EN | ZX_QSPI_RX_DMA_EN)) {
+		dev_dbg(dev, "Attempting DMA %x %s qspi 0x%08x...\n",
+			ctrl0, ctrl0 & ZX_QSPI_TX_DMA_EN ? "tx" : "rx",
+			(unsigned int)qspi);
+		// qspi->dma_desc->callback = zx_qspi_dma_callback;
+		qspi->dma_desc->callback_result = zx_qspi_callback_result;
+		qspi->dma_desc->callback_param = qspi;
+		cookie = dmaengine_submit(qspi->dma_desc);
+		ret = dma_submit_error(cookie);
+		if (ret) {
+			/* FIXME: Does this kind of fallback to PIO work? How do I provoke it to
+			 * test it?
+			 */
+			dev_err(dev, "dmaengine_submit = %d, falling back to PIO\n", ret);
+			ctrl0 &= ~(ZX_QSPI_TX_DMA_EN | ZX_QSPI_RX_DMA_EN);
+			writel_relaxed(ctrl0, qspi->io_base + ZX_QSPI_CTRL0_REG);
+		} else {
+			dma_async_issue_pending(qspi->cur_dma_ch);
+
+			if (!wait_for_completion_io_timeout(&qspi->dma_completion,
+					 msecs_to_jiffies(2000))) {
+				dev_err(dev, "DMA timed out\n");
+				spi_controller_dma_unmap_mem_op_data(qspi->ctrl, op, &qspi->sgt);
+				return -EIO;
+			}
+		}
+		qspi->dma_desc = NULL;
+
+		spi_controller_dma_unmap_mem_op_data(qspi->ctrl, op, &qspi->sgt);
+		writel_relaxed(ctrl0 & ~(ZX_QSPI_TX_DMA_EN | ZX_QSPI_RX_DMA_EN),
+			       qspi->io_base + ZX_QSPI_CTRL0_REG);
+	}
+
+	if (op->data.nbytes && !(ctrl0 & (ZX_QSPI_TX_DMA_EN | ZX_QSPI_RX_DMA_EN))) {
+		ret = zx_qspi_tx(qspi, op);
+		if (ret) {
+			dev_err(dev, "zx_qspi_tx error %d\n", ret);
+			return ret;
+		}
+	}
+
+	/* With DMA the result is ready by the time we receive the DMA complete callback. With PIO,
+	 * the highest wait time I have seen was 1 us.
+	 */
+	ret = readl_relaxed_poll_timeout_atomic(qspi->io_base + ZX_QSPI_INT_RAW_REG,
+						txstatus, txstatus & ZX_QSPI_INT_CMD_END, 1,
+						1000);
+	if (ret) {
+		dev_err(dev, "Wait for transfer end failed: %u.\n", ret);
+		return ret;
+	}
+
+	if (txstatus & ZX_QSPI_INT_FMT_ERR) {
+		dev_err(dev, "SPI controller reported an error, status 0x%08x\n", txstatus);
+		return -EIO;
+	}
+	if (txstatus & ZX_QSPI_INT_CMD_END)
+		return 0;
+
+	dev_err(dev, "Unexpected status 0x%08x\n", txstatus);
+	return -EIO;
+}
+
+#define ZX_QSPI_MAX_DATA			4096
+
+static bool zx_qspi_supports_op(struct spi_mem *mem,
+				const struct spi_mem_op *op)
+{
+	if (!spi_mem_default_supports_op(mem, op))
+		return false;
+
+	/* Command and address are always on one line */
+	if (op->cmd.nbytes != 1 || op->cmd.buswidth != 1)
+		return false;
+	if (op->addr.nbytes > 4 || (op->addr.nbytes && op->addr.buswidth != 1))
+		return false;
+	if (op->dummy.nbytes > 15 || (op->dummy.nbytes && op->dummy.buswidth != 1))
+		return false;
+
+	/* The controller cannot transmit data on two lines */
+	if (op->data.nbytes && op->data.dir == SPI_MEM_DATA_OUT &&
+	    op->data.buswidth == 2)
+		return false;
+
+	return true;
+}
+
+static int zx_qspi_adjust_op_size(struct spi_mem *mem, struct spi_mem_op *op)
+{
+	op->data.nbytes = min_t(unsigned int, op->data.nbytes, ZX_QSPI_MAX_DATA);
+
+	return 0;
+}
+
+static const struct spi_controller_mem_caps zx_qspi_mem_caps = {
+	.per_op_freq = true,
+};
+
+static const struct spi_controller_mem_ops zx_qspi_mem_ops = {
+	.adjust_op_size = zx_qspi_adjust_op_size,
+	.supports_op = zx_qspi_supports_op,
+	.exec_op = zx_qspi_exec_op,
+};
+
+static int zx_qspi_probe(struct platform_device *pdev)
+{
+	struct dma_slave_config dma_cfg = {0};
+	struct device *dev = &pdev->dev;
+	struct spi_controller *ctrl;
+	struct zx_qspi *qspi;
+	struct resource *res;
+	int irq, ret;
+	u32 reg;
+
+	dev_info(dev, "ZX QSPI Controller driver initializing\n");
+
+	ctrl = devm_spi_alloc_host(dev, sizeof(*qspi));
+	if (!ctrl)
+		return -ENOMEM;
+
+	qspi = spi_controller_get_devdata(ctrl);
+	qspi->ctrl = ctrl;
+	platform_set_drvdata(pdev, qspi);
+
+	init_completion(&qspi->dma_completion);
+
+	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	qspi->phys_base = res->start;
+	qspi->io_base = devm_ioremap_resource(dev, res);
+	if (IS_ERR(qspi->io_base))
+		return PTR_ERR(qspi->io_base);
+
+	irq = platform_get_irq_optional(pdev, 0);
+	if (irq > 0) {
+		ret = devm_request_irq(dev, irq, zx_qspi_irq, 0, MODULE_NAME, qspi);
+		if (ret)
+			return dev_err_probe(dev, ret, "failed to request irq\n");
+	}
+
+	qspi->pclk = devm_clk_get_enabled(dev, "pclk");
+	if (IS_ERR(qspi->pclk))
+		return PTR_ERR(qspi->pclk);
+
+	qspi->wclk = devm_clk_get_enabled(dev, "wclk");
+	if (IS_ERR(qspi->wclk))
+		return PTR_ERR(qspi->wclk);
+
+	qspi->rstc = devm_reset_control_get_optional_exclusive(dev, NULL);
+	if (IS_ERR(qspi->rstc))
+		return PTR_ERR(qspi->rstc);
+	if (qspi->rstc) {
+		reset_control_assert(qspi->rstc);
+		udelay(10);
+		reset_control_deassert(qspi->rstc);
+	}
+
+	dma_cfg.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
+	dma_cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
+	dma_cfg.src_addr = qspi->phys_base + ZX_QSPI_DATA_REG;
+	dma_cfg.dst_addr = qspi->phys_base + ZX_QSPI_DATA_REG;
+	/* FIFO_THRES has an impact on the burst size. Don't make them too small. */
+	dma_cfg.src_maxburst = ZX_QSPI_FIFO_SIZE / 2;
+	dma_cfg.dst_maxburst = ZX_QSPI_FIFO_SIZE / 2;
+
+	dma_cfg.direction = DMA_DEV_TO_MEM;
+	qspi->ctrl->dma_rx = devm_dma_request_chan(dev, "rx");
+	if (IS_ERR(qspi->ctrl->dma_rx)) {
+		dev_info(dev, "Cannot get RX DMA, using PIO\n");
+		qspi->ctrl->dma_rx = NULL;
+	} else {
+		if (dmaengine_slave_config(qspi->ctrl->dma_rx, &dma_cfg)) {
+			dev_info(dev, "Cannot configure RX DMA, using PIO\n");
+			dma_release_channel(qspi->ctrl->dma_rx);
+			qspi->ctrl->dma_rx = NULL;
+		}
+	}
+
+	dma_cfg.direction = DMA_MEM_TO_DEV;
+	qspi->ctrl->dma_tx = devm_dma_request_chan(dev, "tx");
+	if (IS_ERR(qspi->ctrl->dma_tx)) {
+		dev_info(dev, "Cannot get TX DMA, using PIO\n");
+		qspi->ctrl->dma_tx = NULL;
+	} else {
+		if (dmaengine_slave_config(qspi->ctrl->dma_tx, &dma_cfg)) {
+			dev_info(dev, "Cannot configure TX DMA, using PIO\n");
+			dma_release_channel(qspi->ctrl->dma_tx);
+			qspi->ctrl->dma_tx = NULL;
+		}
+	}
+
+	ctrl->mode_bits = SPI_RX_QUAD | SPI_RX_DUAL | SPI_TX_QUAD;
+	ctrl->bus_num = -1;
+	ctrl->mem_ops = &zx_qspi_mem_ops;
+	ctrl->mem_caps = &zx_qspi_mem_caps;
+	ctrl->setup = zx_qspi_setup;
+	ctrl->min_speed_hz = clk_round_rate(qspi->wclk, 1);
+	ctrl->max_speed_hz = 50000000;
+	ctrl->use_gpio_descriptors = true;
+	ctrl->num_chipselect = 1;
+	ctrl->dev.of_node = dev->of_node;
+	ctrl->dma_alignment = 4;
+
+	writel_relaxed(ZX_QSPI_EN, qspi->io_base + ZX_QSPI_EN_REG);
+	ret = readl_relaxed_poll_timeout_atomic(qspi->io_base + ZX_QSPI_EN_REG,
+						reg, reg & ZX_QSPI_EN_BACK, 1,
+						1000);
+	if (ret) {
+		dev_err(dev, "SPI controller failed to start: en_reg=%x\n", reg);
+		return ret;
+	}
+
+	writel_relaxed(ZX_QSPI_SCLK_PAUSE_EN, qspi->io_base + ZX_QSPI_CTRL0_REG);
+	writel_relaxed(0, qspi->io_base + ZX_QSPI_INT_EN_REG);
+
+	/* Ensure proper data sampling point */
+	reg = readl_relaxed(qspi->io_base + ZX_QSPI_TIMING_REG);
+	if (!FIELD_GET(GENMASK(17, 16), reg)) {
+		reg |= FIELD_PREP(GENMASK(17, 16), 1);
+		writel_relaxed(reg, qspi->io_base + ZX_QSPI_TIMING_REG);
+	}
+
+	reg = readl_relaxed(qspi->io_base + ZX_QSPI_SW_REG) >> 16;
+	if (reg != ZX_QSPI_FIFO_SIZE)
+		dev_info(dev, "Got unexpected fifo size %u\n", reg);
+
+	ret = devm_spi_register_controller(dev, ctrl);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void zx_qspi_remove(struct platform_device *pdev)
+{
+	struct zx_qspi *qspi = platform_get_drvdata(pdev);
+
+	writel_relaxed(~ZX_QSPI_EN, qspi->io_base + ZX_QSPI_EN_REG);
+}
+
+static const struct of_device_id zx_qspi_match[] = {
+	{ .compatible = "zte,zx29-qspi" },
+	{ .compatible = "zte,zx279128s-spifc" },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, zx_qspi_match);
+
+static struct platform_driver zx_qspi_driver = {
+	.probe	= zx_qspi_probe,
+	.remove = zx_qspi_remove,
+	.driver	= {
+		.name = MODULE_NAME,
+		.of_match_table = zx_qspi_match,
+	},
+};
+module_platform_driver(zx_qspi_driver);
+
+MODULE_AUTHOR("Stefan Dösinger <stefandoesinger@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx29 quad spi driver");
+MODULE_LICENSE("GPL");
-- 
2.55.0



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

* [PATCH v2 21/24] dt-bindings: usb: Add ZTE ZX279128S DWC3 controller
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (19 preceding siblings ...)
  2026-10-14  7:21 ` [PATCH v2 20/24] spi: zx: Add ZTE ZX family SPI flash controller driver Navid Ghahremani
@ 2026-10-14  7:21 ` Navid Ghahremani
  2026-10-14  7:21 ` [PATCH v2 22/24] usb: dwc3: generic-plat: Add ZTE ZX279128S Navid Ghahremani
                   ` (3 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:21 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Greg Kroah-Hartman,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, linux-usb,
	devicetree, linux-kernel

Describe the USB host integration with three CRM clocks and four reset
bits.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 .../bindings/usb/zte,zx279128s-dwc3.yaml      | 76 +++++++++++++++++++
 1 file changed, 76 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/usb/zte,zx279128s-dwc3.yaml

diff --git a/Documentation/devicetree/bindings/usb/zte,zx279128s-dwc3.yaml b/Documentation/devicetree/bindings/usb/zte,zx279128s-dwc3.yaml
new file mode 100644
index 0000000000..2fd9fe0cc7
--- /dev/null
+++ b/Documentation/devicetree/bindings/usb/zte,zx279128s-dwc3.yaml
@@ -0,0 +1,76 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/usb/zte,zx279128s-dwc3.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx279128s DWC3 USB controller
+
+maintainers:
+  - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description:
+  The ZTE zx279128s has a DesignWare USB3 controller (DWC_usb3 2.80a) with
+  integrated USB 2.0 and USB 3.0 PHYs, used as a host. Its clocks and resets
+  come from the top clock and reset module.
+
+allOf:
+  - $ref: snps,dwc3-common.yaml#
+
+properties:
+  compatible:
+    const: zte,zx279128s-dwc3
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: 20 MHz reference clock
+      - description: bus (AXI) clock
+      - description: 32.768 kHz suspend clock
+
+  clock-names:
+    items:
+      - const: ref
+      - const: bus_early
+      - const: suspend
+
+  resets:
+    description: The four reset bits of the controller and its PHYs, in the
+      order they are released.
+    minItems: 4
+    maxItems: 4
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - clocks
+  - clock-names
+  - resets
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/zte,zx279128s-crm.h>
+    #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+    usb@9100000 {
+        compatible = "zte,zx279128s-dwc3";
+        reg = <0x09100000 0x100000>;
+        interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
+        clocks = <&topcrm ZX279128S_TOP_USB_REF>,
+                 <&topcrm ZX279128S_TOP_USB_ACLK>,
+                 <&topcrm ZX279128S_TOP_USB_SUSPEND>;
+        clock-names = "ref", "bus_early", "suspend";
+        resets = <&topcrm ZX279128S_TOP_RST_USB_B11>,
+                 <&topcrm ZX279128S_TOP_RST_USB_B14>,
+                 <&topcrm ZX279128S_TOP_RST_USB_B9>,
+                 <&topcrm ZX279128S_TOP_RST_USB_B10>;
+        dr_mode = "host";
+    };
-- 
2.55.0



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

* [PATCH v2 22/24] usb: dwc3: generic-plat: Add ZTE ZX279128S
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (20 preceding siblings ...)
  2026-10-14  7:21 ` [PATCH v2 21/24] dt-bindings: usb: Add ZTE ZX279128S DWC3 controller Navid Ghahremani
@ 2026-10-14  7:21 ` Navid Ghahremani
  2026-10-14  7:21 ` [PATCH v2 23/24] ARM: dts: zte: Add ZX279128S SoC description Navid Ghahremani
                   ` (2 subsequent siblings)
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:21 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Thinh Nguyen,
	Greg Kroah-Hartman, linux-usb, linux-kernel

Use the generic DWC3 platform integration to enable clocks and release
resets. ZX279128S is an always-on networking gateway SoC without
system low-power sleep/suspend states, and the suspend clock is an APB
clock gating input handled alongside core clocks, making the generic
platform glue sufficient.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 drivers/usb/dwc3/Kconfig             | 2 +-
 drivers/usb/dwc3/dwc3-generic-plat.c | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/drivers/usb/dwc3/Kconfig b/drivers/usb/dwc3/Kconfig
index 18169727a4..dd0f6d25dd 100644
--- a/drivers/usb/dwc3/Kconfig
+++ b/drivers/usb/dwc3/Kconfig
@@ -207,7 +207,7 @@ config USB_DWC3_GENERIC_PLAT
 	default USB_DWC3
 	help
 	  Support USB3 functionality in simple SoC integrations.
-	  Currently supports SpacemiT DWC USB3. Platforms using
+	  Currently supports SpacemiT and ZTE zx279128s DWC USB3. Platforms using
 	  dwc3-of-simple can easily switch to dwc3-generic by flattening
 	  the dwc3 child node in the device tree.
 	  Say 'Y' or 'M' here if your platform integrates DWC3 in a similar way.
diff --git a/drivers/usb/dwc3/dwc3-generic-plat.c b/drivers/usb/dwc3/dwc3-generic-plat.c
index ca69ac0eb0..aa838cf067 100644
--- a/drivers/usb/dwc3/dwc3-generic-plat.c
+++ b/drivers/usb/dwc3/dwc3-generic-plat.c
@@ -237,6 +237,7 @@ static const struct of_device_id dwc3_generic_of_match[] = {
 	{ .compatible = "fsl,ls1028a-dwc3", &fsl_ls1028_dwc3},
 	{ .compatible = "eswin,eic7700-dwc3", &eic7700_dwc3},
 	{ .compatible = "starfive,jhb100-dwc3", },
+	{ .compatible = "zte,zx279128s-dwc3", },
 	{ /* sentinel */ }
 };
 MODULE_DEVICE_TABLE(of, dwc3_generic_of_match);
-- 
2.55.0



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

* [PATCH v2 23/24] ARM: dts: zte: Add ZX279128S SoC description
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (21 preceding siblings ...)
  2026-10-14  7:21 ` [PATCH v2 22/24] usb: dwc3: generic-plat: Add ZTE ZX279128S Navid Ghahremani
@ 2026-10-14  7:21 ` Navid Ghahremani
  2026-10-14  7:21 ` [PATCH v2 24/24] ARM: dts: zte: Add ZTE ZXHN H3600 board Navid Ghahremani
  2026-10-14 20:00 ` [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Stefan Dösinger
  24 siblings, 0 replies; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:21 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, devicetree, linux-kernel

Add the base device tree for the dual Cortex-A9 ZTE ZX279128S SoC,
including GIC, arch timer, CRM clock and reset controllers, GPIO,
pin control, Ethernet switch, MDIO, PCIe PHYs and controllers, SPI,
and DWC3 USB.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 arch/arm/boot/dts/zte/zx279128s.dtsi | 416 +++++++++++++++++++++++++++
 1 file changed, 416 insertions(+)
 create mode 100644 arch/arm/boot/dts/zte/zx279128s.dtsi

diff --git a/arch/arm/boot/dts/zte/zx279128s.dtsi b/arch/arm/boot/dts/zte/zx279128s.dtsi
new file mode 100644
index 0000000000..8896a8b067
--- /dev/null
+++ b/arch/arm/boot/dts/zte/zx279128s.dtsi
@@ -0,0 +1,416 @@
+// SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+/*
+ * ZTE zx279128s SoC: dual Cortex-A9 with an L2C-310 cache controller, a
+ * five-port gigabit switch, two PCIe controllers and a SPI NAND controller.
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#include <dt-bindings/clock/zte,zx279128s-crm.h>
+#include <dt-bindings/reset/zte,zx279128s-reset.h>
+#include <dt-bindings/interrupt-controller/arm-gic.h>
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+	compatible = "zte,zx279128s";
+
+	cpus {
+		#address-cells = <1>;
+		#size-cells = <0>;
+		enable-method = "zte,zx279128s-smp";
+
+		cpu@0 {
+			compatible = "arm,cortex-a9";
+			device_type = "cpu";
+			next-level-cache = <&l2cc>;
+			reg = <0>;
+		};
+
+		cpu@1 {
+			compatible = "arm,cortex-a9";
+			device_type = "cpu";
+			next-level-cache = <&l2cc>;
+			reg = <1>;
+		};
+	};
+
+	osc: clock-25000000 {
+		compatible = "fixed-clock";
+		#clock-cells = <0>;
+		clock-frequency = <25000000>;
+	};
+
+
+	soc {
+		#address-cells = <1>;
+		#size-cells = <1>;
+		compatible = "simple-bus";
+		interrupt-parent = <&gic>;
+		ranges;
+
+		sram@200800 {
+			compatible = "mmio-sram";
+			reg = <0x00200800 0x1000>;
+			#address-cells = <1>;
+			#size-cells = <1>;
+			ranges = <0 0x00200800 0x1000>;
+
+			/* the parked second core jumps here on every wake-up */
+			smp-sram@0 {
+				compatible = "zte,zx279128s-smp-sram";
+				reg = <0x0 0x8>;
+			};
+		};
+
+		scu@800000 {
+			compatible = "arm,cortex-a9-scu";
+			reg = <0x00800000 0x100>;
+		};
+
+		timer@800200 {
+			compatible = "arm,cortex-a9-global-timer";
+			reg = <0x00800200 0x20>;
+			interrupts = <GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(2) | IRQ_TYPE_EDGE_RISING)>;
+			clocks = <&topcrm ZX279128S_TOP_A9_PERIPH>;
+		};
+
+		timer@800600 {
+			compatible = "arm,cortex-a9-twd-timer";
+			reg = <0x00800600 0x20>;
+			interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(2) | IRQ_TYPE_EDGE_RISING)>;
+			clocks = <&topcrm ZX279128S_TOP_A9_PERIPH>;
+		};
+
+		watchdog@800620 {
+			compatible = "arm,cortex-a9-twd-wdt";
+			reg = <0x00800620 0x20>;
+			interrupts = <GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(2) | IRQ_TYPE_EDGE_RISING)>;
+			clocks = <&topcrm ZX279128S_TOP_A9_PERIPH>;
+			status = "disabled";
+		};
+
+		gic: interrupt-controller@801000 {
+			compatible = "arm,cortex-a9-gic";
+			#address-cells = <0>;
+			#interrupt-cells = <3>;
+			interrupt-controller;
+			reg = <0x00801000 0x1000>,
+			      <0x00800100 0x0100>;
+		};
+
+		l2cc: cache-controller@c00000 {
+			compatible = "arm,pl310-cache";
+			reg = <0x00c00000 0x1000>;
+			cache-unified;
+			cache-level = <2>;
+			arm,data-latency = <2 2 2>;
+			arm,tag-latency = <2 2 2>;
+			/*
+			 * As the stock firmware (AUX_CTRL 0x76430001). Without
+			 * shared-override the PL310 turns normal non-cacheable
+			 * shared accesses (dma_alloc_coherent buffers) into
+			 * cacheable ones, so devices and CPUs see stale data.
+			 */
+			arm,shared-override;
+			prefetch-data = <1>;
+			prefetch-instr = <1>;
+			arm,double-linefill = <1>;
+			arm,double-linefill-incr = <0>;
+		};
+
+		usb: usb@9100000 {
+			compatible = "zte,zx279128s-dwc3";
+			reg = <0x09100000 0x100000>;
+			interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
+			clocks = <&topcrm ZX279128S_TOP_USB_REF>,
+				 <&topcrm ZX279128S_TOP_USB_ACLK>,
+				 <&topcrm ZX279128S_TOP_USB_SUSPEND>;
+			clock-names = "ref", "bus_early", "suspend";
+			/* in the order the vendor firmware releases them */
+			resets = <&topcrm ZX279128S_RESET_USB_B11>,
+				 <&topcrm ZX279128S_RESET_USB_B14>,
+				 <&topcrm ZX279128S_RESET_USB_B9>,
+				 <&topcrm ZX279128S_RESET_USB_B10>;
+			dr_mode = "host";
+			status = "disabled";
+		};
+
+		pcie0: pcie@f000000 {
+			compatible = "zte,zx279128s-pcie";
+			reg = <0x0f000000 0x4000>,
+			      <0x09500000 0x1000>,
+			      <0x1c000000 0x200000>;
+			reg-names = "dbi", "ctrl", "config";
+			#address-cells = <3>;
+			#size-cells = <2>;
+			device_type = "pci";
+			bus-range = <0x00 0xff>;
+			ranges = <0x82000000 0 0x10000000 0x10000000 0 0x08000000>;
+			dma-ranges = <0x43000000 0 0x40000000 0x40000000 0 0x20000000>;
+			interrupts = <GIC_SPI 63 IRQ_TYPE_LEVEL_HIGH>;
+			#interrupt-cells = <1>;
+			interrupt-map-mask = <0 0 0 7>;
+			interrupt-map = <0 0 0 1 &gic GIC_SPI 63 IRQ_TYPE_LEVEL_HIGH>,
+					<0 0 0 2 &gic GIC_SPI 63 IRQ_TYPE_LEVEL_HIGH>,
+					<0 0 0 3 &gic GIC_SPI 63 IRQ_TYPE_LEVEL_HIGH>,
+					<0 0 0 4 &gic GIC_SPI 63 IRQ_TYPE_LEVEL_HIGH>;
+			linux,pci-domain = <0>;
+			pinctrl-names = "default";
+			pinctrl-0 = <&pcie0_pins>;
+			clocks = <&crm2 ZX279128S_CRM2_PCIE0_CLK>;
+			clock-names = "pcie";
+			resets = <&crm2 ZX279128S_CRM2_RESET_PCIE0>;
+			reset-names = "core";
+			phys = <&pcie0_phy>;
+			phy-names = "pcie";
+			status = "disabled";
+		};
+
+		pcie1: pcie@f100000 {
+			compatible = "zte,zx279128s-pcie";
+			reg = <0x0f100000 0x4000>,
+			      <0x09600000 0x1000>,
+			      <0x2c000000 0x200000>;
+			reg-names = "dbi", "ctrl", "config";
+			#address-cells = <3>;
+			#size-cells = <2>;
+			device_type = "pci";
+			bus-range = <0x00 0xff>;
+			ranges = <0x82000000 0 0x20000000 0x20000000 0 0x08000000>;
+			dma-ranges = <0x43000000 0 0x40000000 0x40000000 0 0x20000000>;
+			interrupts = <GIC_SPI 78 IRQ_TYPE_LEVEL_HIGH>;
+			#interrupt-cells = <1>;
+			interrupt-map-mask = <0 0 0 7>;
+			interrupt-map = <0 0 0 1 &gic GIC_SPI 78 IRQ_TYPE_LEVEL_HIGH>,
+					<0 0 0 2 &gic GIC_SPI 78 IRQ_TYPE_LEVEL_HIGH>,
+					<0 0 0 3 &gic GIC_SPI 78 IRQ_TYPE_LEVEL_HIGH>,
+					<0 0 0 4 &gic GIC_SPI 78 IRQ_TYPE_LEVEL_HIGH>;
+			linux,pci-domain = <1>;
+			pinctrl-names = "default";
+			pinctrl-0 = <&pcie1_pins>;
+			clocks = <&crm2 ZX279128S_CRM2_PCIE1_CLK>;
+			clock-names = "pcie";
+			resets = <&crm2 ZX279128S_CRM2_RESET_PCIE1>;
+			reset-names = "core";
+			phys = <&pcie1_phy>;
+			phy-names = "pcie";
+			status = "disabled";
+		};
+
+		gmac: ethernet@92000000 {
+			compatible = "zte,zx279128s-gmac";
+			reg = <0x92000000 0x400000>;
+			interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
+			status = "disabled";
+
+			ethernet-ports {
+				#address-cells = <1>;
+				#size-cells = <0>;
+
+				gmac_port0: port@0 {
+					reg = <0>;
+					status = "disabled";
+				};
+
+				gmac_port1: port@1 {
+					reg = <1>;
+					status = "disabled";
+				};
+
+				gmac_port2: port@2 {
+					reg = <2>;
+					status = "disabled";
+				};
+
+				gmac_port3: port@3 {
+					reg = <3>;
+					status = "disabled";
+				};
+
+				gmac_port4: port@4 {
+					reg = <4>;
+					status = "disabled";
+				};
+			};
+		};
+
+		topcrm: clock-controller@94000000 {
+			compatible = "zte,zx279128s-topcrm", "syscon", "simple-mfd";
+			reg = <0x94000000 0x1000>;
+			clocks = <&osc>;
+			clock-names = "osc";
+			#clock-cells = <1>;
+			#reset-cells = <1>;
+			#address-cells = <1>;
+			#size-cells = <0>;
+
+			reboot {
+				compatible = "syscon-reboot";
+				offset = <0x44>;
+				mask = <0x1>;
+				value = <0x1>;
+			};
+
+			pcie0_phy: phy@0 {
+				compatible = "zte,zx279128s-pcie-phy";
+				reg = <0>;
+				#phy-cells = <0>;
+			};
+
+			pcie1_phy: phy@1 {
+				compatible = "zte,zx279128s-pcie-phy";
+				reg = <1>;
+				#phy-cells = <0>;
+			};
+		};
+
+		crm2: clock-controller@94100000 {
+			compatible = "zte,zx279128s-crm2", "syscon";
+			reg = <0x94100000 0x1000>;
+			#clock-cells = <1>;
+			#reset-cells = <1>;
+		};
+
+		/*
+		 * The pin mux. What most of its bits select isn't known, so each
+		 * bit is a pin of its own.
+		 */
+		pinmux: pinctrl@94200000 {
+			compatible = "zte,zx279128s-pinmux", "pinctrl-single";
+			reg = <0x94200000 0x10>;
+			#pinctrl-cells = <2>;
+			pinctrl-single,bit-per-mux;
+			pinctrl-single,register-width = <32>;
+			pinctrl-single,function-mask = <0x1>;
+
+			/* the interface to an external Ethernet PHY */
+			ext_phy_pins: ext-phy-pins {
+				pinctrl-single,bits = <0x0c 0x0 0x00180800>;
+			};
+
+			pcie0_pins: pcie0-pins {
+				pinctrl-single,bits = <0x0c 0x0 0x00004000>;
+			};
+
+			pcie1_pins: pcie1-pins {
+				pinctrl-single,bits = <0x0c 0x0 0x00000002>;
+			};
+		};
+
+		lsp0crpm: clock-controller@94400000 {
+			compatible = "zte,zx279128s-lsp0crpm";
+			reg = <0x94400000 0x1000>;
+			clocks = <&topcrm ZX279128S_TOP_LSP0_PCLK>,
+				 <&topcrm ZX279128S_TOP_LSP0_25M>,
+				 <&topcrm ZX279128S_TOP_LSP0_32K>,
+				 <&topcrm ZX279128S_TOP_LSP0_100M>;
+			clock-names = "pclk", "wclk25m", "wclk32k", "wclk100m";
+			#clock-cells = <1>;
+		};
+
+		serial0: serial@94404000 {
+			compatible = "arm,pl011", "arm,primecell";
+			arm,primecell-periphid = <0x0018c011>;
+			reg = <0x94404000 0x1000>;
+			interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>;
+			clocks = <&lsp0crpm ZX279128S_LSP0_UART0_WCLK>,
+				 <&lsp0crpm ZX279128S_LSP0_UART0_PCLK>;
+			clock-names = "uartclk", "apb_pclk";
+			status = "disabled";
+		};
+
+		serial1: serial@94405000 {
+			compatible = "arm,pl011", "arm,primecell";
+			arm,primecell-periphid = <0x0018c011>;
+			reg = <0x94405000 0x1000>;
+			interrupts = <GIC_SPI 12 IRQ_TYPE_LEVEL_HIGH>;
+			clocks = <&lsp0crpm ZX279128S_LSP0_UART1_WCLK>,
+				 <&lsp0crpm ZX279128S_LSP0_UART1_PCLK>;
+			clock-names = "uartclk", "apb_pclk";
+			status = "disabled";
+		};
+
+		spifc: spi@94406000 {
+			compatible = "zte,zx279128s-spifc";
+			reg = <0x94406000 0x1000>;
+			clocks = <&lsp0crpm ZX279128S_LSP0_SPI_WCLK>,
+				 <&lsp0crpm ZX279128S_LSP0_SPI_PCLK>;
+			clock-names = "wclk", "pclk";
+			#address-cells = <1>;
+			#size-cells = <0>;
+			status = "disabled";
+		};
+
+		gpio0: gpio@94407000 {
+			compatible = "zte,zx279128s-gpio";
+			reg = <0x94407000 0x40>;
+			clocks = <&lsp0crpm ZX279128S_LSP0_GPIO_PCLK>;
+			gpio-controller;
+			#gpio-cells = <2>;
+		};
+
+		gpio1: gpio@94407040 {
+			compatible = "zte,zx279128s-gpio";
+			reg = <0x94407040 0x40>;
+			clocks = <&lsp0crpm ZX279128S_LSP0_GPIO_PCLK>;
+			gpio-controller;
+			#gpio-cells = <2>;
+		};
+
+		gpio2: gpio@94407080 {
+			compatible = "zte,zx279128s-gpio";
+			reg = <0x94407080 0x40>;
+			clocks = <&lsp0crpm ZX279128S_LSP0_GPIO_PCLK>;
+			gpio-controller;
+			#gpio-cells = <2>;
+		};
+
+		gpio3: gpio@944070c0 {
+			compatible = "zte,zx279128s-gpio";
+			reg = <0x944070c0 0x40>;
+			clocks = <&lsp0crpm ZX279128S_LSP0_GPIO_PCLK>;
+			gpio-controller;
+			#gpio-cells = <2>;
+		};
+
+		gpio4: gpio@94407100 {
+			compatible = "zte,zx279128s-gpio";
+			reg = <0x94407100 0x40>;
+			clocks = <&lsp0crpm ZX279128S_LSP0_GPIO_PCLK>;
+			gpio-controller;
+			#gpio-cells = <2>;
+		};
+
+		gpio5: gpio@94407140 {
+			compatible = "zte,zx279128s-gpio";
+			reg = <0x94407140 0x40>;
+			clocks = <&lsp0crpm ZX279128S_LSP0_GPIO_PCLK>;
+			gpio-controller;
+			#gpio-cells = <2>;
+		};
+
+		lsp1crpm: clock-controller@9a100000 {
+			compatible = "zte,zx279128s-lsp1crpm";
+			reg = <0x9a100000 0x1000>;
+			clocks = <&topcrm ZX279128S_TOP_LSP1_PCLK>,
+				 <&topcrm ZX279128S_TOP_LSP1_ACLK>,
+				 <&topcrm ZX279128S_TOP_LSP1_25M>,
+				 <&topcrm ZX279128S_TOP_LSP1_49M>,
+				 <&topcrm ZX279128S_TOP_LSP1_100M>;
+			clock-names = "pclk", "aclk", "wclk25m", "wclk49m", "wclk100m";
+			#clock-cells = <1>;
+		};
+
+		mdio: mdio@9a101000 {
+			compatible = "zte,zx279128s-mdio";
+			reg = <0x9a101000 0x18>;
+			clocks = <&lsp1crpm ZX279128S_LSP1_MDIO_WCLK>,
+				 <&lsp1crpm ZX279128S_LSP1_MDIO_PCLK>;
+			clock-names = "wclk", "pclk";
+			#address-cells = <1>;
+			#size-cells = <0>;
+			status = "disabled";
+		};
+	};
+};
-- 
2.55.0



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

* [PATCH v2 24/24] ARM: dts: zte: Add ZTE ZXHN H3600 board
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (22 preceding siblings ...)
  2026-10-14  7:21 ` [PATCH v2 23/24] ARM: dts: zte: Add ZX279128S SoC description Navid Ghahremani
@ 2026-10-14  7:21 ` Navid Ghahremani
  2026-10-14 12:52   ` Andrew Lunn
  2026-10-14 20:00 ` [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Stefan Dösinger
  24 siblings, 1 reply; 35+ messages in thread
From: Navid Ghahremani @ 2026-10-14  7:21 UTC (permalink / raw)
  To: linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Navid Ghahremani, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, devicetree, linux-kernel

Add the device tree for the ZTE ZXHN H3600 Wi-Fi 6 home gateway router.
Configure the internal Gigabit switch ports, external Sanechips ZX5201
WAN PHY, SPI NAND flash on the SPI controller, LEDs, buttons, and
MediaTek MT7915 Wi-Fi cards on PCIe ports 0 and 1.

Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
 arch/arm/boot/dts/zte/Makefile                |   2 +
 .../boot/dts/zte/zx279128s-zte-zxhn-h3600.dts | 364 ++++++++++++++++++
 2 files changed, 366 insertions(+)
 create mode 100644 arch/arm/boot/dts/zte/zx279128s-zte-zxhn-h3600.dts

diff --git a/arch/arm/boot/dts/zte/Makefile b/arch/arm/boot/dts/zte/Makefile
index f052cfbd63..29cc864459 100644
--- a/arch/arm/boot/dts/zte/Makefile
+++ b/arch/arm/boot/dts/zte/Makefile
@@ -1,3 +1,5 @@
 # SPDX-License-Identifier: GPL-2.0-only
 dtb-$(CONFIG_SOC_ZX297520V3) += \
 	zx297520v3-dlink-dwr932m.dtb
+
+dtb-$(CONFIG_SOC_ZX279128S) += zx279128s-zte-zxhn-h3600.dtb
diff --git a/arch/arm/boot/dts/zte/zx279128s-zte-zxhn-h3600.dts b/arch/arm/boot/dts/zte/zx279128s-zte-zxhn-h3600.dts
new file mode 100644
index 0000000000..d46390f075
--- /dev/null
+++ b/arch/arm/boot/dts/zte/zx279128s-zte-zxhn-h3600.dts
@@ -0,0 +1,364 @@
+// SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+/*
+ * ZTE ZXHN H3600 (also sold as ZXHN H1600): home gateway with four LAN ports,
+ * a WAN port and an MT7915 Wi-Fi 6 chip on both PCIe controllers.
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+/dts-v1/;
+
+#include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/input/input.h>
+#include <dt-bindings/leds/common.h>
+
+#include "zx279128s.dtsi"
+
+/ {
+	model = "ZTE ZXHN H3600";
+	compatible = "zte,zxhn-h3600", "zte,zx279128s";
+
+	aliases {
+		serial0 = &serial0;
+		serial1 = &serial1;
+	};
+
+	chosen {
+		stdout-path = "serial0:115200n8";
+	};
+
+	memory@40000000 {
+		device_type = "memory";
+		reg = <0x40000000 0x20000000>;
+	};
+
+	reserved-memory {
+		#address-cells = <1>;
+		#size-cells = <1>;
+		ranges;
+
+		/*
+		 * The stock kernel reserves the top 38 MiB for the packet
+		 * processor. Keep Linux, and in particular DMA buffers, out.
+		 */
+		pp@5da00000 {
+			reg = <0x5da00000 0x2600000>;
+			no-map;
+		};
+	};
+
+	/* All LEDs are active low. Colours only where seen on the panel. */
+	leds {
+		compatible = "gpio-leds";
+
+		/* One power LED with two elements: both on shows orange */
+		led_power_red: led-power-red {
+			color = <LED_COLOR_ID_RED>;
+			function = LED_FUNCTION_POWER;
+			gpios = <&gpio2 6 GPIO_ACTIVE_LOW>;
+			default-state = "on";
+		};
+
+		led_power_green: led-power-green {
+			color = <LED_COLOR_ID_GREEN>;
+			function = LED_FUNCTION_POWER;
+			gpios = <&gpio2 7 GPIO_ACTIVE_LOW>;
+		};
+
+		led_wlan_2g: led-wlan-2g {
+			color = <LED_COLOR_ID_GREEN>;
+			function = LED_FUNCTION_WLAN_2GHZ;
+			gpios = <&gpio0 1 GPIO_ACTIVE_LOW>;
+		};
+
+		led_broadband: led-broadband {
+			color = <LED_COLOR_ID_GREEN>;
+			function = LED_FUNCTION_WAN;
+			gpios = <&gpio0 8 GPIO_ACTIVE_LOW>;
+		};
+
+		led_phone1: led-phone1 {
+			color = <LED_COLOR_ID_GREEN>;
+			function = "phone";
+			function-enumerator = <1>;
+			gpios = <&gpio1 12 GPIO_ACTIVE_LOW>;
+		};
+
+		led_phone2: led-phone2 {
+			color = <LED_COLOR_ID_GREEN>;
+			function = "phone";
+			function-enumerator = <2>;
+			gpios = <&gpio1 13 GPIO_ACTIVE_LOW>;
+		};
+
+		led_wlan_5g: led-wlan-5g {
+			color = <LED_COLOR_ID_GREEN>;
+			function = LED_FUNCTION_WLAN_5GHZ;
+			gpios = <&gpio2 8 GPIO_ACTIVE_LOW>;
+		};
+
+		led_usb: led-usb {
+			color = <LED_COLOR_ID_GREEN>;
+			function = LED_FUNCTION_USB;
+			gpios = <&gpio2 9 GPIO_ACTIVE_LOW>;
+		};
+
+		led_internet_red: led-internet-red {
+			color = <LED_COLOR_ID_RED>;
+			function = LED_FUNCTION_WAN_ONLINE;
+			gpios = <&gpio3 0 GPIO_ACTIVE_LOW>;
+		};
+
+		led_internet_green: led-internet-green {
+			color = <LED_COLOR_ID_GREEN>;
+			function = LED_FUNCTION_WAN_ONLINE;
+			gpios = <&gpio3 1 GPIO_ACTIVE_LOW>;
+		};
+
+		led_wps: led-wps {
+			color = <LED_COLOR_ID_GREEN>;
+			function = LED_FUNCTION_WPS;
+			gpios = <&gpio3 2 GPIO_ACTIVE_LOW>;
+		};
+	};
+
+	/* Polled: the GPIO driver does not support interrupts */
+	keys {
+		compatible = "gpio-keys-polled";
+		poll-interval = <50>;
+
+		key-reset {
+			label = "reset";
+			gpios = <&gpio0 2 GPIO_ACTIVE_LOW>;
+			linux,code = <KEY_RESTART>;
+			debounce-interval = <60>;
+		};
+
+		key-wps {
+			label = "wps";
+			gpios = <&gpio0 12 GPIO_ACTIVE_LOW>;
+			linux,code = <KEY_WPS_BUTTON>;
+			debounce-interval = <60>;
+		};
+
+		key-wifi {
+			label = "rfkill";
+			gpios = <&gpio3 12 GPIO_ACTIVE_LOW>;
+			linux,code = <KEY_RFKILL>;
+			debounce-interval = <60>;
+		};
+
+		key-led {
+			label = "led";
+			gpios = <&gpio0 3 GPIO_ACTIVE_LOW>;
+			linux,code = <KEY_LIGHTS_TOGGLE>;
+			debounce-interval = <60>;
+		};
+	};
+};
+
+&gmac {
+	status = "okay";
+};
+
+&gmac_port0 {
+	label = "lan1";
+	phy-handle = <&phy10>;
+	phy-mode = "internal";
+	nvmem-cells = <&macaddr_tag_1e 0>;
+	nvmem-cell-names = "mac-address";
+	status = "okay";
+};
+
+&gmac_port1 {
+	label = "lan2";
+	phy-handle = <&phy11>;
+	phy-mode = "internal";
+	nvmem-cells = <&macaddr_tag_1e 0>;
+	nvmem-cell-names = "mac-address";
+	status = "okay";
+};
+
+&gmac_port2 {
+	label = "lan3";
+	phy-handle = <&phy12>;
+	phy-mode = "internal";
+	nvmem-cells = <&macaddr_tag_1e 0>;
+	nvmem-cell-names = "mac-address";
+	status = "okay";
+};
+
+&gmac_port3 {
+	label = "lan4";
+	phy-handle = <&phy13>;
+	phy-mode = "internal";
+	nvmem-cells = <&macaddr_tag_1e 0>;
+	nvmem-cell-names = "mac-address";
+	status = "okay";
+};
+
+&gmac_port4 {
+	label = "wan";
+	phy-handle = <&phy8>;
+	phy-mode = "gmii";
+	nvmem-cells = <&macaddr_tag_1e 1>;
+	nvmem-cell-names = "mac-address";
+	status = "okay";
+};
+
+&mdio {
+	/* the ZX5201 WAN PHY */
+	pinctrl-names = "default";
+	pinctrl-0 = <&ext_phy_pins>;
+	status = "okay";
+
+	/* external Sanechips ZX5201; its SerDes answers on address 9 */
+	phy8: ethernet-phy@8 {
+		reg = <8>;
+	};
+
+	phy10: ethernet-phy@a {
+		reg = <10>;
+	};
+
+	phy11: ethernet-phy@b {
+		reg = <11>;
+	};
+
+	phy12: ethernet-phy@c {
+		reg = <12>;
+	};
+
+	phy13: ethernet-phy@d {
+		reg = <13>;
+	};
+};
+
+/* MT7915: main interface on pcie0, second interface (HIF2) on pcie1 */
+/*
+ * The two lines reset the whole MT7915, which is behind both controllers.
+ * Whichever controller starts first releases it.
+ */
+&pcie0 {
+	reset-gpios = <&gpio3 5 GPIO_ACTIVE_LOW>;
+	enable-gpios = <&gpio3 7 GPIO_ACTIVE_HIGH>;
+	status = "okay";
+};
+
+&pcie1 {
+	reset-gpios = <&gpio3 5 GPIO_ACTIVE_LOW>;
+	enable-gpios = <&gpio3 7 GPIO_ACTIVE_HIGH>;
+	status = "okay";
+};
+
+&serial0 {
+	status = "okay";
+};
+
+&usb {
+	status = "okay";
+};
+
+&serial1 {
+	status = "okay";
+};
+
+&spifc {
+	status = "okay";
+
+	flash@0 {
+		compatible = "spi-nand";
+		reg = <0>;
+		spi-max-frequency = <50000000>;
+		spi-rx-bus-width = <2>;
+		spi-tx-bus-width = <1>;
+
+		partitions {
+			compatible = "fixed-partitions";
+			#address-cells = <1>;
+			#size-cells = <1>;
+
+			partition@0 {
+				label = "bootloader";
+				reg = <0x0000000 0x0100000>;
+				read-only;
+			};
+
+			partition@100000 {
+				label = "tag";
+				reg = <0x0100000 0x0100000>;
+				read-only;
+
+				nvmem-layout {
+					compatible = "fixed-layout";
+					#address-cells = <1>;
+					#size-cells = <1>;
+
+					/* the LAN ports' MAC; the WAN port uses the next one */
+					macaddr_tag_1e: macaddr@1e {
+						compatible = "mac-base";
+						reg = <0x1e 0x6>;
+						#nvmem-cell-cells = <1>;
+					};
+				};
+			};
+
+			partition@200000 {
+				label = "wifi";
+				reg = <0x0200000 0x0100000>;
+				read-only;
+			};
+
+			partition@300000 {
+				label = "usercfg";
+				reg = <0x0300000 0x0200000>;
+				read-only;
+			};
+
+			partition@500000 {
+				label = "defcfg";
+				reg = <0x0500000 0x0200000>;
+				read-only;
+			};
+
+			/*
+			 * Slot 0 of the boot loader, up to its slot 1 at
+			 * 0x2700000: it reads the kernel from the start, stepping
+			 * over bad blocks, and finds the slot header, which
+			 * holds the kernel's length and CRC. A rootfs length of 0
+			 * makes it check nothing else.
+			 */
+			partition@700000 {
+				label = "kernel";
+				reg = <0x0700000 0x0400000>;
+			};
+
+			partition@b00000 {
+				/* Erase old rootfs/UBI headers during migration. */
+				label = "slot0_gap";
+				reg = <0x0b00000 0x1b00000>;
+			};
+
+			partition@2600000 {
+				label = "trailer";
+				reg = <0x2600000 0x0100000>;
+			};
+
+			partition@2700000 {
+				label = "stock";
+				reg = <0x2700000 0x2000000>;
+				read-only;
+			};
+
+			/*
+			 * Keep slot 1 (0x2700000..0x4700000) intact. cspboot
+			 * mistakes UBI erase-counter headers for boot headers;
+			 * its scan must find both firmware headers before UBI.
+			 */
+			partition@4700000 {
+				label = "ubi";
+				reg = <0x4700000 0x3900000>;
+			};
+		};
+	};
+};
-- 
2.55.0



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

* Re: [PATCH v2 15/24] net: mdio: Add ZTE ZX279128S MDIO controller
  2026-10-14  7:20 ` [PATCH v2 15/24] net: mdio: " Navid Ghahremani
@ 2026-10-14 12:13   ` Andrew Lunn
  0 siblings, 0 replies; 35+ messages in thread
From: Andrew Lunn @ 2026-10-14 12:13 UTC (permalink / raw)
  To: Navid Ghahremani
  Cc: linux-arm-kernel, arnd, stefandoesinger, krzk, Heiner Kallweit,
	Russell King, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-kernel, netdev

> +static int zx279128s_mdio_xfer(struct mii_bus *bus, u32 op, int addr,
> +			       int regnum, u16 data)
> +{
> +	struct zx279128s_mdio *priv = bus->priv;
> +	void __iomem *base = priv->base;
> +	u32 cmd, val;
> +	int ret;
> +
> +	/* Clear the previous command and its status */
> +	writel(readl(base + ZX_MDIO_CMD) & ~ZX_MDIO_CMD_START,
> +	       base + ZX_MDIO_CMD);
> +	writel(0, base + ZX_MDIO_STATUS);
> +
> +	cmd = readl(base + ZX_MDIO_CMD);
> +	cmd &= ~(ZX_MDIO_CMD_WR | ZX_MDIO_CMD_RD | ZX_MDIO_CMD_ADDR |
> +		 ZX_MDIO_CMD_REG);
> +	cmd |= op | FIELD_PREP(ZX_MDIO_CMD_ADDR, addr) |
> +	       FIELD_PREP(ZX_MDIO_CMD_REG, regnum);
> +
> +	if (op == ZX_MDIO_CMD_WR)
> +		writel(data, base + ZX_MDIO_DATA_WR);
> +	writel(cmd | ZX_MDIO_CMD_START, base + ZX_MDIO_CMD);
> +
> +	ret = readl_poll_timeout(base + ZX_MDIO_STATUS, val,
> +				 val & ZX_MDIO_STATUS_DONE, ZX_MDIO_POLL_US,
> +				 ZX_MDIO_TIMEOUT_US);
> +	if (ret) {
> +		dev_err_ratelimited(&bus->dev, "timeout (addr %d, reg %d)\n",
> +				    addr, regnum);
> +		return ret;
> +	}
> +
> +	writel(0, base + ZX_MDIO_STATUS);
> +	writel(readl(base + ZX_MDIO_CMD) & ~ZX_MDIO_CMD_START,
> +	       base + ZX_MDIO_CMD);

This is the same as what you did at the beginning, but in the opposite
order. Is it actually needed, and can the same order be used?

> +static int zx279128s_mdio_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct zx279128s_mdio *priv;
> +	struct mii_bus *bus;
> +	struct clk *clk;
> +	u32 val;
> +
> +	bus = devm_mdiobus_alloc_size(dev, sizeof(*priv));
> +	if (!bus)
> +		return -ENOMEM;
> +
> +	priv = bus->priv;
> +	priv->base = devm_platform_ioremap_resource(pdev, 0);
> +	if (IS_ERR(priv->base))
> +		return PTR_ERR(priv->base);
> +
> +	clk = devm_clk_get_enabled(dev, "pclk");
> +	if (IS_ERR(clk))
> +		return dev_err_probe(dev, PTR_ERR(clk),
> +				     "failed to get the register clock\n");
> +	clk = devm_clk_get_enabled(dev, "wclk");
> +	if (IS_ERR(clk))
> +		return dev_err_probe(dev, PTR_ERR(clk),
> +				     "failed to get the work clock\n");
> +

Is this the bus clock? Does it need setting to 2.5Mhz?

> +static const struct of_device_id zx279128s_mdio_of_match[] = {
> +	{ .compatible = "zte,zx279128s-mdio" },
> +	{ }

It needs a device tree binding. But that is mostly boilerplate.

   Andrew


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

* Re: [PATCH v2 16/24] net: phy: Add Sanechips ZX5201 PHY support
  2026-10-14  7:20 ` [PATCH v2 16/24] net: phy: Add Sanechips ZX5201 PHY support Navid Ghahremani
@ 2026-10-14 12:37   ` Andrew Lunn
  0 siblings, 0 replies; 35+ messages in thread
From: Andrew Lunn @ 2026-10-14 12:37 UTC (permalink / raw)
  To: Navid Ghahremani
  Cc: linux-arm-kernel, arnd, stefandoesinger, krzk, Heiner Kallweit,
	Russell King, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-kernel, netdev

> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Sanechips ZX5201 Ethernet PHY
> + *
> + * The ZX5201 answers on two MDIO addresses: the copper PHY on its own
> + * address and its MAC-side SerDes interface on the next one.

What do you get when you read registers 2 and 3 of the SERDES? Is
there an ID in them?

If the mdio bus is scanned, and there is an ID, phylib will try to
probe a PHY driver on the SERDES.

> +static int zx5201_sds_read(struct phy_device *phydev, u32 regnum)
> +{
> +	return mdiobus_read(phydev->mdio.bus, phydev->mdio.addr + 1, regnum);
> +}

I would suggest adding a probe, and validate phydev->mdio.addr is not
31. ENODEV, or EINVAL, to indicate the setup is invalid.

> +static int zx5201_config_init(struct phy_device *phydev)
> +{
> +	static const u16 led_regs[] = { 0xb409, 0xb407, 0xb406, 0xb408 };

> +	/* LED control registers, all cleared as the vendor firmware does */
> +	for (i = 0; i < ARRAY_SIZE(led_regs); i++) {
> +		ret = zx5201_ext_write(phydev, led_regs[i], 0);
> +		if (ret)
> +			return ret;

It would be nice to reverse engineer what these bits do. And then add
proper LED controls. However, that can be later.

Ah, now i'm confused:

https://www.sanechips.com.cn/en/ProductEn/WiredEn/ethernetConnectivityEn/index.html

This says it is an RGMII PHY. Why does it have a SERDES? That suggests
SGMII?

We need to understand this, because if it is an RGMII PHY, we need to
address the RGMII delays, which that page says it can controller.

	Andrew


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

* Re: [PATCH v2 24/24] ARM: dts: zte: Add ZTE ZXHN H3600 board
  2026-10-14  7:21 ` [PATCH v2 24/24] ARM: dts: zte: Add ZTE ZXHN H3600 board Navid Ghahremani
@ 2026-10-14 12:52   ` Andrew Lunn
  0 siblings, 0 replies; 35+ messages in thread
From: Andrew Lunn @ 2026-10-14 12:52 UTC (permalink / raw)
  To: Navid Ghahremani
  Cc: linux-arm-kernel, arnd, stefandoesinger, krzk, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, devicetree, linux-kernel

> +&gmac_port4 {
> +	label = "wan";
> +	phy-handle = <&phy8>;
> +	phy-mode = "gmii";
> +	nvmem-cells = <&macaddr_tag_1e 1>;
> +	nvmem-cell-names = "mac-address";
> +	status = "okay";
> +};
> +
> +&mdio {
> +	/* the ZX5201 WAN PHY */
> +	pinctrl-names = "default";
> +	pinctrl-0 = <&ext_phy_pins>;
> +	status = "okay";
> +
> +	/* external Sanechips ZX5201; its SerDes answers on address 9 */
> +	phy8: ethernet-phy@8 {
> +		reg = <8>;
> +	};

You have phy-mode == gmii. Such a PHY does not need a SERDES. We need
a better understanding of what is actually used here.

	Andrew


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

* Re: [PATCH v2 13/24] PCI: dwc: Add ZTE ZX279128S host controller driver
  2026-10-14  7:20 ` [PATCH v2 13/24] PCI: dwc: Add ZTE ZX279128S host controller driver Navid Ghahremani
@ 2026-10-14 13:19   ` Bjorn Helgaas
  0 siblings, 0 replies; 35+ messages in thread
From: Bjorn Helgaas @ 2026-10-14 13:19 UTC (permalink / raw)
  To: Navid Ghahremani
  Cc: linux-arm-kernel, arnd, stefandoesinger, krzk, Lorenzo Pieralisi,
	Krzysztof Wilczyński, Manivannan Sadhasivam, Rob Herring,
	Bjorn Helgaas, Philipp Zabel, linux-kernel, linux-pci

On Wed, Oct 14, 2026 at 08:20:53AM +0100, Navid Ghahremani wrote:
> Support the two Synopsys DesignWare PCIe host controllers of the
> ZTE ZX279128S SoC. The driver manages the controller clocks via CCF,
> asserts/deasserts resets via the Reset framework, and manages the
> analog SerDes and PLL via the Generic PHY framework.

Thanks for making the commit log complete.

Suggest making the subject line:

  PCI: zx: Add ...

or whatever tag you plan to use for the family of hardware this driver
will support over time.  Future changes to a driver typically don't
include "dwc" because the name of the driver itself is more useful.

I see "zx" used in the spi patch and "zte" used in the clk patch.
"zte" might be too generic; there may be future hardware that requires
a different driver.  "zx279128s" might be too specific; there will
likely be more SoCs that share this driver.  Same consideration
applies to filename, config symbol, struct and function names, etc.

> Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
> ---
>  drivers/pci/controller/dwc/Kconfig          |  13 +
>  drivers/pci/controller/dwc/Makefile         |   1 +
>  drivers/pci/controller/dwc/pcie-zx279128s.c | 273 ++++++++++++++++++++
>  3 files changed, 287 insertions(+)
>  create mode 100644 drivers/pci/controller/dwc/pcie-zx279128s.c
> 
> diff --git a/drivers/pci/controller/dwc/Kconfig b/drivers/pci/controller/dwc/Kconfig
> index dcfbe7e229..3a4008be84 100644
> --- a/drivers/pci/controller/dwc/Kconfig
> +++ b/drivers/pci/controller/dwc/Kconfig
> @@ -577,4 +577,17 @@ config PCIE_ULTRARISC
>  	  By default, this symbol is enabled when ARCH_ULTRARISC is active,
>  	  requiring no further configuration on that platform.
>  
> +config PCIE_ZX279128S
> +	bool "ZTE zx279128s PCIe controller"
> +	depends on ARCH_ZTE || COMPILE_TEST
> +	depends on PCI_MSI
> +	select GENERIC_PHY
> +	select PCIE_DW_HOST
> +	select RESET_CONTROLLER
> +	help
> +	  Say Y here to enable support for the two PCIe host controllers of
> +	  the ZTE zx279128s SoC. They are based on the Synopsys DesignWare
> +	  PCIe IP. On the ZTE ZXHN H3600 they connect the MT7915 Wi-Fi chip.
> +	  The driver only supports legacy INTx interrupts.

Capitalization of "zx279128s" here (menu item, help text, comment
below) should probably match the commit log and marketing materials
for this SoC.

The config symbol should use the same tag as the subject line.
PCIE_ZX279128S might be too specific if more SoCs might share this
driver.

>  endmenu
> diff --git a/drivers/pci/controller/dwc/Makefile b/drivers/pci/controller/dwc/Makefile
> index d4f88cfc62..379198ebd6 100644
> --- a/drivers/pci/controller/dwc/Makefile
> +++ b/drivers/pci/controller/dwc/Makefile
> @@ -35,6 +35,7 @@ obj-$(CONFIG_PCIE_TEGRA194) += pcie-tegra194.o
>  obj-$(CONFIG_PCIE_UNIPHIER) += pcie-uniphier.o
>  obj-$(CONFIG_PCIE_UNIPHIER_EP) += pcie-uniphier-ep.o
>  obj-$(CONFIG_PCIE_VISCONTI_HOST) += pcie-visconti.o
> +obj-$(CONFIG_PCIE_ZX279128S) += pcie-zx279128s.o
>  obj-$(CONFIG_PCIE_RCAR_GEN4) += pcie-rcar-gen4.o
>  obj-$(CONFIG_PCIE_SPACEMIT_K1) += pcie-spacemit-k1.o
>  obj-$(CONFIG_PCIE_STM32_HOST) += pcie-stm32.o
> diff --git a/drivers/pci/controller/dwc/pcie-zx279128s.c b/drivers/pci/controller/dwc/pcie-zx279128s.c
> new file mode 100644
> index 0000000000..592a7f7c4e
> --- /dev/null
> +++ b/drivers/pci/controller/dwc/pcie-zx279128s.c
> @@ -0,0 +1,273 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * ZTE zx279128s PCIe host controller


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

* Re: [PATCH v2 06/24] clk: zte: Add ZX279128S CRM clock and reset driver
  2026-10-14  7:20 ` [PATCH v2 06/24] clk: zte: Add ZX279128S CRM clock and reset driver Navid Ghahremani
@ 2026-10-14 13:21   ` Philipp Zabel
  2026-10-14 19:39   ` Stefan Dösinger
  1 sibling, 0 replies; 35+ messages in thread
From: Philipp Zabel @ 2026-10-14 13:21 UTC (permalink / raw)
  To: Navid Ghahremani, linux-arm-kernel
  Cc: arnd, stefandoesinger, krzk, Stephen Boyd, Brian Masney,
	Jerome Brunet, linux-kernel, linux-clk

On Mi, 2026-10-14 at 08:20 +0100, Navid Ghahremani wrote:
> Support the top CRM and low-speed peripheral clock controllers of the
> ZTE ZX279128S SoC. Implement custom reset controller ops mapping
> logical reset IDs to internal hardware register offsets, and provide
> CRM2 clock gating and reset controller support for PCIe.
>
> Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
> ---
>  drivers/clk/Kconfig             |   1 +
>  drivers/clk/Makefile            |   1 +
>  drivers/clk/zte/Kconfig         |  14 +
>  drivers/clk/zte/Makefile        |   2 +
>  drivers/clk/zte/clk-zx279128s.c | 758 ++++++++++++++++++++++++++++++++
>  5 files changed, 776 insertions(+)
>  create mode 100644 drivers/clk/zte/Kconfig
>  create mode 100644 drivers/clk/zte/Makefile
>  create mode 100644 drivers/clk/zte/clk-zx279128s.c
> 
> diff --git a/drivers/clk/Kconfig b/drivers/clk/Kconfig
> index f9592fd9ec..5200de26c0 100644
> --- a/drivers/clk/Kconfig
> +++ b/drivers/clk/Kconfig
> @@ -547,6 +547,7 @@ source "drivers/clk/uniphier/Kconfig"
>  source "drivers/clk/visconti/Kconfig"
>  source "drivers/clk/x86/Kconfig"
>  source "drivers/clk/xilinx/Kconfig"
> +source "drivers/clk/zte/Kconfig"
>  source "drivers/clk/zynqmp/Kconfig"
>  
>  # Kunit test cases
> diff --git a/drivers/clk/Makefile b/drivers/clk/Makefile
> index b18af485d7..07393019c3 100644
> --- a/drivers/clk/Makefile
> +++ b/drivers/clk/Makefile
> @@ -181,5 +181,6 @@ ifeq ($(CONFIG_COMMON_CLK), y)
>  obj-$(CONFIG_X86)			+= x86/
>  endif
>  obj-y					+= xilinx/
> +obj-y					+= zte/
>  obj-$(CONFIG_ARCH_ZYNQ)			+= zynq/
>  obj-$(CONFIG_COMMON_CLK_ZYNQMP)         += zynqmp/
> diff --git a/drivers/clk/zte/Kconfig b/drivers/clk/zte/Kconfig
> new file mode 100644
> index 0000000000..92fd27ea42
> --- /dev/null
> +++ b/drivers/clk/zte/Kconfig
> @@ -0,0 +1,14 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +config CLK_ZX279128S
> +	bool "ZTE zx279128s clock driver"
> +	depends on SOC_ZX279128S || COMPILE_TEST
> +	depends on OF
> +	default SOC_ZX279128S
> +	select RESET_CONTROLLER
> +	select RESET_SIMPLE

Don't select RESET_SIMPLE without using it.

> +	help
> +	  This driver supports the clocks of the ZTE zx279128s SoC: the
> +	  top clock and reset module (CRM) with the PLLs, the CPU, bus and
> +	  peripheral group clocks, and the CRPMs of the two groups of low
> +	  speed peripherals (UARTs, SPI flash controller, GPIO, MDIO).
> +	  Say Y if you build a kernel for a board with this SoC.
> diff --git a/drivers/clk/zte/Makefile b/drivers/clk/zte/Makefile
> new file mode 100644
> index 0000000000..a55ffbfaf1
> --- /dev/null
> +++ b/drivers/clk/zte/Makefile
> @@ -0,0 +1,2 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +obj-$(CONFIG_CLK_ZX279128S)	+= clk-zx279128s.o
> diff --git a/drivers/clk/zte/clk-zx279128s.c b/drivers/clk/zte/clk-zx279128s.c
> new file mode 100644
> index 0000000000..892e0c22e5
> --- /dev/null
> +++ b/drivers/clk/zte/clk-zx279128s.c
> @@ -0,0 +1,758 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * ZTE zx279128s clocks: the top CRM and the CRPMs of the two groups of low
> + * speed peripherals
> + *
> + * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
> + *
> + * The boot loader sets up the PLLs and the CPU and bus clocks, and the driver
> + * leaves them as they are: it reads them once and registers their rates. The
> + * gates, and the muxes and dividers of the peripherals, are under its
> + * control.
> + */
> +
> +#include <linux/bitfield.h>
> +#include <linux/clk.h>
> +#include <linux/clk-provider.h>
> +#include <linux/io.h>
> +#include <linux/of.h>
> +#include <linux/of_address.h>
> +#include <linux/platform_device.h>
> +#include <linux/property.h>
> +#include <linux/reset-controller.h>
> +
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +
> +#include <dt-bindings/clock/zte,zx279128s-crm.h>
> +#include <dt-bindings/reset/zte,zx279128s-reset.h>
> +
> +/* Top CRM */
> +#define TOP_CLK_SEL		0x0c
> +#define  TOP_MATRIX_SEL_SHIFT	15
> +#define  TOP_CPU_SEL_SHIFT	13
> +#define TOP_CLK_EN		0x14
> +#define TOP_PLL_A9		0x18
> +#define TOP_PLL_AUDIO		0x28
> +#define TOP_USB_EN		0x48
> +
> +#define TOP_NUM_CLKS		(ZX279128S_TOP_USB_REF + 1)
> +
> +struct zx_reset_line {
> +	u16 reg;
> +	u8 bit;
> +};
> +
> +static const struct zx_reset_line zx279128s_top_resets[] = {
> +	[ZX279128S_RESET_USB_B9]  = { 0x4c, 9 },
> +	[ZX279128S_RESET_USB_B10] = { 0x4c, 10 },
> +	[ZX279128S_RESET_USB_B11] = { 0x4c, 11 },
> +	[ZX279128S_RESET_USB_B14] = { 0x4c, 14 },

Why store reg in zx_reset_line when it's all the same value?

On the other hand, keeping reg and adding a bool active_low field would
allow to share ops between the two reset controllers.

> +};
> +
> +static DEFINE_SPINLOCK(zx_top_lock);

Why share the lock? Each reset controller has its own register.

> +
> +struct zx_reset_data {
> +	struct reset_controller_dev rcdev;
> +	void __iomem *base;
> +	spinlock_t *lock;
> +};
> +
> +static inline struct zx_reset_data *to_zx_reset_data(struct reset_controller_dev *rcdev)
> +{
> +	return container_of(rcdev, struct zx_reset_data, rcdev);
> +}
> +
> +static int zx_reset_assert(struct reset_controller_dev *rcdev, unsigned long id)
> +{
> +	struct zx_reset_data *data = to_zx_reset_data(rcdev);
> +	const struct zx_reset_line *line;
> +	unsigned long flags;
> +	u32 val;
> +
> +	if (id >= ARRAY_SIZE(zx279128s_top_resets))
> +		return -EINVAL;

This check is unnecessary, fwnode_reset_simple_xlate() already checked
this before the reset control was created.

> +	line = &zx279128s_top_resets[id];
> +	spin_lock_irqsave(data->lock, flags);
> +	val = readl(data->base + line->reg);
> +	val &= ~BIT(line->bit);
> +	writel(val, data->base + line->reg);
> +	spin_unlock_irqrestore(data->lock, flags);

Consider sharing a read-modify-write helper between implementations.

> +	return 0;
> +}
> +
> +static int zx_reset_deassert(struct reset_controller_dev *rcdev, unsigned long id)
> +{
> +	struct zx_reset_data *data = to_zx_reset_data(rcdev);
> +	const struct zx_reset_line *line;
> +	unsigned long flags;
> +	u32 val;
> +
> +	if (id >= ARRAY_SIZE(zx279128s_top_resets))
> +		return -EINVAL;
> +
> +	line = &zx279128s_top_resets[id];
> +	spin_lock_irqsave(data->lock, flags);
> +	val = readl(data->base + line->reg);
> +	val |= BIT(line->bit);
> +	writel(val, data->base + line->reg);
> +	spin_unlock_irqrestore(data->lock, flags);
> +	return 0;
> +}
> +
> +static int zx_reset_status(struct reset_controller_dev *rcdev, unsigned long id)
> +{
> +	struct zx_reset_data *data = to_zx_reset_data(rcdev);
> +	const struct zx_reset_line *line;
> +	u32 val;
> +
> +	if (id >= ARRAY_SIZE(zx279128s_top_resets))
> +		return -EINVAL;
> +
> +	line = &zx279128s_top_resets[id];
> +	val = readl(data->base + line->reg);
> +	return !(val & BIT(line->bit));
> +}
> +
> +static const struct reset_control_ops zx_reset_ops = {
> +	.assert = zx_reset_assert,
> +	.deassert = zx_reset_deassert,
> +	.status = zx_reset_status,
> +};
> +
> +/*
> + * Reset bits are cleared to hold the reset. The registers also hold other
> + * controls, but no other driver writes the reset registers.
> + */
> +static int __init zx_top_reset_init(struct device_node *np,
> +				    void __iomem *base)

Why is this initialized from CLK_OF_DECLARE_DRIVER()?
Surely USB resets are not required early?

> +{
> +	struct zx_reset_data *rst;
> +	int ret;
> +
> +	rst = kzalloc_obj(*rst, GFP_KERNEL);
> +	if (!rst)
> +		return -ENOMEM;
> +
> +	rst->lock = &zx_top_lock;
> +	rst->base = base;
> +	rst->rcdev.ops = &zx_reset_ops;
> +	rst->rcdev.owner = THIS_MODULE;
> +	rst->rcdev.nr_resets = ARRAY_SIZE(zx279128s_top_resets);
> +	rst->rcdev.of_node = np;
> +
> +	ret = reset_controller_register(&rst->rcdev);
> +	if (ret)
> +		kfree(rst);
> +
> +	return ret;
> +}
[...]
> +static void __init zx279128s_topcrm_init(struct device_node *np)
> +{
[...]
> +}
> +
> +CLK_OF_DECLARE_DRIVER(zx279128s_topcrm, "zte,zx279128s-topcrm",
> +	       zx279128s_topcrm_init);
[...]
> +struct zx_crm2_reset_data {
> +	struct reset_controller_dev rcdev;
> +	void __iomem *base;
> +	spinlock_t *lock;
> +};

This is exactly the same as zx_reset_data, why duplicate it?
Same comments as above apply here.

regards
Philipp


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

* Re: [PATCH v2 20/24] spi: zx: Add ZTE ZX family SPI flash controller driver
  2026-10-14  7:21 ` [PATCH v2 20/24] spi: zx: Add ZTE ZX family SPI flash controller driver Navid Ghahremani
@ 2026-10-14 19:28   ` Stefan Dösinger
  0 siblings, 0 replies; 35+ messages in thread
From: Stefan Dösinger @ 2026-10-14 19:28 UTC (permalink / raw)
  To: linux-arm-kernel, Navid Ghahremani
  Cc: arnd, krzk, Navid Ghahremani, Mark Brown, Philipp Zabel,
	linux-kernel, linux-spi

[-- Attachment #1: Type: text/plain, Size: 1008 bytes --]

Am Mittwoch, 14. Oktober 2026, 09:21:00 Mitteleuropäische Sommerzeit schrieb 
Navid Ghahremani:
> Add support for the SPI flash controller found on ZTE ZX family SoCs
> (ZX297520V3, ZX279128S). Implement tuned PIO to avoid redundant FIFO
> depth reads, dynamic CCF clock scaling, 4KB operation chunking, and
> optional DMA support.

I'll diff this to the version in my tree and see what I can grab. I don't know 
if I'll find the time before November though.

Thanks to Navid for testing the driver on different hardware. I'm happy it just 
worked :-)

> +MODULE_AUTHOR("Stefan Dösinger <stefandoesinger@gmail.com>");
> +MODULE_DESCRIPTION("ZTE zx29 quad spi driver");
> +MODULE_LICENSE("GPL");

I plan to submit this myself eventually. Right now I don't think the state of 
zx297520v3 support is ready for SPI (no pinmux yet), and the code itself has a 
pile of todos.

If zx27 gets to the point where it has the prerequisites  are upstream first I 
can speed this up of course :-)

[-- Attachment #2: This is a digitally signed message part. --]
[-- Type: application/pgp-signature, Size: 870 bytes --]

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

* Re: [PATCH v2 06/24] clk: zte: Add ZX279128S CRM clock and reset driver
  2026-10-14  7:20 ` [PATCH v2 06/24] clk: zte: Add ZX279128S CRM clock and reset driver Navid Ghahremani
  2026-10-14 13:21   ` Philipp Zabel
@ 2026-10-14 19:39   ` Stefan Dösinger
  1 sibling, 0 replies; 35+ messages in thread
From: Stefan Dösinger @ 2026-10-14 19:39 UTC (permalink / raw)
  To: linux-arm-kernel, Navid Ghahremani
  Cc: arnd, krzk, Navid Ghahremani, Stephen Boyd, Brian Masney,
	Jerome Brunet, Philipp Zabel, linux-kernel, linux-clk

[-- Attachment #1: Type: text/plain, Size: 806 bytes --]

Hi,

Am Mittwoch, 14. Oktober 2026, 09:20:46 Mitteleuropäische Sommerzeit schrieb 
Navid Ghahremani:
> Support the top CRM and low-speed peripheral clock controllers of the
> ZTE ZX279128S SoC. Implement custom reset controller ops mapping
> logical reset IDs to internal hardware register offsets, and provide
> CRM2 clock gating and reset controller support for PCIe.
> 
> Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>

The comments I sent for v1 still apply to v2 I think.

Please try to reuse the infrastructure in place for zx29. Ideally clk-
zx279128s.c should be just static data and a minimalistic _probe function that 
invokes zx_clk_common_probe().

The drivers/clk/zte code is upstream in linux-next already. The MFD and reset 
parts are still missing though.

[-- Attachment #2: This is a digitally signed message part. --]
[-- Type: application/pgp-signature, Size: 870 bytes --]

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

* Re: [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600
  2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
                   ` (23 preceding siblings ...)
  2026-10-14  7:21 ` [PATCH v2 24/24] ARM: dts: zte: Add ZTE ZXHN H3600 board Navid Ghahremani
@ 2026-10-14 20:00 ` Stefan Dösinger
  24 siblings, 0 replies; 35+ messages in thread
From: Stefan Dösinger @ 2026-10-14 20:00 UTC (permalink / raw)
  To: linux-arm-kernel, Navid Ghahremani; +Cc: arnd, krzk, Navid Ghahremani

[-- Attachment #1: Type: text/plain, Size: 804 bytes --]

Am Mittwoch, 14. Oktober 2026, 09:20:40 Mitteleuropäische Sommerzeit schrieb 
Navid Ghahremani:

> Series breakdown:
> - Patch 01: Consolidated MAINTAINERS entry.
> - Patches 02-03: SoC platform integration, SMP CPU hotplug trampoline.
> - Patches 04-06: Split CRM clock bindings, logical reset bindings,
>   CCF/reset driver.

I recommend to focus on the above patches first and wait with the rest until 
the basic board description and clock drivers are in. This reduces load on 
reviewers and keeps the patch series focused.

The later patches are still interesting to see though to see where the journey 
is going and for myself to see how your zx27 hardware compares to mine.

Out of curiosity, do you have a public git repository with your patches 
somewhere?

Cheers,
Stefan

[-- Attachment #2: This is a digitally signed message part. --]
[-- Type: application/pgp-signature, Size: 870 bytes --]

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

* Re: [PATCH v2 01/24] MAINTAINERS: Consolidate ARM/ZTE ZX SOC SUPPORT with Stefan Doesinger
  2026-10-14  7:20 ` [PATCH v2 01/24] MAINTAINERS: Consolidate ARM/ZTE ZX SOC SUPPORT with Stefan Doesinger Navid Ghahremani
@ 2026-10-14 20:11   ` Stefan Dösinger
  0 siblings, 0 replies; 35+ messages in thread
From: Stefan Dösinger @ 2026-10-14 20:11 UTC (permalink / raw)
  To: linux-arm-kernel, Navid Ghahremani
  Cc: arnd, krzk, Navid Ghahremani, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 1822 bytes --]

Am Mittwoch, 14. Oktober 2026, 09:20:41 Mitteleuropäische Sommerzeit schrieb 
Navid Ghahremani:

> -ARM/ZTE ZX29 SOC SUPPORT
> +ARM/ZTE ZX SOC SUPPORT
>  M:	Stefan Dösinger <stefandoesinger@gmail.com>
> +M:	Navid Ghahremani <ghahramani.navid@gmail.com>

I am generally OK with that, at least once Navid's patches 1-6 passed review.

>  L:	linux-arm-kernel@lists.infradead.org (moderated for non-
subscribers)
> -S:	Odd fixes
> +S:	Maintained

I think it is better to keep it ad "Odd fixes" as it is a hobby project to both 
myself and Navid. I don't know where the dividing line between "Maintained" 
and "Odd Fixes" is when it comes to SoCs ( yes I read https://docs.kernel.org/
process/maintainers.html ). I guess once we have a third developer and/or an 
openwrt userbase I'd feel better elevating that.

>  F:	Documentation/arch/arm/zte/
> +F:	Documentation/devicetree/bindings/arm/zte/
>  F:	Documentation/devicetree/bindings/arm/zte.yaml
> +F:	Documentation/devicetree/bindings/clock/zte,zx*
> +F:	Documentation/devicetree/bindings/gpio/zte,zx*
> +F:	Documentation/devicetree/bindings/net/zte,zx*
> +F:	Documentation/devicetree/bindings/pci/zte,zx*
> +F:	Documentation/devicetree/bindings/phy/zte,zx*
> +F:	Documentation/devicetree/bindings/reset/zte,zx*
> +F:	Documentation/devicetree/bindings/spi/zte,zx*
> +F:	Documentation/devicetree/bindings/usb/zte,zx*

This should go into or near the patches that add the files in question. ( I 
took that reading from https://lore.kernel.org/linux-mediatek/
2df4cad5e29fbcb4c5c5f59ea0bf322c7a301bdc.camel@gmail.com in particular. 
https://lore.kernel.org/linux-mediatek/20260506-honest-cyan-cow-d6c5b0@quoll/ 
suggests that too ). But yes, that can also cause annoying merge conflicts when 
PR'ing patches to their respective trees.

[-- Attachment #2: This is a digitally signed message part. --]
[-- Type: application/pgp-signature, Size: 870 bytes --]

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

* Re: [PATCH v2 19/24] dt-bindings: spi: Add ZTE ZX family SPI flash controller binding
  2026-10-14  7:20 ` [PATCH v2 19/24] dt-bindings: spi: Add ZTE ZX family SPI flash controller binding Navid Ghahremani
@ 2026-10-14 20:33   ` Stefan Dösinger
  0 siblings, 0 replies; 35+ messages in thread
From: Stefan Dösinger @ 2026-10-14 20:33 UTC (permalink / raw)
  To: linux-arm-kernel, Navid Ghahremani
  Cc: arnd, krzk, Navid Ghahremani, Mark Brown, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, linux-spi, devicetree,
	linux-kernel

[-- Attachment #1: Type: text/plain, Size: 1494 bytes --]

Am Mittwoch, 14. Oktober 2026, 09:20:59 Mitteleuropäische Sommerzeit schrieb 
Navid Ghahremani:

> +      - zte,zx29-qspi
> +      - zte,zx279128s-spifc

Unless there is a hardware difference that matters to the driver I think one 
compatible should be enough. Probably zte,zx-qspi. Although since this 
controller is very specific for flash chips zte,zx-spifc or zte,spifc are worthy 
considerations too. As far as I can see this controller only has flash memory 
ops and can't even send a generic byte stream.

Note that at least my board, and probably yours too, has a "normal" SPI 
controller too. In my case it can talk to SPI displays or a SLIC analog phone 
controller. It has a different hardware interface and needs a separate driver.

> +
> +  clocks:
> +    items:
> +      - description: work clock, the source of the SPI clock
> +      - description: register (APB) clock
> +
> +  clock-names:
> +    items:
> +      - const: wclk
> +      - const: pclk
> +
> +required:
> +  - compatible
> +  - reg
> +  - clocks
> +  - clock-names

I think the interrupt should be required by the binding too. The driver is 
still free to ignore it, but the binding is supposed to describe the hardware, 
not the driver.

My hardware has two DMA lines too. I don't know if they are always there. For 
pragmatic reasons (the DMA controller is its own beast and for usability less 
important than the flash controller) I'd make the DMA properties optional.

[-- Attachment #2: This is a digitally signed message part. --]
[-- Type: application/pgp-signature, Size: 870 bytes --]

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

end of thread, other threads:[~2026-10-14 20:33 UTC | newest]

Thread overview: 35+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-14  7:20 [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 01/24] MAINTAINERS: Consolidate ARM/ZTE ZX SOC SUPPORT with Stefan Doesinger Navid Ghahremani
2026-10-14 20:11   ` Stefan Dösinger
2026-10-14  7:20 ` [PATCH v2 02/24] dt-bindings: arm: Add ZTE ZX279128S platform descriptions Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 03/24] ARM: zte: Add ZX279128S platform and CPU hotplug support Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 04/24] dt-bindings: clock: Add ZTE ZX279128S CRM clock bindings Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 05/24] dt-bindings: reset: Add ZTE ZX279128S reset controller bindings Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 06/24] clk: zte: Add ZX279128S CRM clock and reset driver Navid Ghahremani
2026-10-14 13:21   ` Philipp Zabel
2026-10-14 19:39   ` Stefan Dösinger
2026-10-14  7:20 ` [PATCH v2 07/24] dt-bindings: gpio: Add ZTE ZX279128S GPIO controller Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 08/24] gpio: Add ZTE ZX279128S GPIO driver Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 09/24] dt-bindings: pinctrl: pinctrl-single: Add ZTE ZX279128S pin mux Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 10/24] dt-bindings: phy: Add ZTE ZX279128S PCIe PHY binding Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 11/24] phy: zte: Add ZTE ZX279128S PCIe PHY driver Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 12/24] dt-bindings: PCI: Add ZTE ZX279128S host controller Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 13/24] PCI: dwc: Add ZTE ZX279128S host controller driver Navid Ghahremani
2026-10-14 13:19   ` Bjorn Helgaas
2026-10-14  7:20 ` [PATCH v2 14/24] dt-bindings: net: Add ZTE ZX279128S MDIO controller Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 15/24] net: mdio: " Navid Ghahremani
2026-10-14 12:13   ` Andrew Lunn
2026-10-14  7:20 ` [PATCH v2 16/24] net: phy: Add Sanechips ZX5201 PHY support Navid Ghahremani
2026-10-14 12:37   ` Andrew Lunn
2026-10-14  7:20 ` [PATCH v2 17/24] dt-bindings: net: Add ZTE ZX279128S Ethernet switch Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 18/24] net: ethernet: zte: Add ZX279128S Ethernet switch driver Navid Ghahremani
2026-10-14  7:20 ` [PATCH v2 19/24] dt-bindings: spi: Add ZTE ZX family SPI flash controller binding Navid Ghahremani
2026-10-14 20:33   ` Stefan Dösinger
2026-10-14  7:21 ` [PATCH v2 20/24] spi: zx: Add ZTE ZX family SPI flash controller driver Navid Ghahremani
2026-10-14 19:28   ` Stefan Dösinger
2026-10-14  7:21 ` [PATCH v2 21/24] dt-bindings: usb: Add ZTE ZX279128S DWC3 controller Navid Ghahremani
2026-10-14  7:21 ` [PATCH v2 22/24] usb: dwc3: generic-plat: Add ZTE ZX279128S Navid Ghahremani
2026-10-14  7:21 ` [PATCH v2 23/24] ARM: dts: zte: Add ZX279128S SoC description Navid Ghahremani
2026-10-14  7:21 ` [PATCH v2 24/24] ARM: dts: zte: Add ZTE ZXHN H3600 board Navid Ghahremani
2026-10-14 12:52   ` Andrew Lunn
2026-10-14 20:00 ` [PATCH v2 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Stefan Dösinger

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