* [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600
@ 2026-10-10 12:59 Navid Ghahremani
2026-10-10 12:59 ` [PATCH 01/24] dt-bindings: cache: l2c2x0: Describe ZTE device-read serialization Navid Ghahremani
` (23 more replies)
0 siblings, 24 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
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.
This submission establishes Navid Ghahremani as the maintainer for
ARM/ZTE ZX279128S SOC SUPPORT in MAINTAINERS.
Series breakdown:
- Patches 01-03: Outer cache read serialization mechanism to prevent
bus deadlock between outstanding PCIe device reads and L2C-310 cache
sync operations.
- Patches 04-05: SoC platform integration, SMP CPU hotplug trampoline,
and MAINTAINERS entry.
- Patches 06-11: Core peripheral drivers: CRM clock/reset controller,
GPIO, pinctrl-single bindings, and syscon.
- Patches 12-13: DesignWare PCIe host controller driver for the dual
MT7915 Wi-Fi links with shared reset sequencing.
- Patches 14-18: Networking: MDIO bus controller, ZX5201 Gigabit PHY,
and Gigabit Ethernet switch driver with switchdev and hardware flow
offload integration.
- Patches 19-20: SPI flash controller driver using the Linux SPI-MEM
framework.
- 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 via U-Boot TFTP boot.
- Dual-core SMP, CPU hotplug, and thermal sensors operational.
- Dual-band MT7915 2.4/5 GHz Wi-Fi links initialized and stable.
- Gigabit Ethernet switch verified at line-rate 1 Gbps forwarding.
- SPI-NAND flash persistence and USB 2.0 host verified.
Independent prerequisites sent separately:
- MTD: heyangtek bad block marker reserve byte fix.
- NVMEM: fixed-layout mac-base consumer offset support.
Navid Ghahremani (24):
dt-bindings: cache: l2c2x0: Describe ZTE device-read serialization
ARM: l2c: Serialize device reads with cache maintenance when requested
wifi: mt76: Serialize MMIO reads with outer cache maintenance
dt-bindings: arm: Add ZTE ZX279128S platform descriptions
ARM: zte: Add ZX279128S platform and CPU hotplug support
dt-bindings: clock: Add ZTE ZX279128S CRM clocks and resets
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: mfd: syscon: Add ZX279128S system controller
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 ZX279128S SPI flash controller
spi: Add ZTE ZX279128S SPI flash controller
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 +-
.../devicetree/bindings/cache/l2c2x0.yaml | 9 +
.../bindings/clock/zte,zx279128s-crm.yaml | 141 ++
.../bindings/gpio/zte,zx279128s-gpio.yaml | 53 +
.../devicetree/bindings/mfd/syscon.yaml | 1 +
.../bindings/net/zte,zx279128s-gmac.yaml | 108 ++
.../bindings/net/zte,zx279128s-mdio.yaml | 67 +
.../bindings/pci/zte,zx279128s-pcie.yaml | 97 ++
.../bindings/pinctrl/pinctrl-single.yaml | 1 +
.../bindings/spi/zte,zx279128s-spifc.yaml | 69 +
.../devicetree/bindings/sram/sram.yaml | 1 +
.../bindings/usb/zte,zx279128s-dwc3.yaml | 76 +
MAINTAINERS | 22 +
arch/arm/boot/dts/zte/Makefile | 2 +
.../boot/dts/zte/zx279128s-zte-zxhn-h3600.dts | 361 ++++
arch/arm/boot/dts/zte/zx279128s.dtsi | 397 +++++
arch/arm/include/asm/outercache.h | 22 +
arch/arm/mach-zte/Kconfig | 17 +
arch/arm/mach-zte/Makefile | 4 +
arch/arm/mach-zte/platsmp-zx279128s.c | 173 ++
arch/arm/mach-zte/zx279128s.c | 19 +
arch/arm/mm/Kconfig | 14 +
arch/arm/mm/cache-l2x0.c | 81 +
drivers/clk/Kconfig | 1 +
drivers/clk/Makefile | 1 +
drivers/clk/zte/Kconfig | 14 +
drivers/clk/zte/Makefile | 2 +
drivers/clk/zte/clk-zx279128s.c | 460 +++++
drivers/gpio/Kconfig | 12 +
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-zx279128s.c | 78 +
drivers/net/ethernet/zte/Kconfig | 13 +
drivers/net/ethernet/zte/Makefile | 3 +
drivers/net/ethernet/zte/zx279128s-eth.h | 385 +++++
drivers/net/ethernet/zte/zx279128s-main.c | 1485 +++++++++++++++++
drivers/net/ethernet/zte/zx279128s-ppe.c | 803 +++++++++
drivers/net/ethernet/zte/zx279128s-switch.c | 339 ++++
drivers/net/mdio/Kconfig | 11 +
drivers/net/mdio/Makefile | 1 +
drivers/net/mdio/mdio-zx279128s.c | 158 ++
drivers/net/phy/Kconfig | 8 +
drivers/net/phy/Makefile | 1 +
drivers/net/phy/sanechips.c | 153 ++
drivers/net/wireless/mediatek/mt76/dma.h | 2 +-
drivers/net/wireless/mediatek/mt76/mmio.c | 6 +-
drivers/net/wireless/mediatek/mt76/mt76.h | 23 +
.../net/wireless/mediatek/mt76/mt76x02_mmio.c | 2 +-
.../net/wireless/mediatek/mt76/mt7915/pci.c | 2 +-
drivers/pci/controller/dwc/Kconfig | 12 +
drivers/pci/controller/dwc/Makefile | 1 +
drivers/pci/controller/dwc/pcie-zx279128s.c | 322 ++++
drivers/spi/Kconfig | 13 +
drivers/spi/Makefile | 1 +
drivers/spi/spi-zx279128s-spifc.c | 353 ++++
drivers/usb/dwc3/Kconfig | 2 +-
drivers/usb/dwc3/dwc3-generic-plat.c | 1 +
include/dt-bindings/clock/zte,zx279128s-crm.h | 55 +
58 files changed, 6460 insertions(+), 8 deletions(-)
create mode 100644 Documentation/devicetree/bindings/clock/zte,zx279128s-crm.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/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/spi/spi-zx279128s-spifc.c
create mode 100644 include/dt-bindings/clock/zte,zx279128s-crm.h
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 01/24] dt-bindings: cache: l2c2x0: Describe ZTE device-read serialization
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 02/24] ARM: l2c: Serialize device reads with cache maintenance when requested Navid Ghahremani
` (22 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe the observed interconnect deadlock workaround separately from
the ARM implementation. This property and API remain an RFC requiring
ARM and DT review.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
Documentation/devicetree/bindings/cache/l2c2x0.yaml | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/Documentation/devicetree/bindings/cache/l2c2x0.yaml b/Documentation/devicetree/bindings/cache/l2c2x0.yaml
index ee604117ff..0295846cde 100644
--- a/Documentation/devicetree/bindings/cache/l2c2x0.yaml
+++ b/Documentation/devicetree/bindings/cache/l2c2x0.yaml
@@ -176,6 +176,15 @@ properties:
description: enable ECC protection on the L2 cache
type: boolean
+ zte,l2c-io-read-lock:
+ description: On the ZTE zx279128s, a CPU read from a PCIe device that is
+ still outstanding when an L2C-310 range or sync operation starts hangs
+ the interconnect. This property makes the L2C-310 maintenance operations
+ take a lock that the drivers of such devices also take around their
+ register reads. Valid only when the arm,pl310-cache compatible string
+ is used.
+ type: boolean
+
arm,outer-sync-disable:
description: disable the outer sync operation on the L2 cache.
Some core tiles, especially ARM PB11MPCore have a faulty L220 cache that
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 02/24] ARM: l2c: Serialize device reads with cache maintenance when requested
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
2026-10-10 12:59 ` [PATCH 01/24] dt-bindings: cache: l2c2x0: Describe ZTE device-read serialization Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 14:28 ` Arnd Bergmann
2026-10-10 12:59 ` [PATCH 03/24] wifi: mt76: Serialize MMIO reads with outer cache maintenance Navid Ghahremani
` (21 subsequent siblings)
23 siblings, 1 reply; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Provide the optional lock used by the tested ZX279128S workaround. Lock-
off experiments hung the board; the proposed API remains subject to
maintainer review.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
arch/arm/include/asm/outercache.h | 22 +++++++++
arch/arm/mm/Kconfig | 14 ++++++
arch/arm/mm/cache-l2x0.c | 81 +++++++++++++++++++++++++++++++
3 files changed, 117 insertions(+)
diff --git a/arch/arm/include/asm/outercache.h b/arch/arm/include/asm/outercache.h
index 3364637755..cf9552aaac 100644
--- a/arch/arm/include/asm/outercache.h
+++ b/arch/arm/include/asm/outercache.h
@@ -117,4 +117,26 @@ static inline void outer_resume(void) { }
#endif
+#ifdef CONFIG_CACHE_L2X0_IO_LOCK
+/**
+ * outer_io_lock - serialize a device read with outer cache maintenance
+ *
+ * On some SoCs a device read that is still outstanding while an outer
+ * cache range or sync operation is issued deadlocks the interconnect.
+ * Drivers for devices behind such a bus take this lock around their
+ * register reads. It is a no-op unless the platform enabled it.
+ *
+ * Returns the saved interrupt flags to pass to outer_io_unlock().
+ */
+unsigned long outer_io_lock(void);
+void outer_io_unlock(unsigned long flags);
+#else
+static inline unsigned long outer_io_lock(void)
+{
+ return 0;
+}
+
+static inline void outer_io_unlock(unsigned long flags) { }
+#endif
+
#endif /* __ASM_OUTERCACHE_H */
diff --git a/arch/arm/mm/Kconfig b/arch/arm/mm/Kconfig
index 6574659dfb..871c38b435 100644
--- a/arch/arm/mm/Kconfig
+++ b/arch/arm/mm/Kconfig
@@ -1015,6 +1015,20 @@ config CACHE_L2X0
help
This option enables the L2x0 PrimeCell.
+config CACHE_L2X0_IO_LOCK
+ bool "Serialize device reads with L2C-310 maintenance operations"
+ depends on CACHE_L2X0
+ help
+ On some SoCs, for example the ZTE/Sanechips ZX279128S, issuing an
+ L2C-310 range or sync operation while a CPU read from a PCIe
+ endpoint is still outstanding deadlocks the interconnect. This
+ option lets the L2C-310 maintenance operations take a spinlock
+ that drivers also hold around their device reads, through
+ outer_io_lock()/outer_io_unlock(). It is only active when the
+ cache controller node has the "zte,l2c-io-read-lock" property.
+
+ If unsure, say N.
+
config CACHE_L2X0_PMU
bool "L2x0 performance monitor support" if CACHE_L2X0
depends on PERF_EVENTS
diff --git a/arch/arm/mm/cache-l2x0.c b/arch/arm/mm/cache-l2x0.c
index 4708671600..35cc2eaf85 100644
--- a/arch/arm/mm/cache-l2x0.c
+++ b/arch/arm/mm/cache-l2x0.c
@@ -48,6 +48,7 @@ struct l2x0_regs l2x0_saved_regs;
static bool l2x0_bresp_disable;
static bool l2x0_flz_disable;
+static bool l2x0_io_lock_enable __initdata;
/*
* Common code for all cache controllers.
@@ -778,6 +779,81 @@ static const struct l2c_init_data l2c310_init_fns __initconst = {
},
};
+#ifdef CONFIG_CACHE_L2X0_IO_LOCK
+/*
+ * On some SoCs (e.g. the ZTE/Sanechips ZX279128S) a CPU read from a PCIe
+ * endpoint that is still outstanding when an L2C maintenance or sync
+ * operation is issued deadlocks the interconnect: both CPUs stall and only
+ * a watchdog reset recovers. The vendor kernel avoids this by holding one
+ * spinlock around the L2C operations and around the drivers' device reads.
+ * Do the same when the cache node asks for it. Drivers take the lock
+ * around their device reads with outer_io_lock()/outer_io_unlock().
+ */
+static DEFINE_RAW_SPINLOCK(l2x0_io_lock);
+static struct outer_cache_fns l2x0_io_fns __ro_after_init;
+static bool l2x0_io_lock_active __ro_after_init;
+
+unsigned long outer_io_lock(void)
+{
+ unsigned long flags = 0;
+
+ if (l2x0_io_lock_active)
+ raw_spin_lock_irqsave(&l2x0_io_lock, flags);
+ return flags;
+}
+EXPORT_SYMBOL_GPL(outer_io_lock);
+
+void outer_io_unlock(unsigned long flags)
+{
+ if (l2x0_io_lock_active)
+ raw_spin_unlock_irqrestore(&l2x0_io_lock, flags);
+}
+EXPORT_SYMBOL_GPL(outer_io_unlock);
+
+#define L2C_IO_LOCKED_RANGE_OP(op) \
+static void l2c_io_locked_##op(unsigned long start, unsigned long end) \
+{ \
+ unsigned long flags; \
+ \
+ raw_spin_lock_irqsave(&l2x0_io_lock, flags); \
+ l2x0_io_fns.op(start, end); \
+ raw_spin_unlock_irqrestore(&l2x0_io_lock, flags); \
+}
+
+L2C_IO_LOCKED_RANGE_OP(inv_range)
+L2C_IO_LOCKED_RANGE_OP(clean_range)
+L2C_IO_LOCKED_RANGE_OP(flush_range)
+
+static void l2c_io_locked_sync(void)
+{
+ unsigned long flags;
+
+ raw_spin_lock_irqsave(&l2x0_io_lock, flags);
+ l2x0_io_fns.sync();
+ raw_spin_unlock_irqrestore(&l2x0_io_lock, flags);
+}
+
+static void __init l2c_io_lock_install(struct outer_cache_fns *fns)
+{
+ l2x0_io_fns = *fns;
+ if (fns->inv_range)
+ fns->inv_range = l2c_io_locked_inv_range;
+ if (fns->clean_range)
+ fns->clean_range = l2c_io_locked_clean_range;
+ if (fns->flush_range)
+ fns->flush_range = l2c_io_locked_flush_range;
+ if (fns->sync)
+ fns->sync = l2c_io_locked_sync;
+ l2x0_io_lock_active = true;
+ pr_info("L2C: serializing device reads with maintenance operations\n");
+}
+#else
+static void __init l2c_io_lock_install(struct outer_cache_fns *fns)
+{
+ pr_warn("L2C: io read lock requested but CONFIG_CACHE_L2X0_IO_LOCK is disabled\n");
+}
+#endif
+
static int __init __l2c_init(const struct l2c_init_data *data,
u32 aux_val, u32 aux_mask, u32 cache_id, bool nosync)
{
@@ -859,6 +935,8 @@ static int __init __l2c_init(const struct l2c_init_data *data,
pr_info("L2C: disabling outer sync\n");
fns.sync = NULL;
}
+ if (l2x0_io_lock_enable)
+ l2c_io_lock_install(&fns);
/*
* Check if l2x0 controller is already enabled. If we are booting
@@ -1198,6 +1276,9 @@ static void __init l2c310_of_parse(const struct device_node *np,
if (of_property_read_bool(np, "arm,full-line-zero-disable"))
l2x0_flz_disable = true;
+ if (of_property_read_bool(np, "zte,l2c-io-read-lock"))
+ l2x0_io_lock_enable = true;
+
prefetch = l2x0_saved_regs.prefetch_ctrl;
ret = of_property_read_u32(np, "arm,double-linefill", &val);
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 03/24] wifi: mt76: Serialize MMIO reads with outer cache maintenance
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
2026-10-10 12:59 ` [PATCH 01/24] dt-bindings: cache: l2c2x0: Describe ZTE device-read serialization Navid Ghahremani
2026-10-10 12:59 ` [PATCH 02/24] ARM: l2c: Serialize device reads with cache maintenance when requested Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 04/24] dt-bindings: arm: Add ZTE ZX279128S platform descriptions Navid Ghahremani
` (20 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Apply the existing workaround to current mainline mt76. This depends on
the preceding ARM API and needs review together with that API.
Assisted-by: LLM
The implementation is refreshed from the existing locally signed mt76
patch; this new draft still requires review of the final dependency/API.
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
drivers/net/wireless/mediatek/mt76/dma.h | 2 +-
drivers/net/wireless/mediatek/mt76/mmio.c | 6 ++---
drivers/net/wireless/mediatek/mt76/mt76.h | 23 +++++++++++++++++++
.../net/wireless/mediatek/mt76/mt76x02_mmio.c | 2 +-
.../net/wireless/mediatek/mt76/mt7915/pci.c | 2 +-
5 files changed, 29 insertions(+), 6 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/dma.h b/drivers/net/wireless/mediatek/mt76/dma.h
index 7694764feb..721c54b92f 100644
--- a/drivers/net/wireless/mediatek/mt76/dma.h
+++ b/drivers/net/wireless/mediatek/mt76/dma.h
@@ -107,7 +107,7 @@ mt76_dma_handle_write(struct mt76_queue *q, u32 offset, u32 val)
u32 _offset = offsetof(struct mt76_queue_regs, _field); \
u32 _val; \
if (!mt76_dma_handle_read(_q, _offset, &_val)) \
- _val = readl(&(_q)->regs->_field); \
+ _val = mt76_readl(&(_q)->regs->_field); \
_val; \
})
diff --git a/drivers/net/wireless/mediatek/mt76/mmio.c b/drivers/net/wireless/mediatek/mt76/mmio.c
index 73d47608bf..95c6fe27f3 100644
--- a/drivers/net/wireless/mediatek/mt76/mmio.c
+++ b/drivers/net/wireless/mediatek/mt76/mmio.c
@@ -11,7 +11,7 @@ static u32 mt76_mmio_rr(struct mt76_dev *dev, u32 offset)
{
u32 val;
- val = readl(dev->mmio.regs + offset);
+ val = mt76_readl(dev->mmio.regs + offset);
trace_reg_rr(dev, offset, val);
return val;
@@ -53,13 +53,13 @@ static void mt76_mmio_read_copy(struct mt76_dev *dev, u32 offset,
int i;
for (i = 0; i + 4 <= len; i += 4)
- put_unaligned_le32(readl(dev->mmio.regs + offset + i),
+ put_unaligned_le32(mt76_readl(dev->mmio.regs + offset + i),
data + i);
if (i < len) {
u8 tmp[4];
- put_unaligned_le32(readl(dev->mmio.regs + offset + i), tmp);
+ put_unaligned_le32(mt76_readl(dev->mmio.regs + offset + i), tmp);
memcpy(data + i, tmp, len - i);
}
}
diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
index 62b41c8bb7..54b63ba614 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76.h
@@ -20,6 +20,29 @@
#include "util.h"
#include "testmode.h"
+#ifdef CONFIG_CACHE_L2X0_IO_LOCK
+#include <asm/outercache.h>
+#endif
+
+/*
+ * Some SoCs deadlock when an outer cache maintenance operation runs while a
+ * PCIe read is outstanding (see CONFIG_CACHE_L2X0_IO_LOCK). On those, take
+ * the platform lock around device register reads.
+ */
+static inline u32 mt76_readl(const void __iomem *addr)
+{
+#ifdef CONFIG_CACHE_L2X0_IO_LOCK
+ unsigned long flags = outer_io_lock();
+ u32 val = readl(addr);
+
+ outer_io_unlock(flags);
+
+ return val;
+#else
+ return readl(addr);
+#endif
+}
+
#define MT_MCU_RING_SIZE 32
#define MT_RX_BUF_SIZE 2048
#define MT_SKB_HEAD_LEN 256
diff --git a/drivers/net/wireless/mediatek/mt76/mt76x02_mmio.c b/drivers/net/wireless/mediatek/mt76/mt76x02_mmio.c
index dc7c03d231..0fdf2e3a2a 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76x02_mmio.c
+++ b/drivers/net/wireless/mediatek/mt76/mt76x02_mmio.c
@@ -357,7 +357,7 @@ static bool mt76x02_tx_hang(struct mt76x02_dev *dev)
q = dev->mphy.q_tx[i];
prev_dma_idx = dev->mt76.tx_dma_idx[i];
- dma_idx = readl(&q->regs->dma_idx);
+ dma_idx = mt76_readl(&q->regs->dma_idx);
dev->mt76.tx_dma_idx[i] = dma_idx;
if (!q->queued || prev_dma_idx != dma_idx) {
diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/pci.c b/drivers/net/wireless/mediatek/mt76/mt7915/pci.c
index 8007e62004..4c57e66a21 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7915/pci.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7915/pci.c
@@ -36,7 +36,7 @@ static struct mt7915_hif *mt7915_pci_get_hif2(u32 idx)
spin_lock_bh(&hif_lock);
list_for_each_entry(hif, &hif_list, list) {
- val = readl(hif->regs + MT_PCIE_RECOG_ID);
+ val = mt76_readl(hif->regs + MT_PCIE_RECOG_ID);
val &= MT_PCIE_RECOG_ID_MASK;
if (val != idx)
continue;
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 04/24] dt-bindings: arm: Add ZTE ZX279128S platform descriptions
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (2 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 03/24] wifi: mt76: Serialize MMIO reads with outer cache maintenance Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 05/24] ARM: zte: Add ZX279128S platform and CPU hotplug support Navid Ghahremani
` (19 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe the SoC board compatibility, SRAM used by the parked second CPU
and the SMP enable method.
Assisted-by: LLM
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 [flat|nested] 30+ messages in thread
* [PATCH 05/24] ARM: zte: Add ZX279128S platform and CPU hotplug support
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (3 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 04/24] dt-bindings: arm: Add ZTE ZX279128S platform descriptions Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 14:13 ` Arnd Bergmann
2026-10-10 12:59 ` [PATCH 06/24] dt-bindings: clock: Add ZTE ZX279128S CRM clocks and resets Navid Ghahremani
` (18 subsequent siblings)
23 siblings, 1 reply; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Support the dual Cortex-A9 platform, second-CPU startup through
bootloader SRAM and CPU hotplug. Keep existing ZX297520V3 support
intact.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
MAINTAINERS | 22 ++++
arch/arm/mach-zte/Kconfig | 17 +++
arch/arm/mach-zte/Makefile | 4 +
arch/arm/mach-zte/platsmp-zx279128s.c | 173 ++++++++++++++++++++++++++
arch/arm/mach-zte/zx279128s.c | 19 +++
5 files changed, 235 insertions(+)
create mode 100644 arch/arm/mach-zte/platsmp-zx279128s.c
create mode 100644 arch/arm/mach-zte/zx279128s.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 4f72b4ac27..4d0f4e5485 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3894,6 +3894,28 @@ F: drivers/video/fbdev/vt8500lcdfb.*
F: drivers/video/fbdev/wm8505fb*
F: drivers/video/fbdev/wmt_ge_rops.*
+ARM/ZTE ZX279128S SOC SUPPORT
+M: Navid Ghahremani <ghahramani.navid@gmail.com>
+L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
+S: Maintained
+F: Documentation/devicetree/bindings/clock/zte,zx279128s-crm.yaml
+F: Documentation/devicetree/bindings/gpio/zte,zx279128s-gpio.yaml
+F: Documentation/devicetree/bindings/net/zte,zx279128s-gmac.yaml
+F: Documentation/devicetree/bindings/net/zte,zx279128s-mdio.yaml
+F: Documentation/devicetree/bindings/pci/zte,zx279128s-pcie.yaml
+F: Documentation/devicetree/bindings/spi/zte,zx279128s-spifc.yaml
+F: Documentation/devicetree/bindings/usb/zte,zx279128s-dwc3.yaml
+F: arch/arm/boot/dts/zte/zx279128s*
+F: arch/arm/mach-zte/*zx279128s*
+F: drivers/clk/zte/clk-zx279128s.c
+F: drivers/gpio/gpio-zx279128s.c
+F: drivers/net/ethernet/zte/
+F: drivers/net/mdio/mdio-zx279128s.c
+F: drivers/net/phy/sanechips.c
+F: drivers/pci/controller/dwc/pcie-zx279128s.c
+F: drivers/spi/spi-zx279128s-spifc.c
+F: include/dt-bindings/clock/zte,zx279128s-crm.h
+
ARM/ZTE ZX29 SOC SUPPORT
M: Stefan Dösinger <stefandoesinger@gmail.com>
L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
diff --git a/arch/arm/mach-zte/Kconfig b/arch/arm/mach-zte/Kconfig
index d3b404ca48..ff3845f109 100644
--- a/arch/arm/mach-zte/Kconfig
+++ b/arch/arm/mach-zte/Kconfig
@@ -26,4 +26,21 @@ 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 CACHE_L2X0_IO_LOCK
+ 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 [flat|nested] 30+ messages in thread
* [PATCH 06/24] dt-bindings: clock: Add ZTE ZX279128S CRM clocks and resets
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (4 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 05/24] ARM: zte: Add ZX279128S platform and CPU hotplug support Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 07/24] clk: zte: Add ZX279128S CRM clock and reset driver Navid Ghahremani
` (17 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe the top CRM and the two low-speed peripheral clock domains,
including the confirmed USB reset bits. Uncertain clock-source rates are
called out in the cover letter.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
.../bindings/clock/zte,zx279128s-crm.yaml | 141 ++++++++++++++++++
include/dt-bindings/clock/zte,zx279128s-crm.h | 55 +++++++
2 files changed, 196 insertions(+)
create mode 100644 Documentation/devicetree/bindings/clock/zte,zx279128s-crm.yaml
create mode 100644 include/dt-bindings/clock/zte,zx279128s-crm.h
diff --git a/Documentation/devicetree/bindings/clock/zte,zx279128s-crm.yaml b/Documentation/devicetree/bindings/clock/zte,zx279128s-crm.yaml
new file mode 100644
index 0000000000..2964c0fd6b
--- /dev/null
+++ b/Documentation/devicetree/bindings/clock/zte,zx279128s-crm.yaml
@@ -0,0 +1,141 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/clock/zte,zx279128s-crm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ZTE zx279128s clock and reset modules
+
+maintainers:
+ - Navid Ghahremani <ghahramani.navid@gmail.com>
+
+description: |
+ The top clock and reset module (CRM) of the ZTE zx279128s holds the PLLs and
+ the clocks of the CPUs, the buses and the bus interfaces of the peripheral
+ groups. Each of the two groups of low speed peripherals (LSP) has a clock
+ and reset/power module (CRPM) of its own, fed by the top CRM.
+
+ The top CRM also holds reset bits of several peripherals; it is a reset
+ controller too.
+
+ The clock and reset indices are in
+ include/dt-bindings/clock/zte,zx279128s-crm.h.
+
+properties:
+ compatible:
+ oneOf:
+ - items:
+ - const: zte,zx279128s-topcrm
+ - const: syscon
+ - enum:
+ - zte,zx279128s-lsp0crpm
+ - zte,zx279128s-lsp1crpm
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ minItems: 1
+ maxItems: 5
+
+ clock-names:
+ minItems: 1
+ maxItems: 5
+
+ '#clock-cells':
+ const: 1
+
+ '#reset-cells':
+ const: 1
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - '#clock-cells'
+
+allOf:
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: zte,zx279128s-topcrm
+ then:
+ properties:
+ clocks:
+ items:
+ - description: 25 MHz crystal
+ clock-names:
+ items:
+ - const: osc
+ required:
+ - '#reset-cells'
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: zte,zx279128s-lsp0crpm
+ then:
+ properties:
+ '#reset-cells': false
+ 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:
+ '#reset-cells': false
+ 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>
+
+ topcrm: clock-controller@94000000 {
+ compatible = "zte,zx279128s-topcrm", "syscon";
+ reg = <0x94000000 0x1000>;
+ clocks = <&osc>;
+ clock-names = "osc";
+ #clock-cells = <1>;
+ #reset-cells = <1>;
+ };
+
+ 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/include/dt-bindings/clock/zte,zx279128s-crm.h b/include/dt-bindings/clock/zte,zx279128s-crm.h
new file mode 100644
index 0000000000..41fb041c8e
--- /dev/null
+++ b/include/dt-bindings/clock/zte,zx279128s-crm.h
@@ -0,0 +1,55 @@
+/* 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
+
+/*
+ * Top CRM resets: (register offset / 4) * 32 + bit. The USB 3.0 controller
+ * has four in register 0x4c; what each one resets isn't known.
+ */
+#define ZX279128S_TOP_RST_USB_B9 617
+#define ZX279128S_TOP_RST_USB_B10 618
+#define ZX279128S_TOP_RST_USB_B11 619
+#define ZX279128S_TOP_RST_USB_B14 622
+
+/* 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
+
+#endif
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 07/24] clk: zte: Add ZX279128S CRM clock and reset driver
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (5 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 06/24] dt-bindings: clock: Add ZTE ZX279128S CRM clocks and resets Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 08/24] dt-bindings: gpio: Add ZTE ZX279128S GPIO controller Navid Ghahremani
` (16 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Read the bootloader PLL and bus setup and provide peripheral gates,
muxes, dividers and USB resets. Preserve the parked CPU memory clocks
and use managed registration for platform clocks.
Assisted-by: LLM
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 | 460 ++++++++++++++++++++++++++++++++
5 files changed, 478 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..1282d17fcf
--- /dev/null
+++ b/drivers/clk/zte/clk-zx279128s.c
@@ -0,0 +1,460 @@
+// 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/reset/reset-simple.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+
+#include <dt-bindings/clock/zte,zx279128s-crm.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_RST_LAST 0x4c /* the last register with resets */
+
+#define TOP_NUM_CLKS (ZX279128S_TOP_USB_REF + 1)
+#define TOP_NUM_RESETS ((TOP_RST_LAST / 4 + 1) * 32)
+
+/*
+ * 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
+
+static DEFINE_SPINLOCK(zx_top_lock);
+
+/*
+ * 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);
+}
+
+/*
+ * 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 reset_simple_data *rst;
+
+ rst = kzalloc_obj(*rst);
+ if (!rst)
+ return -ENOMEM;
+
+ spin_lock_init(&rst->lock);
+ rst->membase = base;
+ rst->active_low = true;
+ rst->status_active_low = true;
+ rst->rcdev.ops = &reset_simple_ops;
+ rst->rcdev.owner = THIS_MODULE;
+ rst->rcdev.nr_resets = TOP_NUM_RESETS;
+ rst->rcdev.of_node = np;
+
+ return reset_controller_register(&rst->rcdev);
+}
+
+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(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);
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 08/24] dt-bindings: gpio: Add ZTE ZX279128S GPIO controller
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (6 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 07/24] clk: zte: Add ZX279128S CRM clock and reset driver Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 09/24] gpio: Add ZTE ZX279128S GPIO driver Navid Ghahremani
` (15 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe the register-clock consumer and GPIO banks.
Assisted-by: LLM
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 [flat|nested] 30+ messages in thread
* [PATCH 09/24] gpio: Add ZTE ZX279128S GPIO driver
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (7 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 08/24] dt-bindings: gpio: Add ZTE ZX279128S GPIO controller Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 10/24] dt-bindings: pinctrl: pinctrl-single: Add ZTE ZX279128S pin mux Navid Ghahremani
` (14 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Expose the six banks using the generic GPIO implementation and enable
the register clock before accessing the bank.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
drivers/gpio/Kconfig | 12 ++++++
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-zx279128s.c | 78 +++++++++++++++++++++++++++++++++++
3 files changed, 91 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..8f37b908bb
--- /dev/null
+++ b/drivers/gpio/gpio-zx279128s.c
@@ -0,0 +1,78 @@
+// 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/mod_devicetable.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 = 2;
+ 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");
+
+ 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 [flat|nested] 30+ messages in thread
* [PATCH 10/24] dt-bindings: pinctrl: pinctrl-single: Add ZTE ZX279128S pin mux
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (8 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 09/24] gpio: Add ZTE ZX279128S GPIO driver Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 11/24] dt-bindings: mfd: syscon: Add ZX279128S system controller Navid Ghahremani
` (13 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Use pinctrl-single for the verified PCIe and external-PHY bit selections
without guessing the unverified field layout.
Assisted-by: LLM
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 [flat|nested] 30+ messages in thread
* [PATCH 11/24] dt-bindings: mfd: syscon: Add ZX279128S system controller
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (9 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 10/24] dt-bindings: pinctrl: pinctrl-single: Add ZTE ZX279128S pin mux Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 12/24] dt-bindings: PCI: Add ZTE ZX279128S host controller Navid Ghahremani
` (12 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe the remaining system-control syscon. PCIe clock/reset/PHY
modeling is explicitly a review question.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
Documentation/devicetree/bindings/mfd/syscon.yaml | 1 +
1 file changed, 1 insertion(+)
diff --git a/Documentation/devicetree/bindings/mfd/syscon.yaml b/Documentation/devicetree/bindings/mfd/syscon.yaml
index 945a168b61..cc0eb377ec 100644
--- a/Documentation/devicetree/bindings/mfd/syscon.yaml
+++ b/Documentation/devicetree/bindings/mfd/syscon.yaml
@@ -132,6 +132,7 @@ properties:
- ti,j784s4-acspcie-proxy-ctrl
- ti,j784s4-pcie-ctrl
- ti,keystone-pllctrl
+ - zte,zx279128s-crm2
- const: syscon
- items:
- enum:
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 12/24] dt-bindings: PCI: Add ZTE ZX279128S host controller
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (10 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 11/24] dt-bindings: mfd: syscon: Add ZX279128S system controller Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 13/24] PCI: dwc: Add ZTE ZX279128S host controller driver Navid Ghahremani
` (11 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe both DesignWare hosts, the two control syscons and the shared
Wi-Fi reset lines. The shared-resource model remains an RFC question.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
.../bindings/pci/zte,zx279128s-pcie.yaml | 97 +++++++++++++++++++
1 file changed, 97 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..57a4e96610
--- /dev/null
+++ b/Documentation/devicetree/bindings/pci/zte,zx279128s-pcie.yaml
@@ -0,0 +1,97 @@
+# 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/snps,dw-pcie.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
+
+ zte,crm:
+ $ref: /schemas/types.yaml#/definitions/phandle-array
+ description: The two Top CRM syscons that hold the clock, reset and PHY
+ controls of this controller.
+ items:
+ - items:
+ - description: phandle to Top CRM 1
+ - items:
+ - description: phandle to Top CRM 2
+
+ reset-gpios:
+ maxItems: 1
+ description: Board reset line. A device connected to both controllers
+ (the MT7915 Wi-Fi chip of the ZTE ZXHN H3600) has its lines listed in
+ both nodes; the first controller to start resets and releases it.
+
+ enable-gpios:
+ maxItems: 1
+ description: Second board line that is driven together with reset-gpios.
+
+required:
+ - reg
+ - reg-names
+ - interrupts
+ - '#interrupt-cells'
+ - interrupt-map
+ - interrupt-map-mask
+ - zte,crm
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/arm-gic.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>;
+ zte,crm = <&topcrm>, <&crm2>;
+ reset-gpios = <&gpio3 5 GPIO_ACTIVE_LOW>;
+ enable-gpios = <&gpio3 7 GPIO_ACTIVE_HIGH>;
+ };
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 13/24] PCI: dwc: Add ZTE ZX279128S host controller driver
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (11 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 12/24] dt-bindings: PCI: Add ZTE ZX279128S host controller Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 14/24] dt-bindings: net: Add ZTE ZX279128S MDIO controller Navid Ghahremani
` (10 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Bring up both controller links using the verified vendor sequence and
shared board reset. Propagate CRM/GPIO failures and retain the tested
Gen1/INTx configuration.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
drivers/pci/controller/dwc/Kconfig | 12 +
drivers/pci/controller/dwc/Makefile | 1 +
drivers/pci/controller/dwc/pcie-zx279128s.c | 322 ++++++++++++++++++++
3 files changed, 335 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..511f59d079 100644
--- a/drivers/pci/controller/dwc/Kconfig
+++ b/drivers/pci/controller/dwc/Kconfig
@@ -577,4 +577,16 @@ 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 MFD_SYSCON
+ select PCIE_DW_HOST
+ 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..3559beb35a
--- /dev/null
+++ b/drivers/pci/controller/dwc/pcie-zx279128s.c
@@ -0,0 +1,322 @@
+// 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 controlled through the Top CRM (clock and reset
+ * module) blocks, and a pin state from the pin mux is applied before probe.
+ * The register sequence comes from the vendor firmware, and the meaning of
+ * most of its bits is not documented.
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/init.h>
+#include <linux/io.h>
+#include <linux/mfd/syscon.h>
+#include <linux/mod_devicetable.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/pci.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#include "pcie-designware.h"
+
+/* Top CRM 1: PHY and PLL controls */
+#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
+
+/* Top CRM 2: clocks and resets */
+#define CRM2_CLK 0x070
+#define CRM2_RST 0x074
+#define CRM2_RST_ALL 0x3f
+
+/* 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
+
+enum { ZX_CRM1, ZX_CRM2, ZX_NUM_CRM };
+
+/* The CRM bits of one controller, which is identified by its ctrl address */
+struct zx279128s_pcie_port {
+ resource_size_t ctrl_addr;
+ u32 clk_on; /* CRM2_CLK bits set first */
+ u32 clk_off; /* CRM2_CLK bit cleared at the end */
+ u32 phy_on[3]; /* CRM1_PHY_CTRL bits, set in this order */
+ bool phy_cal; /* write the CRM1_PHY1_CAL values */
+ u32 rst; /* CRM2_RST bit */
+};
+
+static const struct zx279128s_pcie_port zx279128s_pcie_ports[] = {
+ {
+ .ctrl_addr = 0x09500000,
+ .clk_on = 0xb5,
+ .clk_off = BIT(8),
+ .phy_on = { BIT(15), BIT(13), BIT(14) },
+ .rst = BIT(0),
+ }, {
+ .ctrl_addr = 0x09600000,
+ .clk_on = 0x1aa00,
+ .clk_off = BIT(17),
+ .phy_on = { BIT(27), BIT(25), BIT(26) },
+ .phy_cal = true,
+ .rst = BIT(3),
+ },
+};
+
+struct zx279128s_pcie {
+ struct dw_pcie pci;
+ void __iomem *ctrl;
+ struct regmap *crm[ZX_NUM_CRM];
+ const struct zx279128s_pcie_port *port;
+ 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;
+
+ if (WARN_ON(!cap))
+ return;
+
+ dw_pcie_dbi_ro_wr_en(pci);
+
+ val = dw_pcie_readl_dbi(pci, cap + PCI_EXP_LNKCTL2);
+ val &= ~PCI_EXP_LNKCTL2_TLS;
+ val |= PCI_EXP_LNKCTL2_TLS_2_5GT;
+ dw_pcie_writel_dbi(pci, cap + PCI_EXP_LNKCTL2, val);
+
+ val = dw_pcie_readl_dbi(pci, cap + PCI_EXP_LNKCTL);
+ dw_pcie_writel_dbi(pci, cap + PCI_EXP_LNKCTL, val | PCI_EXP_LNKCTL_RL);
+
+ dw_pcie_dbi_ro_wr_dis(pci);
+}
+
+static int zx279128s_pcie_init_crm(struct zx279128s_pcie *pcie)
+{
+ const struct zx279128s_pcie_port *port = pcie->port;
+ struct regmap *crm1 = pcie->crm[ZX_CRM1];
+ struct regmap *crm2 = pcie->crm[ZX_CRM2];
+ int ret, i;
+
+ /* Ungate the clocks */
+ ret = regmap_write_bits(crm2, CRM2_CLK, port->clk_on, port->clk_on);
+ if (ret)
+ return ret;
+ fsleep(50);
+
+ /* Power up the PHY and its PLL */
+ for (i = 0; i < ARRAY_SIZE(port->phy_on); i++) {
+ ret = regmap_write_bits(crm1, CRM1_PHY_CTRL, port->phy_on[i],
+ port->phy_on[i]);
+ if (ret)
+ return ret;
+ fsleep(50);
+
+ if (i == 0 && port->phy_cal) {
+ ret = regmap_write(crm1, CRM1_PHY1_CAL1, 0x00202d5a);
+ if (ret)
+ return ret;
+ ret = regmap_write(crm1, CRM1_PHY1_CAL0, 0x0046c24a);
+ if (ret)
+ return ret;
+ }
+ }
+
+ ret = regmap_write_bits(crm2, CRM2_RST, port->rst, 0);
+ if (ret)
+ return ret;
+ fsleep(50);
+
+ ret = regmap_write_bits(crm1, CRM1_MISC, CRM1_MISC_BIT26, 0);
+ if (ret)
+ return ret;
+ fsleep(50);
+
+ ret = regmap_write_bits(crm2, CRM2_RST, port->rst, port->rst);
+ if (ret)
+ return ret;
+ ret = regmap_write_bits(crm2, CRM2_CLK, port->clk_off, 0);
+ if (ret)
+ return ret;
+
+ return regmap_write(crm2, CRM2_RST, CRM2_RST_ALL);
+}
+
+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;
+ }
+ mutex_unlock(&zx279128s_board_lock);
+
+ ret = zx279128s_pcie_init_crm(pcie);
+ if (ret)
+ return dev_err_probe(pci->dev, ret, "failed to initialize CRM\n");
+
+ /* 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_unlock:
+ mutex_unlock(&zx279128s_board_lock);
+ return dev_err_probe(pci->dev, ret, "failed to drive the board GPIOs\n");
+}
+
+/*
+ * 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,
+ .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 device_node *np;
+ struct resource *res;
+ int i;
+
+ 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);
+
+ for (i = 0; i < ARRAY_SIZE(zx279128s_pcie_ports); i++)
+ if (zx279128s_pcie_ports[i].ctrl_addr == res->start)
+ pcie->port = &zx279128s_pcie_ports[i];
+ if (!pcie->port)
+ return dev_err_probe(dev, -EINVAL, "unknown controller at %pR\n",
+ res);
+
+ for (i = 0; i < ZX_NUM_CRM; i++) {
+ np = of_parse_phandle(dev->of_node, "zte,crm", i);
+ if (!np)
+ return dev_err_probe(dev, -EINVAL,
+ "missing Top CRM %d\n", i + 1);
+
+ pcie->crm[i] = syscon_node_to_regmap(np);
+ of_node_put(np);
+ if (IS_ERR(pcie->crm[i]))
+ return dev_err_probe(dev, PTR_ERR(pcie->crm[i]),
+ "failed to get Top CRM %d\n", i + 1);
+ }
+
+ /*
+ * Shared with the other controller: take the lines as they are, so
+ * that a late probe can't reset a device the other one already uses.
+ */
+ pcie->reset = devm_gpiod_get_optional(dev, "reset",
+ GPIOD_ASIS | GPIOD_FLAGS_BIT_NONEXCLUSIVE);
+ if (IS_ERR(pcie->reset))
+ 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 | GPIOD_FLAGS_BIT_NONEXCLUSIVE);
+ if (IS_ERR(pcie->enable))
+ 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 [flat|nested] 30+ messages in thread
* [PATCH 14/24] dt-bindings: net: Add ZTE ZX279128S MDIO controller
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (12 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 13/24] PCI: dwc: Add ZTE ZX279128S host controller driver Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 15/24] net: mdio: " Navid Ghahremani
` (9 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe the MDIO register block and its work/register clocks. External-
PHY pin selections use pinctrl rather than direct syscon writes.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
.../bindings/net/zte,zx279128s-mdio.yaml | 67 +++++++++++++++++++
1 file changed, 67 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..49ff7e7b1a
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/zte,zx279128s-mdio.yaml
@@ -0,0 +1,67 @@
+# 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>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&ext_phy_pins>;
+
+ ethernet-phy@8 {
+ reg = <8>;
+ };
+
+ ethernet-phy@10 {
+ reg = <10>;
+ };
+ };
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 15/24] net: mdio: Add ZTE ZX279128S MDIO controller
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (13 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 14/24] dt-bindings: net: Add ZTE ZX279128S MDIO controller Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 16/24] net: phy: Add Sanechips ZX5201 PHY support Navid Ghahremani
` (8 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Provide bounded Clause 22 transactions and managed clock and MDIO-bus
registration.
Assisted-by: LLM
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 | 158 ++++++++++++++++++++++++++++++
3 files changed, 170 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..4c4a9ada68
--- /dev/null
+++ b/drivers/net/mdio/mdio-zx279128s.c
@@ -0,0 +1,158 @@
+// 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/mod_devicetable.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 [flat|nested] 30+ messages in thread
* [PATCH 16/24] net: phy: Add Sanechips ZX5201 PHY support
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (14 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 15/24] net: mdio: " Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 17/24] dt-bindings: net: Add ZTE ZX279128S Ethernet switch Navid Ghahremani
` (7 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Initialize the external WAN PHY and its SerDes path; generic PHY support
handles negotiation.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
drivers/net/phy/Kconfig | 8 ++
drivers/net/phy/Makefile | 1 +
drivers/net/phy/sanechips.c | 153 ++++++++++++++++++++++++++++++++++++
3 files changed, 162 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..bb5129b2bb
--- /dev/null
+++ b/drivers/net/phy/sanechips.c
@@ -0,0 +1,153 @@
+// 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/mod_devicetable.h>
+#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 [flat|nested] 30+ messages in thread
* [PATCH 17/24] dt-bindings: net: Add ZTE ZX279128S Ethernet switch
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (15 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 16/24] net: phy: Add Sanechips ZX5201 PHY support Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 18/24] net: ethernet: zte: Add ZX279128S Ethernet switch driver Navid Ghahremani
` (6 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe the switch, receive interrupt and per-port PHY modes and MAC
addresses.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
.../bindings/net/zte,zx279128s-gmac.yaml | 108 ++++++++++++++++++
1 file changed, 108 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..56c81aa6e7
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/zte,zx279128s-gmac.yaml
@@ -0,0 +1,108 @@
+# 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>
+
+ 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 [flat|nested] 30+ messages in thread
* [PATCH 18/24] net: ethernet: zte: Add ZX279128S Ethernet switch driver
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (16 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 17/24] dt-bindings: net: Add ZTE ZX279128S Ethernet switch Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 19/24] dt-bindings: spi: Add ZTE ZX279128S SPI flash controller Navid Ghahremani
` (5 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
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.
Assisted-by: LLM
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 | 385 +++++
drivers/net/ethernet/zte/zx279128s-main.c | 1485 +++++++++++++++++++
drivers/net/ethernet/zte/zx279128s-ppe.c | 803 ++++++++++
drivers/net/ethernet/zte/zx279128s-switch.c | 339 +++++
6 files changed, 3028 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..4c41b6c5ed
--- /dev/null
+++ b/drivers/net/ethernet/zte/zx279128s-eth.h
@@ -0,0 +1,385 @@
+/* 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_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..3b9834d63f
--- /dev/null
+++ b/drivers/net/ethernet/zte/zx279128s-main.c
@@ -0,0 +1,1485 @@
+// 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;
+
+ if (bp == 0) {
+ adapter->rx_nobuf_cnt++;
+ zx_rx_consume(desc, rel);
+ adapter->rx_cur_idx[q]++;
+ work_done++;
+ avail--;
+ continue;
+ }
+
+ /* 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;
+ int i, ret;
+
+ struct device_node *ports_np __free(device_node) =
+ 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_scoped(ports_np, port_np) {
+ u32 port;
+
+ if (of_property_read_u32(port_np, "reg", &port) ||
+ port >= ZX_NUM_PORTS)
+ continue;
+ ret = of_get_mac_address(port_np, addrs[port]);
+ if (ret == -EPROBE_DEFER)
+ return ret;
+ if (!ret)
+ __set_bit(port, &have_addr);
+ }
+
+ ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to set the DMA mask\n");
+
+ adapter = devm_kzalloc(dev, sizeof(*adapter), GFP_KERNEL);
+ if (!adapter)
+ return -ENOMEM;
+
+ adapter->irq = platform_get_irq(pdev, 0);
+ if (adapter->irq < 0)
+ 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)
+ return ret;
+
+ adapter->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(adapter->base))
+ return PTR_ERR(adapter->base);
+
+ adapter->buffer_pool = dma_alloc_coherent(dev, ZX_DMA_ARENA_SIZE,
+ &adapter->buf_pool_dma, GFP_KERNEL);
+ if (!adapter->buffer_pool)
+ return -ENOMEM;
+ 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)
+ return ret;
+ 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);
+ }
+
+ /* 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);
+ }
+ }
+ 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;
+
+ 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..d9c6c1d249
--- /dev/null
+++ b/drivers/net/ethernet/zte/zx279128s-ppe.c
@@ -0,0 +1,803 @@
+// 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, ð);
+ 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;
+
+ /* 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);
+ rhashtable_init(&adapter->ppe_flows, &zx_ppe_ht_params);
+ 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..91df8de455
--- /dev/null
+++ b/drivers/net/ethernet/zte/zx279128s-switch.c
@@ -0,0 +1,339 @@
+// 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 n = sizes[readl(base + ZX_SBRG_TABLE_SEL) & 3];
+ int ram, i;
+
+ for (ram = 0; ram < 4; ram++) {
+ for (i = 0; i < n; 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;
+ if (zx_sbrg_ind(adapter, ram << 22 | i))
+ return;
+ writel(0, base + ZX_SBRG_IND_DATA(2));
+ writel(0, base + ZX_SBRG_IND_DATA(1));
+ writel(0, base + ZX_SBRG_IND_DATA(0));
+ 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);
+ if (zx_sbrg_ind(adapter, ZX_SBRG_MEM_VLAN << 22 | 1))
+ dev_warn(adapter->dev, "bridge VLAN table access timed out\n");
+ else
+ writel(members ? vlan | ZX_SBRG_VLAN_VALID : 0,
+ base + ZX_SBRG_IND_DATA(0));
+ /* 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)
+{
+ 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;
+ adapter->sw_ready =
+ !register_netdevice_notifier(&adapter->sw_netdev_nb) &&
+ !register_switchdev_notifier(&adapter->sw_switchdev_nb) &&
+ !register_switchdev_blocking_notifier(&adapter->sw_switchdev_blocking_nb);
+ if (!adapter->sw_ready)
+ dev_warn(adapter->dev,
+ "no bridge notifications: hardware LAN switching unavailable\n");
+}
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 19/24] dt-bindings: spi: Add ZTE ZX279128S SPI flash controller
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (17 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 18/24] net: ethernet: zte: Add ZX279128S Ethernet switch driver Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 20/24] " Navid Ghahremani
` (4 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe the flash-operation controller and its work and register
clocks.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
.../bindings/spi/zte,zx279128s-spifc.yaml | 69 +++++++++++++++++++
1 file changed, 69 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..967d460ec2
--- /dev/null
+++ b/Documentation/devicetree/bindings/spi/zte,zx279128s-spifc.yaml
@@ -0,0 +1,69 @@
+# 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:
+ - 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:
+ const: 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 [flat|nested] 30+ messages in thread
* [PATCH 20/24] spi: Add ZTE ZX279128S SPI flash controller
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (18 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 19/24] dt-bindings: spi: Add ZTE ZX279128S SPI flash controller Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 21/24] dt-bindings: usb: Add ZTE ZX279128S DWC3 controller Navid Ghahremani
` (3 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Execute bounded SPI-memory operations through the controller FIFO. Honor
device and operation clock limits through the CRPM divider and cap the
validated maximum at 50 MHz.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
drivers/spi/Kconfig | 13 ++
drivers/spi/Makefile | 1 +
drivers/spi/spi-zx279128s-spifc.c | 353 ++++++++++++++++++++++++++++++
3 files changed, 367 insertions(+)
create mode 100644 drivers/spi/spi-zx279128s-spifc.c
diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index a0f37e2df7..f40fbbb181 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -1320,6 +1320,19 @@ config SPI_XTENSA_XTFPGA
16 bit words in SPI mode 0, automatically asserting CS on transfer
start and deasserting on end.
+config SPI_ZX279128S_SPIFC
+ tristate "ZTE zx279128s 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 of the ZTE
+ zx279128s SoC, through which it reaches its boot flash (a SPI
+ NAND on the ZTE ZXHN H3600).
+
+ 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..fc93fcc991 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_ZX279128S_SPIFC) += spi-zx279128s-spifc.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-zx279128s-spifc.c b/drivers/spi/spi-zx279128s-spifc.c
new file mode 100644
index 0000000000..12aa22baf8
--- /dev/null
+++ b/drivers/spi/spi-zx279128s-spifc.c
@@ -0,0 +1,353 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ZTE zx279128s SPI flash controller (SPIFC)
+ *
+ * Copyright (C) 2026 Navid Ghahremani <ghahramani.navid@gmail.com>
+ *
+ * The controller runs one flash operation at a time: the driver writes the
+ * opcode, the address, a description of the operation's phases and the data
+ * length, and starts it. Data passes through a 32-bit FIFO. The command and
+ * address phases always use one line; the data phase can use one, two or
+ * four. The register layout comes from the vendor boot loader's table of
+ * flash operations.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/minmax.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/spi/spi.h>
+#include <linux/spi/spi-mem.h>
+#include <linux/string.h>
+
+#define SPIFC_TRIGGER 0x04
+#define SPIFC_TRIGGER_START BIT(0)
+#define SPIFC_ACK 0x08
+#define SPIFC_ACK_DONE BIT(0) /* written while _BUSY is clear */
+#define SPIFC_ACK_BUSY BIT(1)
+#define SPIFC_XFER_CFG 0x0c
+#define SPIFC_XFER_CFG_BITS 0x1c440 /* set before every operation */
+#define SPIFC_FMT0 0x10 /* the phases of the operation */
+#define SPIFC_FMT0_DATA_OUT BIT(0)
+#define SPIFC_FMT0_DATA_IN BIT(1)
+#define SPIFC_FMT0_DUMMY BIT(2)
+#define SPIFC_FMT0_ADDR BIT(4)
+#define SPIFC_FMT1 0x14 /* and their sizes */
+#define SPIFC_FMT1_DATA_LINES GENMASK(2, 0)
+#define SPIFC_LINES_1 0
+#define SPIFC_LINES_2 4
+#define SPIFC_LINES_4 5
+#define SPIFC_FMT1_ADDR_BYTES GENMASK(6, 5) /* bytes - 1 */
+#define SPIFC_FMT1_DUMMY_BYTES GENMASK(15, 12)
+#define SPIFC_LEN 0x18 /* data bytes - 1 */
+#define SPIFC_ADDR 0x1c
+#define SPIFC_OPCODE 0x20
+#define SPIFC_TIMING 0x24
+#define SPIFC_TIMING_SAMPLE GENMASK(17, 16)
+#define SPIFC_STATUS 0x2c
+#define SPIFC_STATUS_DONE BIT(0)
+#define SPIFC_BURST 0x30
+#define SPIFC_BURST_VAL 0x3f
+#define SPIFC_FIFO_STATUS 0x34
+#define SPIFC_FIFO_RX_AVAIL GENMASK(12, 8)
+#define SPIFC_FIFO_TX_FREE GENMASK(20, 16)
+#define SPIFC_FIFO_DATA 0x38
+
+#define SPIFC_MAX_DATA 4096
+#define SPIFC_TIMEOUT_US 20000
+
+struct zx_spifc {
+ void __iomem *base;
+ struct clk *wclk;
+ unsigned long speed_hz;
+};
+
+static void zx_spifc_ack(struct zx_spifc *spifc)
+{
+ u32 val = readl(spifc->base + SPIFC_ACK);
+
+ if (!(val & SPIFC_ACK_BUSY))
+ writel(val | SPIFC_ACK_DONE, spifc->base + SPIFC_ACK);
+}
+
+static void zx_spifc_start(struct zx_spifc *spifc)
+{
+ writel(readl(spifc->base + SPIFC_TRIGGER) | SPIFC_TRIGGER_START,
+ spifc->base + SPIFC_TRIGGER);
+}
+
+static int zx_spifc_wait_fifo(struct zx_spifc *spifc, u32 mask)
+{
+ u32 val;
+
+ return readl_poll_timeout_atomic(spifc->base + SPIFC_FIFO_STATUS, val,
+ val & mask, 1, SPIFC_TIMEOUT_US);
+}
+
+static int zx_spifc_read_fifo(struct zx_spifc *spifc, u8 *buf,
+ unsigned int len)
+{
+ unsigned int i;
+ u32 word;
+ int ret;
+
+ for (i = 0; i < len; i += 4) {
+ ret = zx_spifc_wait_fifo(spifc, SPIFC_FIFO_RX_AVAIL);
+ if (ret)
+ return ret;
+ word = readl(spifc->base + SPIFC_FIFO_DATA);
+ memcpy(buf + i, &word, min(4U, len - i));
+ }
+
+ return 0;
+}
+
+/* Preload the FIFO, start the operation, then keep the FIFO filled */
+static int zx_spifc_write_fifo(struct zx_spifc *spifc, const u8 *buf,
+ unsigned int len)
+{
+ unsigned int i = 0;
+ bool started = false;
+ u32 word;
+ int ret;
+
+ while (i < len) {
+ if (!(readl(spifc->base + SPIFC_FIFO_STATUS) &
+ SPIFC_FIFO_TX_FREE)) {
+ if (!started) {
+ zx_spifc_start(spifc);
+ started = true;
+ }
+ ret = zx_spifc_wait_fifo(spifc, SPIFC_FIFO_TX_FREE);
+ if (ret)
+ return ret;
+ }
+ word = 0;
+ memcpy(&word, buf + i, min(4U, len - i));
+ writel(word, spifc->base + SPIFC_FIFO_DATA);
+ i += 4;
+ }
+
+ if (!started)
+ zx_spifc_start(spifc);
+
+ return 0;
+}
+
+static u32 zx_spifc_lines(u8 buswidth)
+{
+ switch (buswidth) {
+ case 4:
+ return SPIFC_LINES_4;
+ case 2:
+ return SPIFC_LINES_2;
+ default:
+ return SPIFC_LINES_1;
+ }
+}
+
+/* The CRPM divider must never round above the flash's frequency limit. */
+static int zx_spifc_set_speed(struct zx_spifc *spifc, unsigned int max_hz)
+{
+ unsigned long rate;
+ long rounded;
+ int ret;
+
+ if (!max_hz)
+ return -EINVAL;
+ if (spifc->speed_hz == max_hz)
+ return 0;
+
+ rounded = clk_round_rate(spifc->wclk, max_hz);
+ if (rounded < 0)
+ return rounded;
+ if (!rounded || rounded > max_hz)
+ return -EINVAL;
+
+ ret = clk_set_rate(spifc->wclk, rounded);
+ if (ret)
+ return ret;
+ rate = clk_get_rate(spifc->wclk);
+ if (!rate || rate > max_hz)
+ return -EINVAL;
+ spifc->speed_hz = rate;
+
+ return 0;
+}
+
+static int zx_spifc_setup(struct spi_device *spi)
+{
+ struct zx_spifc *spifc = spi_controller_get_devdata(spi->controller);
+
+ return zx_spifc_set_speed(spifc, spi->max_speed_hz);
+}
+
+static int zx_spifc_exec_op(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+ struct zx_spifc *spifc = spi_controller_get_devdata(mem->spi->controller);
+ void __iomem *base = spifc->base;
+ u32 fmt0 = 0, fmt1 = 0, val;
+ int ret;
+
+ ret = zx_spifc_set_speed(spifc, op->max_freq ?: mem->spi->max_speed_hz);
+ if (ret)
+ return ret;
+
+ if (op->addr.nbytes) {
+ fmt0 |= SPIFC_FMT0_ADDR;
+ fmt1 |= FIELD_PREP(SPIFC_FMT1_ADDR_BYTES, op->addr.nbytes - 1);
+ }
+ if (op->dummy.nbytes) {
+ fmt0 |= SPIFC_FMT0_DUMMY;
+ fmt1 |= FIELD_PREP(SPIFC_FMT1_DUMMY_BYTES, op->dummy.nbytes);
+ }
+ if (op->data.nbytes) {
+ fmt0 |= op->data.dir == SPI_MEM_DATA_IN ? SPIFC_FMT0_DATA_IN :
+ SPIFC_FMT0_DATA_OUT;
+ fmt1 |= FIELD_PREP(SPIFC_FMT1_DATA_LINES,
+ zx_spifc_lines(op->data.buswidth));
+ }
+
+ zx_spifc_ack(spifc);
+ writel(op->cmd.opcode, base + SPIFC_OPCODE);
+ writel(fmt0, base + SPIFC_FMT0);
+ writel(fmt1, base + SPIFC_FMT1);
+ writel(op->data.nbytes ? op->data.nbytes - 1 : 0, base + SPIFC_LEN);
+ writel(op->addr.nbytes ? op->addr.val : 0, base + SPIFC_ADDR);
+ writel(readl(base + SPIFC_XFER_CFG) | SPIFC_XFER_CFG_BITS,
+ base + SPIFC_XFER_CFG);
+ writel(SPIFC_BURST_VAL, base + SPIFC_BURST);
+
+ if (op->data.nbytes && op->data.dir == SPI_MEM_DATA_OUT) {
+ ret = zx_spifc_write_fifo(spifc, op->data.buf.out,
+ op->data.nbytes);
+ } else {
+ zx_spifc_start(spifc);
+ ret = 0;
+ if (op->data.nbytes)
+ ret = zx_spifc_read_fifo(spifc, op->data.buf.in,
+ op->data.nbytes);
+ }
+
+ if (!ret)
+ ret = readl_poll_timeout_atomic(base + SPIFC_STATUS, val,
+ val & SPIFC_STATUS_DONE, 1,
+ SPIFC_TIMEOUT_US);
+ zx_spifc_ack(spifc);
+
+ return ret;
+}
+
+static bool zx_spifc_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 > 3 || (op->addr.nbytes && op->addr.buswidth != 1))
+ return false;
+ if (op->dummy.nbytes > 15 ||
+ (op->dummy.nbytes && op->dummy.buswidth != 1))
+ return false;
+
+ /* The vendor code programs on one or four lines, never two */
+ if (op->data.nbytes && op->data.dir == SPI_MEM_DATA_OUT &&
+ op->data.buswidth == 2)
+ return false;
+
+ return true;
+}
+
+static int zx_spifc_adjust_op_size(struct spi_mem *mem, struct spi_mem_op *op)
+{
+ op->data.nbytes = min(op->data.nbytes, SPIFC_MAX_DATA);
+
+ return 0;
+}
+
+static const struct spi_controller_mem_caps zx_spifc_mem_caps = {
+ .per_op_freq = true,
+};
+
+static const struct spi_controller_mem_ops zx_spifc_mem_ops = {
+ .adjust_op_size = zx_spifc_adjust_op_size,
+ .supports_op = zx_spifc_supports_op,
+ .exec_op = zx_spifc_exec_op,
+};
+
+static int zx_spifc_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct spi_controller *ctlr;
+ struct zx_spifc *spifc;
+ struct clk *clk;
+ long min_hz;
+ u32 val;
+
+ ctlr = devm_spi_alloc_host(dev, sizeof(*spifc));
+ if (!ctlr)
+ return -ENOMEM;
+ spifc = spi_controller_get_devdata(ctlr);
+
+ spifc->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(spifc->base))
+ return PTR_ERR(spifc->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");
+ spifc->wclk = devm_clk_get_enabled(dev, "wclk");
+ if (IS_ERR(spifc->wclk))
+ return dev_err_probe(dev, PTR_ERR(spifc->wclk),
+ "failed to get the work clock\n");
+
+ min_hz = clk_round_rate(spifc->wclk, 1);
+ if (min_hz <= 0)
+ return dev_err_probe(dev, min_hz ?: -EINVAL,
+ "invalid minimum work clock rate\n");
+
+ /* The data sampling point, as the boot loader sets it */
+ val = readl(spifc->base + SPIFC_TIMING);
+ val &= ~SPIFC_TIMING_SAMPLE;
+ val |= FIELD_PREP(SPIFC_TIMING_SAMPLE, 1);
+ writel(val, spifc->base + SPIFC_TIMING);
+
+ ctlr->mode_bits = SPI_RX_DUAL | SPI_RX_QUAD | SPI_TX_QUAD;
+ ctlr->mem_ops = &zx_spifc_mem_ops;
+ ctlr->mem_caps = &zx_spifc_mem_caps;
+ ctlr->setup = zx_spifc_setup;
+ ctlr->min_speed_hz = min_hz;
+ /* Faster rates have not been validated on this controller. */
+ ctlr->max_speed_hz = 50000000;
+ ctlr->num_chipselect = 1;
+ ctlr->dev.of_node = dev->of_node;
+
+ return devm_spi_register_controller(dev, ctlr);
+}
+
+static const struct of_device_id zx_spifc_of_match[] = {
+ { .compatible = "zte,zx279128s-spifc" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, zx_spifc_of_match);
+
+static struct platform_driver zx_spifc_driver = {
+ .probe = zx_spifc_probe,
+ .driver = {
+ .name = "zx279128s-spifc",
+ .of_match_table = zx_spifc_of_match,
+ },
+};
+module_platform_driver(zx_spifc_driver);
+
+MODULE_AUTHOR("Navid Ghahremani <ghahramani.navid@gmail.com>");
+MODULE_DESCRIPTION("ZTE zx279128s SPI flash controller driver");
+MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 21/24] dt-bindings: usb: Add ZTE ZX279128S DWC3 controller
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (19 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 20/24] " Navid Ghahremani
@ 2026-10-10 12:59 ` Navid Ghahremani
2026-10-10 13:00 ` [PATCH 22/24] usb: dwc3: generic-plat: Add ZTE ZX279128S Navid Ghahremani
` (2 subsequent siblings)
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 12:59 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe the USB host integration with three CRM clocks and four reset
bits.
Assisted-by: LLM
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..ee26915f42
--- /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.
+ 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";
+ snps,ref-clock-period-ns = <50>;
+ };
--
2.55.0
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH 22/24] usb: dwc3: generic-plat: Add ZTE ZX279128S
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (20 preceding siblings ...)
2026-10-10 12:59 ` [PATCH 21/24] dt-bindings: usb: Add ZTE ZX279128S DWC3 controller Navid Ghahremani
@ 2026-10-10 13:00 ` Navid Ghahremani
2026-10-10 13:00 ` [PATCH 23/24] ARM: dts: zte: Add ZX279128S SoC description Navid Ghahremani
2026-10-10 13:00 ` [PATCH 24/24] ARM: dts: zte: Add ZTE ZXHN H3600 board Navid Ghahremani
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 13:00 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Use the generic DWC3 platform integration to enable clocks and release
resets.
Assisted-by: LLM
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 [flat|nested] 30+ messages in thread
* [PATCH 23/24] ARM: dts: zte: Add ZX279128S SoC description
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (21 preceding siblings ...)
2026-10-10 13:00 ` [PATCH 22/24] usb: dwc3: generic-plat: Add ZTE ZX279128S Navid Ghahremani
@ 2026-10-10 13:00 ` Navid Ghahremani
2026-10-10 13:00 ` [PATCH 24/24] ARM: dts: zte: Add ZTE ZXHN H3600 board Navid Ghahremani
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 13:00 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Describe the CPUs, SRAM, timers, interrupt controller, cache, clocks,
pin mux and peripheral controllers.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
arch/arm/boot/dts/zte/zx279128s.dtsi | 397 +++++++++++++++++++++++++++
1 file changed, 397 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..e2bbc70de3
--- /dev/null
+++ b/arch/arm/boot/dts/zte/zx279128s.dtsi
@@ -0,0 +1,397 @@
+// 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/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>;
+ };
+
+ /* The boot loader's "reset" command writes 1 to CRM 1 + 0x44 */
+ reboot {
+ compatible = "syscon-reboot";
+ regmap = <&topcrm>;
+ offset = <0x44>;
+ mask = <0x1>;
+ value = <0x1>;
+ };
+
+ 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(1) | IRQ_TYPE_LEVEL_HIGH)>;
+ 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>;
+ /*
+ * A PCIe read that is still outstanding while an L2
+ * maintenance or sync operation runs hangs this SoC.
+ */
+ zte,l2c-io-read-lock;
+ };
+
+ 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_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";
+ /* the 20 MHz reference clock */
+ snps,ref-clock-period-ns = <50>;
+ 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>;
+ zte,crm = <&topcrm>, <&crm2>;
+ 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>;
+ zte,crm = <&topcrm>, <&crm2>;
+ 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";
+ reg = <0x94000000 0x1000>;
+ clocks = <&osc>;
+ clock-names = "osc";
+ #clock-cells = <1>;
+ #reset-cells = <1>;
+ };
+
+ crm2: syscon@94100000 {
+ compatible = "zte,zx279128s-crm2", "syscon";
+ reg = <0x94100000 0x1000>;
+ };
+
+ /*
+ * 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 [flat|nested] 30+ messages in thread
* [PATCH 24/24] ARM: dts: zte: Add ZTE ZXHN H3600 board
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
` (22 preceding siblings ...)
2026-10-10 13:00 ` [PATCH 23/24] ARM: dts: zte: Add ZX279128S SoC description Navid Ghahremani
@ 2026-10-10 13:00 ` Navid Ghahremani
23 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 13:00 UTC (permalink / raw)
To: linux-arm-kernel
Cc: devicetree, linux-kernel, arnd, linux, stefandoesinger, robh,
krzk, Navid Ghahremani
Enable the board ports, Wi-Fi links, USB, LEDs, buttons and generic SPI
NAND. Keep the stock firmware slot intact and UBI above both bootloader-
visible headers.
Assisted-by: LLM
Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
---
arch/arm/boot/dts/zte/Makefile | 2 +
.../boot/dts/zte/zx279128s-zte-zxhn-h3600.dts | 361 ++++++++++++++++++
2 files changed, 363 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..96d09f0296
--- /dev/null
+++ b/arch/arm/boot/dts/zte/zx279128s-zte-zxhn-h3600.dts
@@ -0,0 +1,361 @@
+// 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 {
+ function = LED_FUNCTION_WLAN_2GHZ;
+ gpios = <&gpio0 1 GPIO_ACTIVE_LOW>;
+ };
+
+ led_broadband: led-broadband {
+ 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 {
+ 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@10 {
+ reg = <10>;
+ };
+
+ phy11: ethernet-phy@11 {
+ reg = <11>;
+ };
+
+ phy12: ethernet-phy@12 {
+ reg = <12>;
+ };
+
+ phy13: ethernet-phy@13 {
+ 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 [flat|nested] 30+ messages in thread
* Re: [PATCH 05/24] ARM: zte: Add ZX279128S platform and CPU hotplug support
2026-10-10 12:59 ` [PATCH 05/24] ARM: zte: Add ZX279128S platform and CPU hotplug support Navid Ghahremani
@ 2026-10-10 14:13 ` Arnd Bergmann
2026-10-10 17:34 ` Stefan Dösinger
0 siblings, 1 reply; 30+ messages in thread
From: Arnd Bergmann @ 2026-10-10 14:13 UTC (permalink / raw)
To: Navid Ghahremani, linux-arm-kernel
Cc: devicetree, linux-kernel, Russell King, Stefan Dösinger,
Rob Herring, Krzysztof Kozlowski
On Sat, Oct 10, 2026, at 14:59, Navid Ghahremani wrote:
> Support the dual Cortex-A9 platform, second-CPU startup through
> bootloader SRAM and CPU hotplug. Keep existing ZX297520V3 support
> intact.
>
> +ARM/ZTE ZX279128S SOC SUPPORT
> +M: Navid Ghahremani <ghahramani.navid@gmail.com>
> +L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
> +S: Maintained
...
> ARM/ZTE ZX29 SOC SUPPORT
> M: Stefan Dösinger <stefandoesinger@gmail.com>
> L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
Hi Navid,
Since this is really the same platform, I would expect to have a single
maintainer or team of maintainers. Since Stefan is already established
here, I would ask you to work with him first on reviewing the code and
deciding how the two of you split the responsibilities going forward,
but I don't think that having two separate platforms with separate
maintainers is going to work.
Arnd
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH 02/24] ARM: l2c: Serialize device reads with cache maintenance when requested
2026-10-10 12:59 ` [PATCH 02/24] ARM: l2c: Serialize device reads with cache maintenance when requested Navid Ghahremani
@ 2026-10-10 14:28 ` Arnd Bergmann
2026-10-10 21:41 ` Navid Ghahremani
0 siblings, 1 reply; 30+ messages in thread
From: Arnd Bergmann @ 2026-10-10 14:28 UTC (permalink / raw)
To: Navid Ghahremani, linux-arm-kernel
Cc: devicetree, linux-kernel, Russell King, Stefan Dösinger,
Rob Herring, Krzysztof Kozlowski
On Sat, Oct 10, 2026, at 14:59, Navid Ghahremani wrote:
> Provide the optional lock used by the tested ZX279128S workaround. Lock-
> off experiments hung the board; the proposed API remains subject to
> maintainer review.
>
> Assisted-by: LLM
> Signed-off-by: Navid Ghahremani <ghahramani.navid@gmail.com>
Hi Navid,
This seems like a very invasive change and at the same time does not
look generic enough as it requires changes to each driver behind the
PCIe bus.
Please describe how you ended up with this version, did you copy
the from the vendor BSP (where?) or did you arrive here by
trial-and-error?
This looks related to the CONFIG_OUTER_CACHE_SYNC workaround,
though I can't tell whether that means it should be connected to
that, or if the actual symptom you see may be caused by that
workaround. Do you have any more information here? Does the
problem appear both with arm,outer-sync-disable enabled and
disabled?
Arnd
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH 05/24] ARM: zte: Add ZX279128S platform and CPU hotplug support
2026-10-10 14:13 ` Arnd Bergmann
@ 2026-10-10 17:34 ` Stefan Dösinger
2026-10-10 21:26 ` Navid Ghahremani
0 siblings, 1 reply; 30+ messages in thread
From: Stefan Dösinger @ 2026-10-10 17:34 UTC (permalink / raw)
To: Navid Ghahremani, linux-arm-kernel, Arnd Bergmann
Cc: devicetree, linux-kernel, Russell King, Rob Herring, Krzysztof Kozlowski
[-- Attachment #1: Type: text/plain, Size: 760 bytes --]
Hi Arnd, Navid,
Am Samstag, 10. Oktober 2026, 16:13:47 Mitteleuropäische Sommerzeit schrieb
Arnd Bergmann:
> Since this is really the same platform, I would expect to have a single
> maintainer or team of maintainers. Since Stefan is already established
> here, I would ask you to work with him first on reviewing the code and
> deciding how the two of you split the responsibilities going forward,
> but I don't think that having two separate platforms with separate
> maintainers is going to work.
I am open to co-maintainership of the ZTE zx platform, especially for things
where I don't have hardware access.
I am away from home in the next 3 weeks, but I hope to have time to review
the submission this weekend.
Cheers,
Stefan
[-- Attachment #2: This is a digitally signed message part. --]
[-- Type: application/pgp-signature, Size: 870 bytes --]
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH 05/24] ARM: zte: Add ZX279128S platform and CPU hotplug support
2026-10-10 17:34 ` Stefan Dösinger
@ 2026-10-10 21:26 ` Navid Ghahremani
0 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 21:26 UTC (permalink / raw)
To: stefandoesinger, arnd
Cc: linux-arm-kernel, devicetree, linux-kernel, linux, robh, krzk,
Navid Ghahremani
Hi Stefan and Arnd,
Thank you both for the feedback and guidance.
I would be very glad to co-maintain the ZTE ZX platform together with Stefan, and I am happy to take responsibility for testing and maintaining the ZX279128S SoC and hardware support.
In v2, I will update MAINTAINERS to combine this into a single unified ARM/ZTE ZX SOC SUPPORT entry with both of us listed as maintainers, rather than creating a separate platform entry.
Stefan, take your time with the review. I look forward to your thoughts over the weekend.
Best regards,
Navid
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH 02/24] ARM: l2c: Serialize device reads with cache maintenance when requested
2026-10-10 14:28 ` Arnd Bergmann
@ 2026-10-10 21:41 ` Navid Ghahremani
0 siblings, 0 replies; 30+ messages in thread
From: Navid Ghahremani @ 2026-10-10 21:41 UTC (permalink / raw)
To: arnd
Cc: linux-arm-kernel, devicetree, linux-kernel, stefandoesinger,
linux, Navid Ghahremani
Hi Arnd,
Thank you very much for reviewing this and pointing out arm,outer-sync-disable.
To answer your questions honestly:
1. Origin: This was developed purely through trial and error debugging on the physical ZTE ZXHN H3600 hardware. During initial bring up with the MT7915 Wifi 6 card, heavy DMA traffic caused MCU firmware timeouts and froze the PCIe bus when register reads collided with L2 maintenance. My early tests without the lock froze every time, which led me to devise this custom locking mechanism.
2. arm,outer-sync-disable: I was honestly not aware of this existing mechanism. This is my first upstream Linux SoC submission (and returning to C systems programming after many years), so I am still getting up to speed with modern kernel architecture conventions. Thank you for pointing me in the right direction and bearing with me!
3. Testing: I will test arm,outer-sync-disable on the physical router shortly and verify whether disabling outer-sync avoids the interconnect deadlock under heavy WiFi traffic. If it works, I will gladly drop patches 01, 02, and 03 completely in favor of the standard DT property.
I will report back with the hardware test results soon.
Best regards,
Navid
^ permalink raw reply [flat|nested] 30+ messages in thread
end of thread, other threads:[~2026-10-10 21:41 UTC | newest]
Thread overview: 30+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-10 12:59 [PATCH RFC 00/24] ARM: zte: Add support for Sanechips ZX279128S and ZTE ZXHN H3600 Navid Ghahremani
2026-10-10 12:59 ` [PATCH 01/24] dt-bindings: cache: l2c2x0: Describe ZTE device-read serialization Navid Ghahremani
2026-10-10 12:59 ` [PATCH 02/24] ARM: l2c: Serialize device reads with cache maintenance when requested Navid Ghahremani
2026-10-10 14:28 ` Arnd Bergmann
2026-10-10 21:41 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 03/24] wifi: mt76: Serialize MMIO reads with outer cache maintenance Navid Ghahremani
2026-10-10 12:59 ` [PATCH 04/24] dt-bindings: arm: Add ZTE ZX279128S platform descriptions Navid Ghahremani
2026-10-10 12:59 ` [PATCH 05/24] ARM: zte: Add ZX279128S platform and CPU hotplug support Navid Ghahremani
2026-10-10 14:13 ` Arnd Bergmann
2026-10-10 17:34 ` Stefan Dösinger
2026-10-10 21:26 ` Navid Ghahremani
2026-10-10 12:59 ` [PATCH 06/24] dt-bindings: clock: Add ZTE ZX279128S CRM clocks and resets Navid Ghahremani
2026-10-10 12:59 ` [PATCH 07/24] clk: zte: Add ZX279128S CRM clock and reset driver Navid Ghahremani
2026-10-10 12:59 ` [PATCH 08/24] dt-bindings: gpio: Add ZTE ZX279128S GPIO controller Navid Ghahremani
2026-10-10 12:59 ` [PATCH 09/24] gpio: Add ZTE ZX279128S GPIO driver Navid Ghahremani
2026-10-10 12:59 ` [PATCH 10/24] dt-bindings: pinctrl: pinctrl-single: Add ZTE ZX279128S pin mux Navid Ghahremani
2026-10-10 12:59 ` [PATCH 11/24] dt-bindings: mfd: syscon: Add ZX279128S system controller Navid Ghahremani
2026-10-10 12:59 ` [PATCH 12/24] dt-bindings: PCI: Add ZTE ZX279128S host controller Navid Ghahremani
2026-10-10 12:59 ` [PATCH 13/24] PCI: dwc: Add ZTE ZX279128S host controller driver Navid Ghahremani
2026-10-10 12:59 ` [PATCH 14/24] dt-bindings: net: Add ZTE ZX279128S MDIO controller Navid Ghahremani
2026-10-10 12:59 ` [PATCH 15/24] net: mdio: " Navid Ghahremani
2026-10-10 12:59 ` [PATCH 16/24] net: phy: Add Sanechips ZX5201 PHY support Navid Ghahremani
2026-10-10 12:59 ` [PATCH 17/24] dt-bindings: net: Add ZTE ZX279128S Ethernet switch Navid Ghahremani
2026-10-10 12:59 ` [PATCH 18/24] net: ethernet: zte: Add ZX279128S Ethernet switch driver Navid Ghahremani
2026-10-10 12:59 ` [PATCH 19/24] dt-bindings: spi: Add ZTE ZX279128S SPI flash controller Navid Ghahremani
2026-10-10 12:59 ` [PATCH 20/24] " Navid Ghahremani
2026-10-10 12:59 ` [PATCH 21/24] dt-bindings: usb: Add ZTE ZX279128S DWC3 controller Navid Ghahremani
2026-10-10 13:00 ` [PATCH 22/24] usb: dwc3: generic-plat: Add ZTE ZX279128S Navid Ghahremani
2026-10-10 13:00 ` [PATCH 23/24] ARM: dts: zte: Add ZX279128S SoC description Navid Ghahremani
2026-10-10 13:00 ` [PATCH 24/24] ARM: dts: zte: Add ZTE ZXHN H3600 board Navid Ghahremani
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®