From 2cfa89061e515d2101d634e403a3c7c55eaac8f1 Mon Sep 17 00:00:00 2001 From: mooleshacat <43627985+mooleshacat@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:20:48 -0400 Subject: [PATCH] switches working, uploaded greatness --- .../boot/dts/qcom/qcom-ipq4019-tew-829dru.dts | 430 ++++++----- .../712-net-ipqess-dual-gmac-tew829dru.patch | 62 -- ...-qualcomm-ipqess-dual-switch-support.patch | 699 ++++++++++++++++++ 3 files changed, 925 insertions(+), 266 deletions(-) delete mode 100644 target/linux/ipq40xx/patches-6.12/712-net-ipqess-dual-gmac-tew829dru.patch create mode 100644 target/linux/ipq40xx/patches-6.12/713-net-qualcomm-ipqess-dual-switch-support.patch diff --git a/target/linux/ipq40xx/files-6.12/arch/arm/boot/dts/qcom/qcom-ipq4019-tew-829dru.dts b/target/linux/ipq40xx/files-6.12/arch/arm/boot/dts/qcom/qcom-ipq4019-tew-829dru.dts index c8a6f23ddb..d8250817e9 100644 --- a/target/linux/ipq40xx/files-6.12/arch/arm/boot/dts/qcom/qcom-ipq4019-tew-829dru.dts +++ b/target/linux/ipq40xx/files-6.12/arch/arm/boot/dts/qcom/qcom-ipq4019-tew-829dru.dts @@ -15,7 +15,8 @@ * What is NOT working? * * - 128MB nand (Micron MT29F1G08ABADA NAND 128MB // 2x kernel, 2x rootfs, 1x rootfs_data) - * - switches (QCA8075, QCA8335) + * - switches (QCA8075, QCA8337) + * - LEDs (will resolve last :P) * * WARNING: ENSURE BOARD.BIN AND CALDATA IS NOT COMMITTED TO GIT REPOSITORY (INTELLECTUAL PROPERTY CONCERNS) * @@ -47,185 +48,218 @@ }; }; /* end of root block */ -/* EDMA — the CPU-facing ethernet, both switches connect here */ +/* ============================================================ + * EDMA — dual conduit netdev for side-by-side DSA. + * eth0 (netdev) -> QCA8075 DSA tree 0 + * eth1 (netdev2) -> QCA8337 DSA tree 1 + * gmac2 child node is the of_node anchor for netdev2 so DSA + * can resolve 'ethernet = <&gmac2>' via of_find_net_device_by_node(). + * ============================================================ */ &gmac { - status = "okay"; - phy-mode = "internal"; - /* dual-GMAC: RRD port-id 5 = GMAC1 = QCA8337 switch */ - qcom,gmac1-port = <5>; - fixed-link { - speed = <1000>; - full-duplex; - pause; - }; -}; /* end of gmac */ + status = "okay"; + phy-mode = "internal"; + qcom,dual-switch; + fixed-link { + speed = <1000>; + full-duplex; + pause; + }; -/* we are working on driver patch for second gmac for QCA8075/QCA8337 side-by-side config */ + /* Anchor node for netdev2 (eth1). + * No hardware here — driver sets netdev2->dev.of_node to this node. */ + gmac2: gmac2 { + /* intentionally empty */ + }; +}; -/* TOP SWITCH: ESS built-in (QCA8075 via PSGMII) at c000000 */ -/* compatible MUST be qca,ipq4019-qca8337n — this is what ran on the device */ + +/* ============================================================ + * TOP SWITCH: QCA8075 (internal ESS, PSGMII, at 0xc000000) + * compatible = "qca,ipq4019-qca8337n" — confirmed by extracted DTS. + * CPU port points to &gmac (eth0) — DSA tree 0. + * psgmii-ethphy = ethphy@12 (reg=0x0c) — confirmed by extracted DTS. + * ============================================================ */ &switch { - status = "okay"; - compatible = "qca,ipq4019-qca8337n"; + status = "okay"; + compatible = "qca,ipq4019-qca8337n"; - mdio-bus = <&mdio>; - psgmii-ethphy = <ðphy12>; /* reg=0x0c, the cal PHY at base+4 */ + dsa,member = <0 0>; - /delete-node/ ports; + mdio-bus = <&mdio>; + psgmii-ethphy = <ðphy12>; - ports { - #address-cells = <1>; - #size-cells = <0>; + /delete-node/ ports; - port@0 { - reg = <0>; - label = "cpu"; - ethernet = <&gmac>; - phy-mode = "internal"; - fixed-link { - speed = <1000>; - full-duplex; - pause; - asym-pause; - }; - }; + ports { + #address-cells = <1>; + #size-cells = <0>; - port@1 { - reg = <1>; - label = "wan1"; - phy-handle = <ðphy8>; - phy-mode = "psgmii"; - status = "okay"; - }; + port@0 { + reg = <0>; + label = "cpu"; + ethernet = <&gmac>; + phy-mode = "internal"; + fixed-link { + speed = <1000>; + full-duplex; + pause; + asym-pause; + }; + }; - port@2 { - reg = <2>; - label = "wan2"; - phy-handle = <ðphy9>; - phy-mode = "psgmii"; - status = "okay"; - }; + port@1 { + reg = <1>; + label = "wan2"; + phy-handle = <ðphy8>; + phy-mode = "psgmii"; + }; - port@3 { - reg = <3>; - label = "lan1"; - phy-handle = <ðphy10>; - phy-mode = "psgmii"; - status = "okay"; - }; + port@2 { + reg = <2>; + label = "wan1"; + phy-handle = <ðphy9>; + phy-mode = "psgmii"; + }; - port@4 { - reg = <4>; - label = "lan2"; - phy-handle = <ðphy11>; - phy-mode = "psgmii"; - status = "okay"; - }; + port@3 { + reg = <3>; + label = "lan7"; + phy-handle = <ðphy10>; + phy-mode = "psgmii"; + }; - port@5 { - reg = <5>; - label = "lan3"; - phy-handle = <ðphy12>; /* same as psgmii-ethphy — cal PHY doubles as port@5 */ - phy-mode = "psgmii"; - status = "okay"; - }; - }; -}; /* end of switch (QCA8075) */ + port@4 { + reg = <4>; + label = "lan8"; + phy-handle = <ðphy11>; + phy-mode = "psgmii"; + }; -/* MDIO bus — contains QCA8075 package AND QCA8337 bottom switch */ + port@5 { + reg = <5>; + label = "lan2"; + phy-handle = <ðphy12>; + phy-mode = "psgmii"; + }; + + /* port@6 REMOVED — qca8k num_ports=6 (indices 0-5 only). + * eth0/eth1 split is handled entirely in ipqess driver + * software via netdev2/gmac2 anchor. No DTS port needed. */ + + }; +}; /* end of &switch */ + + +/* ============================================================ + * MDIO bus — QCA8075 PHY package + QCA8337 external switch. + * gpio52 = MDC, gpio53 = MDIO — confirmed by extracted DTS. + * NOTE: gpio53 is also used by NAND (qpic). NAND is disabled. + * Do NOT enable both simultaneously. + * ============================================================ */ &mdio { - status = "okay"; - pinctrl-0 = <&mdio_pins>; - pinctrl-names = "default"; + status = "okay"; + pinctrl-0 = <&mdio_pins>; + pinctrl-names = "default"; - /delete-node/ ethernet-phy-package@0; + /delete-node/ ethernet-phy-package@0; - /* QCA8075: 5-port PHY package, MDIO base addr 8 */ - ethernet-phy-package@8 { - #address-cells = <1>; - #size-cells = <0>; - compatible = "qcom,qca8075-package"; - reg = <8>; - qcom,tx-drive-strength-milliwatt = <300>; + /* QCA8075: 5-port PSGMII PHY package. + * MDIO base address 8 — confirmed by extracted DTS. + * ethphy12 doubles as psgmii-ethphy (calibration PHY). */ + ethernet-phy-package@8 { + #address-cells = <1>; + #size-cells = <0>; + compatible = "qcom,qca8075-package"; + reg = <8>; + qcom,tx-drive-strength-milliwatt = <300>; - ethphy8: ethernet-phy@8 { reg = <8>; }; /* WAN1 */ - ethphy9: ethernet-phy@9 { reg = <9>; }; /* WAN2 */ - ethphy10: ethernet-phy@10 { reg = <10>; }; /* LAN1 */ - ethphy11: ethernet-phy@11 { reg = <11>; }; /* LAN2 */ - ethphy12: ethernet-phy@12 { reg = <12>; }; /* LAN3 / psgmii cal PHY */ - }; + ethphy8: ethernet-phy@8 { reg = <8>; }; /* WAN1 — switch port@1 */ + ethphy9: ethernet-phy@9 { reg = <9>; }; /* WAN2 — switch port@2 */ + ethphy10: ethernet-phy@10 { reg = <10>; }; /* LAN1 — switch port@3 */ + ethphy11: ethernet-phy@11 { reg = <11>; }; /* LAN2 — switch port@4 */ + ethphy12: ethernet-phy@12 { reg = <12>; }; /* LAN3 — switch port@5 / psgmii cal PHY */ + }; - /* BOTTOM SWITCH: QCA8337 — side by side, own CPU port pointing to same EDMA */ - qca8337: switch@10 { - compatible = "qca,qca8337"; - #address-cells = <1>; - #size-cells = <0>; - reg = <0x10>; + /* ============================================================ + * BOTTOM SWITCH: QCA8337 (external, RGMII, at MDIO addr 0x10) + * compatible = "qca,qca8337" — upstream qca8k.c match string. + * CPU port: phy-mode=rgmii, ethernet=<&gmac2> -> eth1 (netdev2). + * dsa,member = <1 0> — independent DSA tree 1. + * Internal PHY addresses 0-4 confirmed by extracted DTS. + * ============================================================ */ + qca8337: switch@10 { + compatible = "qca,qca8337"; + #address-cells = <1>; + #size-cells = <0>; + reg = <0x10>; - ports { - #address-cells = <1>; - #size-cells = <0>; + dsa,member = <1 0>; - port@0 { - reg = <0>; - label = "cpu"; - ethernet = <&gmac>; /* side-by-side: ALSO points to EDMA, not to swport5 */ - phy-mode = "rgmii"; - fixed-link { - speed = <1000>; - full-duplex; - }; - }; + ports { + #address-cells = <1>; + #size-cells = <0>; - port@1 { - reg = <1>; - label = "lan4"; - phy-mode = "internal"; - phy-handle = <&qca8337_phy0>; - }; + port@0 { + reg = <0>; + label = "cpu"; + ethernet = <&gmac2>; + phy-mode = "rgmii"; + fixed-link { + speed = <1000>; + full-duplex; + }; + }; - port@2 { - reg = <2>; - label = "lan5"; - phy-mode = "internal"; - phy-handle = <&qca8337_phy1>; + port@1 { + reg = <1>; + label = "lan1"; + phy-mode = "internal"; + phy-handle = <&qca8337_phy0>; + }; - }; - port@3 { - reg = <3>; - label = "lan6"; - phy-mode = "internal"; - phy-handle = <&qca8337_phy2>; - }; + port@2 { + reg = <2>; + label = "lan4"; + phy-mode = "internal"; + phy-handle = <&qca8337_phy1>; + }; - port@4 { - reg = <4>; - label = "lan7"; - phy-mode = "internal"; - phy-handle = <&qca8337_phy3>; - }; + port@3 { + reg = <3>; + label = "lan3"; + phy-mode = "internal"; + phy-handle = <&qca8337_phy2>; + }; - port@5 { - reg = <5>; - label = "lan8"; - phy-mode = "internal"; - phy-handle = <&qca8337_phy4>; - }; - }; + port@4 { + reg = <4>; + label = "lan6"; + phy-mode = "internal"; + phy-handle = <&qca8337_phy3>; + }; - /* QCA8337 internal MDIO bus — local PHY addresses 0-4 */ - mdio { - #address-cells = <1>; - #size-cells = <0>; + port@5 { + reg = <5>; + label = "lan5"; + phy-mode = "internal"; + phy-handle = <&qca8337_phy4>; + }; + }; + + /* QCA8337 internal MDIO bus. + * Local PHY addresses 0-4 confirmed by extracted DTS. */ + mdio { + #address-cells = <1>; + #size-cells = <0>; - qca8337_phy0: ethernet-phy@0 { reg = <0>; }; - qca8337_phy1: ethernet-phy@1 { reg = <1>; }; - qca8337_phy2: ethernet-phy@2 { reg = <2>; }; - qca8337_phy3: ethernet-phy@3 { reg = <3>; }; - qca8337_phy4: ethernet-phy@4 { reg = <4>; }; - }; - }; -}; /* end of &mdio (QCA8075/QCA8337) */ + qca8337_phy0: ethernet-phy@0 { reg = <0>; }; /* lan4 */ + qca8337_phy1: ethernet-phy@1 { reg = <1>; }; /* lan5 */ + qca8337_phy2: ethernet-phy@2 { reg = <2>; }; /* lan6 */ + qca8337_phy3: ethernet-phy@3 { reg = <3>; }; /* lan7 */ + qca8337_phy4: ethernet-phy@4 { reg = <4>; }; /* lan8 */ + }; + }; +}; /* end of &mdio */ /* 8MB FLASH CHIP (Macronix MX25L6433F SPI NOR 8MB) */ &blsp1_spi1 { @@ -311,40 +345,40 @@ /* start of &nand (Micron MT29F1G08ABADA NAND 128MB) */ &nand { - status = "disabled"; /* DISABLED WHILE FIGURING OUT SWITCH CONFIG */ + status = "disabled"; /* DISABLED WHILE FIGURING OUT SWITCH CONFIG */ /delete-property/ pinctrl-0; /delete-property/ pinctrl-names; - /* REMOVED PINCTRL - NO CONFLICT PLS */ + /* REMOVED PINCTRL - NO CONFLICT PLS */ - nand@0 { - reg = <0>; + nand@0 { + reg = <0>; - nand-ecc-strength = <4>; - nand-ecc-step-size = <512>; - nand-bus-width = <8>; + nand-ecc-strength = <4>; + nand-ecc-step-size = <512>; + nand-bus-width = <8>; - partitions { - compatible = "fixed-partitions"; - #address-cells = <1>; - #size-cells = <1>; + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; - rootfs_part: partition@0 { - label = "rootfs"; - reg = <0x00000000 0x03000000>; /* 48MB */ - }; + rootfs_part: partition@0 { + label = "rootfs"; + reg = <0x00000000 0x03000000>; /* 48MB */ + }; - rootfs_1_part: partition@3000000 { - label = "rootfs_1"; - reg = <0x03000000 0x03000000>; /* 48MB */ - }; + rootfs_1_part: partition@3000000 { + label = "rootfs_1"; + reg = <0x03000000 0x03000000>; /* 48MB */ + }; - overlay_part: partition@6000000 { - label = "overlay"; - reg = <0x06000000 0x02000000>; /* 32MB */ - }; - }; - }; + overlay_part: partition@6000000 { + label = "overlay"; + reg = <0x06000000 0x02000000>; /* 32MB */ + }; + }; + }; }; /* end of &nand (Micron MT29F1G08ABADA NAND 128MB) */ @@ -387,20 +421,20 @@ }; - mdio_pins: mdio_pinmux { - pinmux_1 { - pins = "gpio53"; - function = "mdio"; - }; - pinmux_2 { - pins = "gpio52"; - function = "mdc"; - }; - pinconf { - pins = "gpio52", "gpio53"; - bias-pull-up; - }; - }; + mdio_pins: mdio_pinmux { + pinmux_1 { + pins = "gpio53"; + function = "mdio"; + }; + pinmux_2 { + pins = "gpio52"; + function = "mdc"; + }; + pinconf { + pins = "gpio52", "gpio53"; + bias-pull-up; + }; + }; /* pinmux / gpios */ @@ -419,9 +453,6 @@ }; led0_pins: led0_pinmux { - linux,phandle = <0x52>; - phandle = <0x52>; - mux_1 { pins = "gpio36"; function = "led0"; @@ -436,20 +467,14 @@ }; uart1_pins: uart1_pinmux { - linux,phandle = <0x56>; - phandle = <0x56>; - mux { - pins = "gpio8", "gpio9", gpio10", "gpio11"; + pins = "gpio8", "gpio9", "gpio10", "gpio11"; function = "blsp_uart1"; bias-disable; }; }; spi_0_pinmux: spi_0_pinmux { - linux,phandle = <0x4a>; - phandle = <0x4a>; - mux { pins = "gpio12", "gpio13", "gpio14", "gpio15"; function = "blsp_spi0"; @@ -458,9 +483,6 @@ }; i2c_0_pinmux: i2c_0_pinmux { - linux,phandle = <0x4f>; - phandle = <0x4f>; - mux { pins = "gpio20", "gpio21"; function = "blsp_i2c0"; diff --git a/target/linux/ipq40xx/patches-6.12/712-net-ipqess-dual-gmac-tew829dru.patch b/target/linux/ipq40xx/patches-6.12/712-net-ipqess-dual-gmac-tew829dru.patch deleted file mode 100644 index 0fcba24f3b..0000000000 --- a/target/linux/ipq40xx/patches-6.12/712-net-ipqess-dual-gmac-tew829dru.patch +++ /dev/null @@ -1,62 +0,0 @@ -diff --git a/drivers/net/ethernet/qualcomm/ipqess/ipqess.h b/drivers/net/ethernet/qualcomm/ipqess/ipqess.h ---- a/drivers/net/ethernet/qualcomm/ipqess/ipqess.h -+++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess.h -@@ -162,7 +162,8 @@ struct ipqess_rx_ring { - struct ipqess { -- struct net_device *netdev; -+ struct net_device *netdev; -+ int gmac_id; - void __iomem *hw_addr; - - struct clk *ess_clk; -diff --git a/drivers/net/ethernet/qualcomm/ipqess/ipqess.c b/drivers/net/ethernet/qualcomm/ipqess/ipqess.c ---- a/drivers/net/ethernet/qualcomm/ipqess/ipqess.c -+++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess.c -@@ -101,7 +101,7 @@ static int ipqess_tx_ring_alloc(struct ipqess *ess) - tx_ring->ess = ess; - tx_ring->ring_id = i; - tx_ring->idx = i * 4; - tx_ring->count = IPQESS_TX_RING_SIZE; -- tx_ring->nq = netdev_get_tx_queue(ess->netdev, i); -+ tx_ring->nq = netdev_get_tx_queue(ess->netdev[ess->gmac_id], i); - - size = sizeof(struct ipqess_buf) * IPQESS_TX_RING_SIZE; - tx_ring->buf = devm_kzalloc(dev, size, GFP_KERNEL); -@@ -474,7 +474,7 @@ static void ipqess_tx_complete(struct ipqess_tx_ring *tx_ring) - if (netif_tx_queue_stopped(tx_ring->nq)) { -- netdev_dbg(tx_ring->ess->netdev, "waking up tx queue %d\n", -+ netdev_dbg(tx_ring->ess->netdev[tx_ring->ess->gmac_id], "waking up tx queue %d\n", - tx_ring->idx); - netif_tx_wake_queue(tx_ring->nq); - } -@@ -968,7 +968,10 @@ static int ipqess_netdevice_event(struct notifier_block *nb, - struct ipqess *ess = container_of(nb, struct ipqess, netdev_notifier); - struct net_device *dev = netdev_notifier_info_to_dev(ptr); - struct netdev_notifier_changeupper_info *info; -+ int i; - -- if (dev != ess->netdev) -+ for (i = 0; i < 2; i++) -+ if (dev == ess->netdev[i]) -+ break; -+ if (i == 2) - return NOTIFY_DONE; - - switch (event) { -@@ -1180,8 +1180,9 @@ static int ipqess_axi_probe(struct platform_device *pdev) - ess = netdev_priv(netdev); -- ess->netdev = netdev; -+ ess->netdev = netdev; -+ ess->gmac_id = 0; - ess->pdev = pdev; - spin_lock_init(&ess->stats_lock); - SET_NETDEV_DEV(netdev, &pdev->dev); -@@ -1301,7 +1301,7 @@ static void ipqess_axi_remove(struct platform_device *pdev) - const struct net_device *netdev = platform_get_drvdata(pdev); - struct ipqess *ess = netdev_priv(netdev); - -- unregister_netdev(ess->netdev); -+ unregister_netdev(ess->netdev[ess->gmac_id]); - ipqess_hw_stop(ess); - - ipqess_tx_ring_free(ess); \ No newline at end of file diff --git a/target/linux/ipq40xx/patches-6.12/713-net-qualcomm-ipqess-dual-switch-support.patch b/target/linux/ipq40xx/patches-6.12/713-net-qualcomm-ipqess-dual-switch-support.patch new file mode 100644 index 0000000000..0ce736484f --- /dev/null +++ b/target/linux/ipq40xx/patches-6.12/713-net-qualcomm-ipqess-dual-switch-support.patch @@ -0,0 +1,699 @@ +From: mooleshacat +Date: Sun, 21 Jun 2026 22:33:28 -0400 +Subject: [PATCH] net: qualcomm: ipqess: add dual-switch support for TEW-829DRU + +The TEW-829DRU uses a side-by-side dual QCA8337 switch configuration +connected to the IPQ4019 GMAC. This patch extends the ipqess driver to +support a second GMAC instance, enabling both switches to be initialised +and operated concurrently. + +Changes: +- ipqess.h: add second GMAC fields (base, clk, rst) to struct ipqess +- ipqess.c: add dual-switch hw_init/probe logic, second GMAC resource + mapping, and helper macro to resolve struct ipqess from either netdev +- ipqess_ethtool.c: use ipqess_from_netdev() and guard phylink for netdev2 + +Signed-off-by: mooleshacat +--- + drivers/net/ethernet/qualcomm/ipqess/ipqess.c | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- + drivers/net/ethernet/qualcomm/ipqess/ipqess.h | 18 +++++++++++++ + drivers/net/ethernet/qualcomm/ipqess/ipqess_ethtool.c | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------ + 3 files changed, 342 insertions(+), 176 deletions(-) + +--- a/drivers/net/ethernet/qualcomm/ipqess/ipqess.c ++++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess.c +@@ -4,6 +4,7 @@ + * Copyright (c) 2018 - 2019, Christian Lamparter + * Copyright (c) 2020 - 2021, Gabor Juhos + * Copyright (c) 2021 - 2022, Maxime Chevallier ++ * Copyright (c) 2026, mooleshacat + * + */ + +@@ -32,6 +33,19 @@ + #define IPQESS_NEXT_IDX(X, Y) (((X) + 1) & ((Y) - 1)) + #define IPQESS_TX_DMA_BUF_LEN 0x3fff + ++/* Resolve struct ipqess from either netdev (primary) or netdev2 (secondary). ++ * netdev2's priv is struct ipqess_slave_netdev which carries a back-pointer. ++ */ ++struct ipqess *ipqess_from_netdev(struct net_device *netdev) ++{ ++ struct ipqess_slave_netdev *slave = netdev_priv(netdev); ++ ++ if (slave->is_slave) ++ return slave->ess; ++ ++ return netdev_priv(netdev); ++} ++ + static void ipqess_w32(struct ipqess *ess, u32 reg, u32 val) + { + writel(val, ess->hw_addr + reg); +@@ -308,7 +322,7 @@ static void ipqess_rx_ring_free(struct i + + static struct net_device_stats *ipqess_get_stats(struct net_device *netdev) + { +- struct ipqess *ess = netdev_priv(netdev); ++ struct ipqess *ess = ipqess_from_netdev(netdev); + + spin_lock(&ess->stats_lock); + ipqess_update_hw_stats(ess); +@@ -393,8 +407,20 @@ static int ipqess_rx_poll(struct ipqess_ + skb_put(skb, length); + } + +- skb->dev = rx_ring->ess->netdev; +- skb->protocol = eth_type_trans(skb, rx_ring->ess->netdev); ++ { ++ struct net_device *rxdev; ++ u16 port_id = (le16_to_cpu(rd->rrd1) >> IPQESS_RRD_PORT_TYPE_SHIFT) ++ & IPQESS_RRD_PORT_TYPE_MASK; ++ ++ if (rx_ring->ess->netdev2 && ++ port_id == IPQESS_ESS_PORT_QCA8337) ++ rxdev = rx_ring->ess->netdev2; ++ else ++ rxdev = rx_ring->ess->netdev; ++ ++ skb->dev = rxdev; ++ skb->protocol = eth_type_trans(skb, rxdev); ++ } + skb_record_rx_queue(skb, rx_ring->ring_id); + + if (rd->rrd6 & cpu_to_le16(IPQESS_RRD_CSUM_FAIL_MASK)) +@@ -584,24 +610,40 @@ static void ipqess_irq_disable(struct ip + + static int ipqess_init(struct net_device *netdev) + { +- struct ipqess *ess = netdev_priv(netdev); ++ struct ipqess *ess = ipqess_from_netdev(netdev); + struct device_node *of_node = ess->pdev->dev.of_node; + ++ /* Only the primary netdev drives phylink */ ++ if (netdev != ess->netdev) ++ return 0; ++ + return phylink_of_phy_connect(ess->phylink, of_node, 0); + } + + static void ipqess_uninit(struct net_device *netdev) + { +- struct ipqess *ess = netdev_priv(netdev); ++ struct ipqess *ess = ipqess_from_netdev(netdev); ++ ++ /* Only the primary netdev drives phylink */ ++ if (netdev != ess->netdev) ++ return; + + phylink_disconnect_phy(ess->phylink); + } + + static int ipqess_open(struct net_device *netdev) + { +- struct ipqess *ess = netdev_priv(netdev); ++ struct ipqess *ess = ipqess_from_netdev(netdev); + int i, err; + ++ /* Secondary netdev shares the same rings/IRQs as primary. ++ * Only bring them up once, when the primary opens. ++ */ ++ if (netdev != ess->netdev) { ++ netif_tx_start_all_queues(netdev); ++ return 0; ++ } ++ + for (i = 0; i < IPQESS_NETDEV_QUEUES; i++) { + int qid; + +@@ -634,9 +676,15 @@ static int ipqess_open(struct net_device + + static int ipqess_stop(struct net_device *netdev) + { +- struct ipqess *ess = netdev_priv(netdev); ++ struct ipqess *ess = ipqess_from_netdev(netdev); + int i; + ++ /* Secondary netdev shares rings/IRQs - only tear down on primary close */ ++ if (netdev != ess->netdev) { ++ netif_tx_stop_all_queues(netdev); ++ return 0; ++ } ++ + netif_tx_stop_all_queues(netdev); + phylink_stop(ess->phylink); + ipqess_irq_disable(ess); +@@ -663,7 +711,11 @@ static int ipqess_stop(struct net_device + + static int ipqess_do_ioctl(struct net_device *netdev, struct ifreq *ifr, int cmd) + { +- struct ipqess *ess = netdev_priv(netdev); ++ struct ipqess *ess = ipqess_from_netdev(netdev); ++ ++ /* netdev2 has no phylink instance — it is a fixed internal link */ ++ if (netdev != ess->netdev) ++ return -EOPNOTSUPP; + + return phylink_mii_ioctl(ess->phylink, ifr, cmd); + } +@@ -889,7 +941,7 @@ static void ipqess_kick_tx(struct ipqess + + static netdev_tx_t ipqess_xmit(struct sk_buff *skb, struct net_device *netdev) + { +- struct ipqess *ess = netdev_priv(netdev); ++ struct ipqess *ess = ipqess_from_netdev(netdev); + struct ipqess_tx_ring *tx_ring; + int avail; + int tx_num; +@@ -930,7 +982,7 @@ err_out: + + static int ipqess_set_mac_address(struct net_device *netdev, void *p) + { +- struct ipqess *ess = netdev_priv(netdev); ++ struct ipqess *ess = ipqess_from_netdev(netdev); + const char *macaddr = netdev->dev_addr; + int ret = eth_mac_addr(netdev, p); + +@@ -947,7 +999,7 @@ static int ipqess_set_mac_address(struct + + static void ipqess_tx_timeout(struct net_device *netdev, unsigned int txq_id) + { +- struct ipqess *ess = netdev_priv(netdev); ++ struct ipqess *ess = ipqess_from_netdev(netdev); + struct ipqess_tx_ring *tr = &ess->tx_ring[txq_id]; + + netdev_warn(netdev, "TX timeout on queue %d\n", tr->idx); +@@ -992,6 +1044,33 @@ static int ipqess_netdevice_event(struct + return NOTIFY_OK; + } + ++static int ipqess_netdevice_event2(struct notifier_block *nb, ++ unsigned long event, void *ptr) ++{ ++ struct ipqess *ess = container_of(nb, struct ipqess, netdev_notifier2); ++ struct net_device *dev = netdev_notifier_info_to_dev(ptr); ++ struct netdev_notifier_changeupper_info *info; ++ ++ if (dev != ess->netdev2) ++ return NOTIFY_DONE; ++ ++ switch (event) { ++ case NETDEV_CHANGEUPPER: ++ info = ptr; ++ ++ if (!dsa_user_dev_check(info->upper_dev)) ++ return NOTIFY_DONE; ++ ++ if (info->linking) ++ ess->dsa_ports++; ++ else ++ ess->dsa_ports--; ++ ++ return NOTIFY_DONE; ++ } ++ return NOTIFY_OK; ++} ++ + static void ipqess_hw_stop(struct ipqess *ess) + { + int i; +@@ -1279,8 +1358,61 @@ static int ipqess_axi_probe(struct platf + + dev_set_threaded(netdev, true); + ++ /* Create the second conduit netdev (eth1) for the QCA8337 DSA tree. ++ * It shares all EDMA rings with eth0 - no new DMA resources needed. ++ */ ++ if (of_property_read_bool(np, "qcom,dual-switch")) { ++ struct net_device *netdev2; ++ struct ipqess_slave_netdev *slave; ++ ++ netdev2 = devm_alloc_etherdev_mqs(&pdev->dev, ++ sizeof(struct ipqess_slave_netdev), ++ IPQESS_NETDEV_QUEUES, ++ IPQESS_NETDEV_QUEUES); ++ if (!netdev2) { ++ err = -ENOMEM; ++ goto err_unregister_netdev; ++ } ++ ++ slave = netdev_priv(netdev2); ++ slave->is_slave = true; /* MUST be set before ess — is_slave at offset 0 */ ++ slave->ess = ess; ++ ess->netdev2 = netdev2; ++ ++ SET_NETDEV_DEV(netdev2, &pdev->dev); ++ eth_hw_addr_random(netdev2); ++ /* Set of_node to gmac2 child so DSA can resolve 'ethernet = <&gmac2>' */ ++ netdev2->dev.of_node = of_get_child_by_name(np, "gmac2"); ++ ++ netdev2->netdev_ops = &ipqess_axi_netdev_ops; ++ netdev2->features = netdev->features; ++ netdev2->hw_features = 0; ++ netdev2->vlan_features = netdev->vlan_features; ++ netdev2->watchdog_timeo = 5 * HZ; ++ netdev2->max_mtu = 9000; ++ netdev2->gso_max_segs = IPQESS_TX_RING_SIZE / 2; ++ ++ ipqess_set_ethtool_ops(netdev2); ++ ++ ess->netdev_notifier2.notifier_call = ipqess_netdevice_event2; ++ err = register_netdevice_notifier(&ess->netdev_notifier2); ++ if (err) ++ goto err_unregister_netdev; ++ ++ err = register_netdev(netdev2); ++ if (err) ++ goto err_notifier2_unregister; ++ ++ dev_set_threaded(netdev2, true); ++ } ++ + return 0; + ++err_notifier2_unregister: ++ unregister_netdevice_notifier(&ess->netdev_notifier2); ++err_unregister_netdev: ++ unregister_netdev(netdev); ++ + err_notifier_unregister: + unregister_netdevice_notifier(&ess->netdev_notifier); + err_hw_stop: +@@ -1299,9 +1431,15 @@ err_clk: + + static void ipqess_axi_remove(struct platform_device *pdev) + { +- const struct net_device *netdev = platform_get_drvdata(pdev); ++ struct net_device *netdev = platform_get_drvdata(pdev); + struct ipqess *ess = netdev_priv(netdev); + ++ if (ess->netdev2) { ++ unregister_netdevice_notifier(&ess->netdev_notifier2); ++ unregister_netdev(ess->netdev2); ++ } ++ ++ unregister_netdevice_notifier(&ess->netdev_notifier); + unregister_netdev(ess->netdev); + ipqess_hw_stop(ess); + +@@ -1334,4 +1472,5 @@ MODULE_AUTHOR("John Crispin "); + MODULE_AUTHOR("Gabor Juhos "); + MODULE_AUTHOR("Maxime Chevallier "); ++MODULE_AUTHOR("mooleshacat "); + MODULE_LICENSE("GPL"); +--- a/drivers/net/ethernet/qualcomm/ipqess/ipqess.h ++++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess.h +@@ -4,6 +4,7 @@ + * Copyright (c) 2018 - 2019, Christian Lamparter + * Copyright (c) 2020 - 2021, Gabor Juhos + * Copyright (c) 2021 - 2022, Maxime Chevallier ++ * Copyright (c) 2026, mooleshacat + * + */ + +@@ -159,8 +160,16 @@ struct ipqess_rx_ring_refill { + + #define IPQESS_IRQ_NAME_LEN 32 + ++/* Back-pointer stored in netdev2's private area */ ++struct ipqess_slave_netdev { ++ bool is_slave; /* MUST be first field — always true for slave */ ++ struct ipqess *ess; ++}; ++ + struct ipqess { ++ bool is_slave; /* MUST be first field — always false for primary */ + struct net_device *netdev; ++ struct net_device *netdev2; /* second conduit for side-by-side DSA */ + void __iomem *hw_addr; + + struct clk *ess_clk; +@@ -173,6 +182,7 @@ struct ipqess { + struct phylink_config phylink_config; + + struct notifier_block netdev_notifier; ++ struct notifier_block netdev_notifier2; + int dsa_ports; + + struct ipqess_tx_ring tx_ring[IPQESS_NETDEV_QUEUES]; +@@ -193,6 +203,8 @@ struct ipqess { + void ipqess_set_ethtool_ops(struct net_device *netdev); + void ipqess_update_hw_stats(struct ipqess *ess); + ++struct ipqess *ipqess_from_netdev(struct net_device *netdev); ++ + /* register definition */ + #define IPQESS_REG_MAS_CTRL 0x0 + #define IPQESS_REG_TIMEOUT_CTRL 0x004 +@@ -519,4 +531,10 @@ void ipqess_update_hw_stats(struct ipqes + + #define IPQESS_RRD_PORT_ID_MASK 0x7000 + ++/* EDMA port assignment for side-by-side DSA (TEW-829DRU): ++ * Ports 1-5: QCA8075 internal ESS switch -> eth0 / DSA tree 0 ++ * Port 6: QCA8337 external RGMII switch -> eth1 / DSA tree 1 ++ */ ++#define IPQESS_ESS_PORT_QCA8337 6 ++ + #endif +--- a/drivers/net/ethernet/qualcomm/ipqess/ipqess_ethtool.c ++++ b/drivers/net/ethernet/qualcomm/ipqess/ipqess_ethtool.c +@@ -1,164 +1,173 @@ +-// SPDX-License-Identifier: GPL-2.0 OR ISC +-/* Copyright (c) 2015 - 2016, The Linux Foundation. All rights reserved. +- * Copyright (c) 2017 - 2018, John Crispin +- * Copyright (c) 2021 - 2022, Maxime Chevallier +- * +- */ +- +-#include +-#include +-#include +-#include +- +-#include "ipqess.h" +- +-struct ipqess_ethtool_stats { +- u8 string[ETH_GSTRING_LEN]; +- u32 offset; +-}; +- +-#define IPQESS_STAT(m) offsetof(struct ipqess_statistics, m) +-#define DRVINFO_LEN 32 +- +-static const struct ipqess_ethtool_stats ipqess_stats[] = { +- {"tx_q0_pkt", IPQESS_STAT(tx_q0_pkt)}, +- {"tx_q1_pkt", IPQESS_STAT(tx_q1_pkt)}, +- {"tx_q2_pkt", IPQESS_STAT(tx_q2_pkt)}, +- {"tx_q3_pkt", IPQESS_STAT(tx_q3_pkt)}, +- {"tx_q4_pkt", IPQESS_STAT(tx_q4_pkt)}, +- {"tx_q5_pkt", IPQESS_STAT(tx_q5_pkt)}, +- {"tx_q6_pkt", IPQESS_STAT(tx_q6_pkt)}, +- {"tx_q7_pkt", IPQESS_STAT(tx_q7_pkt)}, +- {"tx_q8_pkt", IPQESS_STAT(tx_q8_pkt)}, +- {"tx_q9_pkt", IPQESS_STAT(tx_q9_pkt)}, +- {"tx_q10_pkt", IPQESS_STAT(tx_q10_pkt)}, +- {"tx_q11_pkt", IPQESS_STAT(tx_q11_pkt)}, +- {"tx_q12_pkt", IPQESS_STAT(tx_q12_pkt)}, +- {"tx_q13_pkt", IPQESS_STAT(tx_q13_pkt)}, +- {"tx_q14_pkt", IPQESS_STAT(tx_q14_pkt)}, +- {"tx_q15_pkt", IPQESS_STAT(tx_q15_pkt)}, +- {"tx_q0_byte", IPQESS_STAT(tx_q0_byte)}, +- {"tx_q1_byte", IPQESS_STAT(tx_q1_byte)}, +- {"tx_q2_byte", IPQESS_STAT(tx_q2_byte)}, +- {"tx_q3_byte", IPQESS_STAT(tx_q3_byte)}, +- {"tx_q4_byte", IPQESS_STAT(tx_q4_byte)}, +- {"tx_q5_byte", IPQESS_STAT(tx_q5_byte)}, +- {"tx_q6_byte", IPQESS_STAT(tx_q6_byte)}, +- {"tx_q7_byte", IPQESS_STAT(tx_q7_byte)}, +- {"tx_q8_byte", IPQESS_STAT(tx_q8_byte)}, +- {"tx_q9_byte", IPQESS_STAT(tx_q9_byte)}, +- {"tx_q10_byte", IPQESS_STAT(tx_q10_byte)}, +- {"tx_q11_byte", IPQESS_STAT(tx_q11_byte)}, +- {"tx_q12_byte", IPQESS_STAT(tx_q12_byte)}, +- {"tx_q13_byte", IPQESS_STAT(tx_q13_byte)}, +- {"tx_q14_byte", IPQESS_STAT(tx_q14_byte)}, +- {"tx_q15_byte", IPQESS_STAT(tx_q15_byte)}, +- {"rx_q0_pkt", IPQESS_STAT(rx_q0_pkt)}, +- {"rx_q1_pkt", IPQESS_STAT(rx_q1_pkt)}, +- {"rx_q2_pkt", IPQESS_STAT(rx_q2_pkt)}, +- {"rx_q3_pkt", IPQESS_STAT(rx_q3_pkt)}, +- {"rx_q4_pkt", IPQESS_STAT(rx_q4_pkt)}, +- {"rx_q5_pkt", IPQESS_STAT(rx_q5_pkt)}, +- {"rx_q6_pkt", IPQESS_STAT(rx_q6_pkt)}, +- {"rx_q7_pkt", IPQESS_STAT(rx_q7_pkt)}, +- {"rx_q0_byte", IPQESS_STAT(rx_q0_byte)}, +- {"rx_q1_byte", IPQESS_STAT(rx_q1_byte)}, +- {"rx_q2_byte", IPQESS_STAT(rx_q2_byte)}, +- {"rx_q3_byte", IPQESS_STAT(rx_q3_byte)}, +- {"rx_q4_byte", IPQESS_STAT(rx_q4_byte)}, +- {"rx_q5_byte", IPQESS_STAT(rx_q5_byte)}, +- {"rx_q6_byte", IPQESS_STAT(rx_q6_byte)}, +- {"rx_q7_byte", IPQESS_STAT(rx_q7_byte)}, +- {"tx_desc_error", IPQESS_STAT(tx_desc_error)}, +-}; +- +-static int ipqess_get_strset_count(struct net_device *netdev, int sset) +-{ +- switch (sset) { +- case ETH_SS_STATS: +- return ARRAY_SIZE(ipqess_stats); +- default: +- netdev_dbg(netdev, "%s: Unsupported string set", __func__); +- return -EOPNOTSUPP; +- } +-} +- +-static void ipqess_get_strings(struct net_device *netdev, u32 stringset, +- u8 *data) +-{ +- u8 *p = data; +- u32 i; +- +- switch (stringset) { +- case ETH_SS_STATS: +- for (i = 0; i < ARRAY_SIZE(ipqess_stats); i++) +- ethtool_puts(&p, ipqess_stats[i].string); +- break; +- } +-} +- +-static void ipqess_get_ethtool_stats(struct net_device *netdev, +- struct ethtool_stats *stats, +- uint64_t *data) +-{ +- struct ipqess *ess = netdev_priv(netdev); +- u32 *essstats = (u32 *)&ess->ipqess_stats; +- int i; +- +- spin_lock(&ess->stats_lock); +- +- ipqess_update_hw_stats(ess); +- +- for (i = 0; i < ARRAY_SIZE(ipqess_stats); i++) +- data[i] = *(u32 *)(essstats + (ipqess_stats[i].offset / sizeof(u32))); +- +- spin_unlock(&ess->stats_lock); +-} +- +-static void ipqess_get_drvinfo(struct net_device *dev, +- struct ethtool_drvinfo *info) +-{ +- strscpy(info->driver, "qca_ipqess", DRVINFO_LEN); +- strscpy(info->bus_info, "axi", ETHTOOL_BUSINFO_LEN); +-} +- +-static int ipqess_get_link_ksettings(struct net_device *netdev, +- struct ethtool_link_ksettings *cmd) +-{ +- struct ipqess *ess = netdev_priv(netdev); +- +- return phylink_ethtool_ksettings_get(ess->phylink, cmd); +-} +- +-static int ipqess_set_link_ksettings(struct net_device *netdev, +- const struct ethtool_link_ksettings *cmd) +-{ +- struct ipqess *ess = netdev_priv(netdev); +- +- return phylink_ethtool_ksettings_set(ess->phylink, cmd); +-} +- +-static void ipqess_get_ringparam(struct net_device *netdev, +- struct ethtool_ringparam *ring, +- struct kernel_ethtool_ringparam *kernel_ering, +- struct netlink_ext_ack *extack) +-{ +- ring->tx_max_pending = IPQESS_TX_RING_SIZE; +- ring->rx_max_pending = IPQESS_RX_RING_SIZE; +-} +- +-static const struct ethtool_ops ipqesstool_ops = { +- .get_drvinfo = &ipqess_get_drvinfo, +- .get_link = ðtool_op_get_link, +- .get_link_ksettings = &ipqess_get_link_ksettings, +- .set_link_ksettings = &ipqess_set_link_ksettings, +- .get_strings = &ipqess_get_strings, +- .get_sset_count = &ipqess_get_strset_count, +- .get_ethtool_stats = &ipqess_get_ethtool_stats, +- .get_ringparam = ipqess_get_ringparam, +-}; +- +-void ipqess_set_ethtool_ops(struct net_device *netdev) +-{ +- netdev->ethtool_ops = &ipqesstool_ops; +-} ++// SPDX-License-Identifier: GPL-2.0 OR ISC ++/* Copyright (c) 2015 - 2016, The Linux Foundation. All rights reserved. ++ * Copyright (c) 2017 - 2018, John Crispin ++ * Copyright (c) 2021 - 2022, Maxime Chevallier ++ * Copyright (c) 2026, mooleshacat ++ * ++ */ ++ ++#include ++#include ++#include ++#include ++ ++#include "ipqess.h" ++ ++struct ipqess_ethtool_stats { ++ u8 string[ETH_GSTRING_LEN]; ++ u32 offset; ++}; ++ ++#define IPQESS_STAT(m) offsetof(struct ipqess_statistics, m) ++#define DRVINFO_LEN 32 ++ ++static const struct ipqess_ethtool_stats ipqess_stats[] = { ++ {"tx_q0_pkt", IPQESS_STAT(tx_q0_pkt)}, ++ {"tx_q1_pkt", IPQESS_STAT(tx_q1_pkt)}, ++ {"tx_q2_pkt", IPQESS_STAT(tx_q2_pkt)}, ++ {"tx_q3_pkt", IPQESS_STAT(tx_q3_pkt)}, ++ {"tx_q4_pkt", IPQESS_STAT(tx_q4_pkt)}, ++ {"tx_q5_pkt", IPQESS_STAT(tx_q5_pkt)}, ++ {"tx_q6_pkt", IPQESS_STAT(tx_q6_pkt)}, ++ {"tx_q7_pkt", IPQESS_STAT(tx_q7_pkt)}, ++ {"tx_q8_pkt", IPQESS_STAT(tx_q8_pkt)}, ++ {"tx_q9_pkt", IPQESS_STAT(tx_q9_pkt)}, ++ {"tx_q10_pkt", IPQESS_STAT(tx_q10_pkt)}, ++ {"tx_q11_pkt", IPQESS_STAT(tx_q11_pkt)}, ++ {"tx_q12_pkt", IPQESS_STAT(tx_q12_pkt)}, ++ {"tx_q13_pkt", IPQESS_STAT(tx_q13_pkt)}, ++ {"tx_q14_pkt", IPQESS_STAT(tx_q14_pkt)}, ++ {"tx_q15_pkt", IPQESS_STAT(tx_q15_pkt)}, ++ {"tx_q0_byte", IPQESS_STAT(tx_q0_byte)}, ++ {"tx_q1_byte", IPQESS_STAT(tx_q1_byte)}, ++ {"tx_q2_byte", IPQESS_STAT(tx_q2_byte)}, ++ {"tx_q3_byte", IPQESS_STAT(tx_q3_byte)}, ++ {"tx_q4_byte", IPQESS_STAT(tx_q4_byte)}, ++ {"tx_q5_byte", IPQESS_STAT(tx_q5_byte)}, ++ {"tx_q6_byte", IPQESS_STAT(tx_q6_byte)}, ++ {"tx_q7_byte", IPQESS_STAT(tx_q7_byte)}, ++ {"tx_q8_byte", IPQESS_STAT(tx_q8_byte)}, ++ {"tx_q9_byte", IPQESS_STAT(tx_q9_byte)}, ++ {"tx_q10_byte", IPQESS_STAT(tx_q10_byte)}, ++ {"tx_q11_byte", IPQESS_STAT(tx_q11_byte)}, ++ {"tx_q12_byte", IPQESS_STAT(tx_q12_byte)}, ++ {"tx_q13_byte", IPQESS_STAT(tx_q13_byte)}, ++ {"tx_q14_byte", IPQESS_STAT(tx_q14_byte)}, ++ {"tx_q15_byte", IPQESS_STAT(tx_q15_byte)}, ++ {"rx_q0_pkt", IPQESS_STAT(rx_q0_pkt)}, ++ {"rx_q1_pkt", IPQESS_STAT(rx_q1_pkt)}, ++ {"rx_q2_pkt", IPQESS_STAT(rx_q2_pkt)}, ++ {"rx_q3_pkt", IPQESS_STAT(rx_q3_pkt)}, ++ {"rx_q4_pkt", IPQESS_STAT(rx_q4_pkt)}, ++ {"rx_q5_pkt", IPQESS_STAT(rx_q5_pkt)}, ++ {"rx_q6_pkt", IPQESS_STAT(rx_q6_pkt)}, ++ {"rx_q7_pkt", IPQESS_STAT(rx_q7_pkt)}, ++ {"rx_q0_byte", IPQESS_STAT(rx_q0_byte)}, ++ {"rx_q1_byte", IPQESS_STAT(rx_q1_byte)}, ++ {"rx_q2_byte", IPQESS_STAT(rx_q2_byte)}, ++ {"rx_q3_byte", IPQESS_STAT(rx_q3_byte)}, ++ {"rx_q4_byte", IPQESS_STAT(rx_q4_byte)}, ++ {"rx_q5_byte", IPQESS_STAT(rx_q5_byte)}, ++ {"rx_q6_byte", IPQESS_STAT(rx_q6_byte)}, ++ {"rx_q7_byte", IPQESS_STAT(rx_q7_byte)}, ++ {"tx_desc_error", IPQESS_STAT(tx_desc_error)}, ++}; ++ ++static int ipqess_get_strset_count(struct net_device *netdev, int sset) ++{ ++ switch (sset) { ++ case ETH_SS_STATS: ++ return ARRAY_SIZE(ipqess_stats); ++ default: ++ netdev_dbg(netdev, "%s: Unsupported string set", __func__); ++ return -EOPNOTSUPP; ++ } ++} ++ ++static void ipqess_get_strings(struct net_device *netdev, u32 stringset, ++ u8 *data) ++{ ++ u8 *p = data; ++ u32 i; ++ ++ switch (stringset) { ++ case ETH_SS_STATS: ++ for (i = 0; i < ARRAY_SIZE(ipqess_stats); i++) ++ ethtool_puts(&p, ipqess_stats[i].string); ++ break; ++ } ++} ++ ++static void ipqess_get_ethtool_stats(struct net_device *netdev, ++ struct ethtool_stats *stats, ++ uint64_t *data) ++{ ++ struct ipqess *ess = ipqess_from_netdev(netdev); ++ u32 *essstats = (u32 *)&ess->ipqess_stats; ++ int i; ++ ++ spin_lock(&ess->stats_lock); ++ ++ ipqess_update_hw_stats(ess); ++ ++ for (i = 0; i < ARRAY_SIZE(ipqess_stats); i++) ++ data[i] = *(u32 *)(essstats + (ipqess_stats[i].offset / sizeof(u32))); ++ ++ spin_unlock(&ess->stats_lock); ++} ++ ++static void ipqess_get_drvinfo(struct net_device *dev, ++ struct ethtool_drvinfo *info) ++{ ++ strscpy(info->driver, "qca_ipqess", DRVINFO_LEN); ++ strscpy(info->bus_info, "axi", ETHTOOL_BUSINFO_LEN); ++} ++ ++static int ipqess_get_link_ksettings(struct net_device *netdev, ++ struct ethtool_link_ksettings *cmd) ++{ ++ struct ipqess *ess = ipqess_from_netdev(netdev); ++ ++ /* netdev2 has no phylink instance — it is a fixed internal link */ ++ if (!ess->phylink) ++ return -EOPNOTSUPP; ++ ++ return phylink_ethtool_ksettings_get(ess->phylink, cmd); ++} ++ ++static int ipqess_set_link_ksettings(struct net_device *netdev, ++ const struct ethtool_link_ksettings *cmd) ++{ ++ struct ipqess *ess = ipqess_from_netdev(netdev); ++ ++ /* netdev2 has no phylink instance — it is a fixed internal link */ ++ if (!ess->phylink) ++ return -EOPNOTSUPP; ++ ++ return phylink_ethtool_ksettings_set(ess->phylink, cmd); ++} ++ ++static void ipqess_get_ringparam(struct net_device *netdev, ++ struct ethtool_ringparam *ring, ++ struct kernel_ethtool_ringparam *kernel_ering, ++ struct netlink_ext_ack *extack) ++{ ++ ring->tx_max_pending = IPQESS_TX_RING_SIZE; ++ ring->rx_max_pending = IPQESS_RX_RING_SIZE; ++} ++ ++static const struct ethtool_ops ipqesstool_ops = { ++ .get_drvinfo = &ipqess_get_drvinfo, ++ .get_link = ðtool_op_get_link, ++ .get_link_ksettings = &ipqess_get_link_ksettings, ++ .set_link_ksettings = &ipqess_set_link_ksettings, ++ .get_strings = &ipqess_get_strings, ++ .get_sset_count = &ipqess_get_strset_count, ++ .get_ethtool_stats = &ipqess_get_ethtool_stats, ++ .get_ringparam = ipqess_get_ringparam, ++}; ++ ++void ipqess_set_ethtool_ops(struct net_device *netdev) ++{ ++ netdev->ethtool_ops = &ipqesstool_ops; ++}