From 9e8f964630927ae1a5e1e4b2f523e1a67018e61d Mon Sep 17 00:00:00 2001 From: Xilin Wu Date: Mon, 8 Jun 2026 14:04:04 +0800 Subject: [PATCH 01/21] misc: tc956x_pci: maintain support for v1 style DT Signed-off-by: Xilin Wu --- drivers/misc/tc956x_pci.c | 111 ++++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 10 deletions(-) diff --git a/drivers/misc/tc956x_pci.c b/drivers/misc/tc956x_pci.c index 741a0ae0d3afb..18025a8356e5a 100644 --- a/drivers/misc/tc956x_pci.c +++ b/drivers/misc/tc956x_pci.c @@ -236,25 +236,27 @@ static void adev_remove(void *data) } static int adev_device_add(struct device *dev, const char *name, u32 id, - void *platform_data) + struct device_node *of_node, void *platform_data) { struct auxiliary_device *adev; int ret; adev = kzalloc_obj(*adev); - if (!adev) + if (!adev) { + of_node_put(of_node); return -ENOMEM; + } adev->id = id; adev->name = name; adev->dev.parent = dev; adev->dev.platform_data = platform_data; adev->dev.release = adev_release; - device_set_of_node_from_dev(&adev->dev, dev); + adev->dev.of_node = of_node; ret = auxiliary_device_init(adev); if (ret) { - of_node_put(adev->dev.of_node); + of_node_put(of_node); kfree(adev); return ret; } @@ -268,21 +270,102 @@ static int adev_device_add(struct device *dev, const char *name, u32 id, return devm_add_action_or_reset(dev, adev_remove, adev); } +static bool dev_node_has_mdio_child(struct device_node *np) +{ + struct device_node *mdio; + + mdio = of_get_child_by_name(np, "mdio"); + if (!mdio) + return false; + + of_node_put(mdio); + + return true; +} + +static bool dev_node_is_gpio(struct device *dev, struct device_node *np) +{ + if (!of_property_present(np, "gpio-controller")) + return false; + + if (!of_property_present(np, "#gpio-cells")) { + dev_err(dev, "gpio node contains no #gpio-cells property\n"); + return false; + } + + return true; +} + +/* Returns a reference to the GPIO's DT node, or a null pointer */ +static struct device_node *dev_node_gpio(struct device *dev) +{ + struct device_node *np; + + /* The GPIO sub-node is not required (platform might not need it) */ + for_each_child_of_node(dev->of_node, np) + if (!strcmp(np->name, "gpio")) + break; + if (np) { + if (dev_node_is_gpio(dev, np)) + return np; + + of_node_put(np); + + return NULL; + } + + /* + * The original TC956x binding placed the GPIO controller properties on + * PCI function 0 itself. Keep accepting that form so existing DTs do not + * need to grow a gpio sub-node. + */ + if (dev_node_is_gpio(dev, dev->of_node)) + return of_node_get(dev->of_node); + + return NULL; +} + +/* Returns a reference to the XGMAC's DT node, or a null pointer */ +static struct device_node *dev_node_xgmac(struct device *dev) +{ + struct device_node *np; + + for_each_child_of_node(dev->of_node, np) + if (!strcmp(np->name, "ethernet")) + return np; + + /* + * The original TC956x binding placed Ethernet controller properties on + * the PCI function node. Treat that node as the XGMAC node when it has + * the old shape. + */ + if (of_property_present(dev->of_node, "phy-mode") || + of_property_present(dev->of_node, "phy-connection-type") || + of_property_present(dev->of_node, "phy-handle") || + dev_node_has_mdio_child(dev->of_node)) + return of_node_get(dev->of_node); + + return NULL; +} + /* The embedded GPIO controller has an auxiliary device driver */ static int chip_gpio_adev_add(struct tc956x_chip *chip) { struct device *dev = chip->dev; + struct device_node *np; struct regmap *regmap; - /* If needed, PCIe function 0 implements the GPIO controller. */ - if (!device_property_present(dev, "gpio-controller")) + np = dev_node_gpio(dev); + if (!np) return 0; regmap = devm_regmap_init_mmio(dev, chip->sfr[0], &gpio_regmap_config); - if (IS_ERR(regmap)) + if (IS_ERR(regmap)) { + of_node_put(np); return PTR_ERR(regmap); + } - return adev_device_add(dev, GPIO_DEVICE_NAME, 0, regmap); + return adev_device_add(dev, GPIO_DEVICE_NAME, 0, np, regmap); } /* The two embedded XGMAC controllers have an auxiliary device driver */ @@ -293,16 +376,24 @@ static int function_xgmac_adev_add(struct pci_dev *pdev, u8 mac_id = PCI_FUNC(pdev->devfn); struct device *dev = &pdev->dev; struct tc956x_dwmac_data *data; + struct device_node *np; void __iomem *sfr; int ret; if (mac_id > 1) return -EINVAL; + + np = dev_node_xgmac(dev); + if (!np) + return 0; + sfr = chip->sfr[mac_id]; data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); - if (!data) + if (!data) { + of_node_put(np); return -ENOMEM; + } data->chip = chip; data->msigen = sfr + MSIGEN_OFFSET(mac_id); @@ -312,7 +403,7 @@ static int function_xgmac_adev_add(struct pci_dev *pdev, data->rev_id = chip->rev_id; data->mac_id = mac_id; - ret = adev_device_add(dev, TC956X_XGMAC_DEV_NAME, mac_id, data); + ret = adev_device_add(dev, TC956X_XGMAC_DEV_NAME, mac_id, np, data); if (ret) return ret; From 9953b70b0db12c094aed29c75b17e8fb614c32f6 Mon Sep 17 00:00:00 2001 From: Xilin Wu Date: Mon, 8 Jun 2026 14:26:43 +0800 Subject: [PATCH 02/21] net: stmmac: tc956x: handle 2.5G links with SGMII There are some discussions about this upstream. Handle 2.5G links with SGMII to make it work for now. Signed-off-by: Xilin Wu --- .../net/ethernet/stmicro/stmmac/dwmac-tc956x.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c index fbe1fc9e7bddd..ceccc6953e851 100644 --- a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c +++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c @@ -37,7 +37,7 @@ /* Fields and values for the EMACTL registers */ #define EMAC_SP_SEL_MASK GENMASK(3, 0) -#define SP_SEL_SGMII_2500M 4 +#define SP_SEL_2500BASEX 4 #define SP_SEL_SGMII_1000M 5 #define SP_SEL_SGMII_100M 6 #define SP_SEL_SGMII_10M 7 @@ -131,11 +131,16 @@ struct tc956x_mac_speed { }; static struct tc956x_mac_speed mac_speed[] = { - { PHY_INTERFACE_MODE_2500BASEX, SPEED_2500, SP_SEL_SGMII_2500M, }, - { PHY_INTERFACE_MODE_SGMII, SPEED_2500, SP_SEL_SGMII_2500M, }, - { PHY_INTERFACE_MODE_SGMII, SPEED_1000, SP_SEL_SGMII_1000M, }, - { PHY_INTERFACE_MODE_SGMII, SPEED_100, SP_SEL_SGMII_100M, }, - { PHY_INTERFACE_MODE_SGMII, SPEED_10, SP_SEL_SGMII_10M, }, + { PHY_INTERFACE_MODE_2500BASEX, SPEED_2500, SP_SEL_2500BASEX }, + /* + * QCA808x switches its host interface to 2500BASE-X at 2.5G, but older + * stmmac only passes speed to fix_mac_speed(). Legacy DTs therefore + * still arrive here as SGMII, so program the 2500BASE-X selector. + */ + { PHY_INTERFACE_MODE_SGMII, SPEED_2500, SP_SEL_2500BASEX }, + { PHY_INTERFACE_MODE_SGMII, SPEED_1000, SP_SEL_SGMII_1000M }, + { PHY_INTERFACE_MODE_SGMII, SPEED_100, SP_SEL_SGMII_100M }, + { PHY_INTERFACE_MODE_SGMII, SPEED_10, SP_SEL_SGMII_10M }, }; /* TC956x uses indirect addressing so this need only describe a 1KiB range */ From e9e09f8bcd9f06cf9dc4efd7deb3d73e35d4dff1 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Wed, 10 Jun 2026 18:10:55 +0800 Subject: [PATCH 03/21] misc: tc956x_pci: use unique auxiliary device IDs per chip The TC956x PCI driver currently uses fixed auxiliary device IDs for the GPIO and XGMAC devices. This works for a single QPS615, but breaks when multiple QPS615 chips are present because their auxiliary devices are registered with the same names. Allocate a per-chip instance ID and include it in the auxiliary device IDs. Keep the two XGMAC IDs adjacent for each chip and use the same instance ID for the optional GPIO auxiliary device. This allows systems with multiple QPS615 chips to probe all TC956x auxiliary devices without name collisions. Signed-off-by: Junhao Xie --- drivers/misc/tc956x_pci.c | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/drivers/misc/tc956x_pci.c b/drivers/misc/tc956x_pci.c index 18025a8356e5a..17410fe7649f2 100644 --- a/drivers/misc/tc956x_pci.c +++ b/drivers/misc/tc956x_pci.c @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -149,6 +150,7 @@ enum clock_id { * @sfr: Mapped SFR regions (BAR 4, one per PCI function) * @bridge_config: Regmap used for bridge configuration (BAR 0) * @reset_clock_regmap: Regmap used for resets and clocks + * @instance_id: TC956X instance ID used for auxiliary device IDs * @rev_id: Chip revision ID (for quirks) */ struct tc956x_chip { @@ -156,9 +158,12 @@ struct tc956x_chip { void __iomem *sfr[2]; void __iomem *bridge_config; struct regmap *reset_clock_regmap; + int instance_id; u8 rev_id; }; +static DEFINE_IDA(tc956x_instance_ida); + static const struct regmap_config gpio_regmap_config = { .name = "tc956x-gpio", .reg_bits = 32, @@ -235,6 +240,14 @@ static void adev_remove(void *data) auxiliary_device_uninit(adev); } +static void chip_instance_id_free(void *data) +{ + struct tc956x_chip *chip = data; + + ida_free(&tc956x_instance_ida, chip->instance_id); +} + +/* The of_node reference is always be dropped (success or not) */ static int adev_device_add(struct device *dev, const char *name, u32 id, struct device_node *of_node, void *platform_data) { @@ -365,7 +378,8 @@ static int chip_gpio_adev_add(struct tc956x_chip *chip) return PTR_ERR(regmap); } - return adev_device_add(dev, GPIO_DEVICE_NAME, 0, np, regmap); + return adev_device_add(dev, GPIO_DEVICE_NAME, chip->instance_id, np, + regmap); } /* The two embedded XGMAC controllers have an auxiliary device driver */ @@ -403,7 +417,8 @@ static int function_xgmac_adev_add(struct pci_dev *pdev, data->rev_id = chip->rev_id; data->mac_id = mac_id; - ret = adev_device_add(dev, TC956X_XGMAC_DEV_NAME, mac_id, np, data); + ret = adev_device_add(dev, TC956X_XGMAC_DEV_NAME, + chip->instance_id * 2 + mac_id, np, data); if (ret) return ret; @@ -567,6 +582,15 @@ static struct tc956x_chip *chip_get(struct pci_dev *pdev) if (!chip) return ERR_PTR(-ENOMEM); + ret = ida_alloc(&tc956x_instance_ida, GFP_KERNEL); + if (ret < 0) + return ERR_PTR(ret); + chip->instance_id = ret; + + ret = devm_add_action_or_reset(dev, chip_instance_id_free, chip); + if (ret) + return ERR_PTR(ret); + /* * The function whose device pointer matches the chip's * device pointer manages common resources (like MSIGEN). From a6c9c1970e1fd749b19c89a218674600b978cbc5 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Wed, 10 Jun 2026 18:19:37 +0800 Subject: [PATCH 04/21] net: stmmac: tc956x: use auxiliary device ID as bus ID The TC956x DWMAC driver uses the MAC index as the stmmac bus ID. This is unique for a single QPS615, but not for systems with multiple QPS615 chips, where each chip exposes MAC0 and MAC1. Use the auxiliary device ID instead. The parent TC956x PCI driver assigns globally unique auxiliary IDs, so this keeps the stmmac bus ID unique across all QPS615 instances. Signed-off-by: Junhao Xie --- drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c index ceccc6953e851..567eacfd1cb28 100644 --- a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c +++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c @@ -598,7 +598,7 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) speed = ret; plat->core_type = DWMAC_CORE_XGMAC; - plat->bus_id = td->auxbus_data->mac_id; + plat->bus_id = to_auxiliary_dev(dev)->id; plat->phy_interface = phy_interface; plat->mdio_bus_data = &td->mdio_bus_data; /* Parent PCI device is used for DMA */ From 4ad2c439150e5b57dd8b88e64a9f1a50d80d801e Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Wed, 10 Jun 2026 18:21:01 +0800 Subject: [PATCH 05/21] net: stmmac: tc956x: add USXGMII support Add TC956x MAC programming for USXGMII links. Select the proper EMAC speed selector for 10M/100M/1G/2.5G/5G/10G, report 10G as the maximum speed for USXGMII, and keep in-band autonegotiation disabled for this PHY mode. USXGMII also requires the common SerDes PLL, SGMII clock, reference clock output, and per-MAC 125 MHz and 312.5 MHz clocks. Enable those clocks only for USXGMII so the existing SGMII and 2500BASE-X paths are unchanged. Configure the MAC before releasing reset so the XGMAC, PMA and XPCS come out of reset with a valid USXGMII mode. Signed-off-by: Junhao Xie --- drivers/misc/tc956x_pci.c | 1 + .../ethernet/stmicro/stmmac/dwmac-tc956x.c | 65 ++++++++++++++++--- include/soc/toshiba/tc956x-dwmac.h | 20 ++++++ 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/drivers/misc/tc956x_pci.c b/drivers/misc/tc956x_pci.c index 17410fe7649f2..b6f022d52fbc9 100644 --- a/drivers/misc/tc956x_pci.c +++ b/drivers/misc/tc956x_pci.c @@ -414,6 +414,7 @@ static int function_xgmac_adev_add(struct pci_dev *pdev, data->msigen_irq = msigen_irq; data->emac = sfr + DWMAC_OFFSET(mac_id); data->emac_ctl = sfr + EMAC_CTL_OFFSET(mac_id); + data->sfr = sfr; data->rev_id = chip->rev_id; data->mac_id = mac_id; diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c index 567eacfd1cb28..019ecd2b25581 100644 --- a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c +++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c @@ -41,6 +41,10 @@ #define SP_SEL_SGMII_1000M 5 #define SP_SEL_SGMII_100M 6 #define SP_SEL_SGMII_10M 7 +#define SP_SEL_USXGMII_10G 8 +#define SP_SEL_USXGMII_100M_10G 9 +#define SP_SEL_USXGMII_5G 10 +#define SP_SEL_USXGMII_2500M 13 #define EMAC_PHY_INF_SEL_MASK GENMASK(5, 4) #define PCS_CLK_PHY 1 /* Clock from PHY */ #define EMAC_INV_SGM_SIG_DET BIT(6) /* 1 = polarity inverted */ @@ -73,6 +77,11 @@ enum msigen_hwirq { /* Offset to the PMATOP memory block, relative to the EMAC address range */ #define DWMAC_PMATOP_OFFSET 0x4000 +/* TC956X SFR offsets used for USXGMII PMA/lane setup */ +#define TC956X_GLUE_PHY_REG_ACCESS_CTRL 0x2c030 +#define TC956X_PHY_CORE0_GL_LANE_ACCESS 0x28000 +#define TC956X_PMA_LN_PCS2PMA_PHYMODE_R2 0x2b268 + #define PMA_CML_GL_PM_CFG0 0x01b8 /* @@ -131,6 +140,12 @@ struct tc956x_mac_speed { }; static struct tc956x_mac_speed mac_speed[] = { + { PHY_INTERFACE_MODE_USXGMII, SPEED_10000, SP_SEL_USXGMII_10G }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_5000, SP_SEL_USXGMII_5G }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_2500, SP_SEL_USXGMII_2500M }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_1000, SP_SEL_USXGMII_10G }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_100, SP_SEL_USXGMII_100M_10G }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_10, SP_SEL_USXGMII_100M_10G }, { PHY_INTERFACE_MODE_2500BASEX, SPEED_2500, SP_SEL_2500BASEX }, /* * QCA808x switches its host interface to 2500BASE-X at 2.5G, but older @@ -254,6 +269,7 @@ static void tc956x_pma_init(struct tc956x_data *td) { const struct tc956x_chip *chip = td->auxbus_data->chip; void __iomem *emac_ctl = td->auxbus_data->emac_ctl; + void __iomem *sfr = td->auxbus_data->sfr; u32 id = td->auxbus_data->mac_id; void __iomem *pmatop; u32 val; @@ -290,12 +306,18 @@ static void tc956x_pma_init(struct tc956x_data *td) tc956x_reset_deassert(chip, id, MAC_RESET_PMA); + if (td->plat->phy_interface == PHY_INTERFACE_MODE_USXGMII) { + writel(0x0f, sfr + TC956X_GLUE_PHY_REG_ACCESS_CTRL); + writel(0x0f, sfr + TC956X_PHY_CORE0_GL_LANE_ACCESS); + writel(0x02, sfr + TC956X_PMA_LN_PCS2PMA_PHYMODE_R2); + } + WARN_ON(readl_poll_timeout(emac_ctl, val, val & EMAC_INIT_DONE, 50, 1000000)); } -static int tc956x_mac_speed_select(struct tc956x_data *td, int speed) +static int tc956x_mac_speed_select(struct tc956x_data *td, + phy_interface_t phy_interface, int speed) { - phy_interface_t phy_interface = td->plat->phy_interface; struct net_device *netdev; int i; @@ -313,13 +335,14 @@ static int tc956x_mac_speed_select(struct tc956x_data *td, int speed) return -EOPNOTSUPP; } -static int tc956x_mac_configure(struct tc956x_data *td, int speed) +static int tc956x_mac_configure(struct tc956x_data *td, + phy_interface_t phy_interface, int speed) { void __iomem *emac_ctl = td->auxbus_data->emac_ctl; int sp_sel; u32 val; - sp_sel = tc956x_mac_speed_select(td, speed); + sp_sel = tc956x_mac_speed_select(td, phy_interface, speed); if (sp_sel < 0) return sp_sel; @@ -346,10 +369,17 @@ static int tc956x_mac_enable(struct tc956x_data *td) if (id) tc956x_clock_enable(chip, id, MAC_CLOCK_RMII); - /* Set the speed related registers */ - ret = tc956x_mac_configure(td, plat->max_speed); + if (plat->phy_interface == PHY_INTERFACE_MODE_USXGMII) { + tc956x_common_clock_enable(chip, COMMON_CLOCK_PLL); + tc956x_common_clock_enable(chip, COMMON_CLOCK_SGMII); + tc956x_common_clock_enable(chip, COMMON_CLOCK_REFCLK); + tc956x_clock_enable(chip, id, MAC_CLOCK_125M); + tc956x_clock_enable(chip, id, MAC_CLOCK_312_5M); + } + + ret = tc956x_mac_configure(td, plat->phy_interface, plat->max_speed); if (ret) - return ret; + dev_warn(td->dev, "failed to configure MAC speed: %d\n", ret); tc956x_reset_deassert(chip, id, MAC_RESET_MAC); tc956x_pma_init(td); @@ -361,6 +391,7 @@ static int tc956x_mac_enable(struct tc956x_data *td) static void tc956x_mac_disable(struct tc956x_data *td) { const struct tc956x_chip *chip = td->auxbus_data->chip; + struct plat_stmmacenet_data *plat = td->plat; u32 id = td->auxbus_data->mac_id; tc956x_reset_assert(chip, id, MAC_RESET_MAC); @@ -372,6 +403,14 @@ static void tc956x_mac_disable(struct tc956x_data *td) tc956x_clock_disable(chip, id, MAC_CLOCK_TX); if (id) tc956x_clock_disable(chip, id, MAC_CLOCK_RMII); + + if (plat->phy_interface == PHY_INTERFACE_MODE_USXGMII) { + tc956x_clock_disable(chip, id, MAC_CLOCK_125M); + tc956x_clock_disable(chip, id, MAC_CLOCK_312_5M); + tc956x_common_clock_disable(chip, COMMON_CLOCK_REFCLK); + tc956x_common_clock_disable(chip, COMMON_CLOCK_SGMII); + tc956x_common_clock_disable(chip, COMMON_CLOCK_PLL); + } } static void tc956x_mac_init_state(struct tc956x_data *td) @@ -513,7 +552,10 @@ static void tc956x_fix_mac_speed(void *bsp_priv, int speed, unsigned int mode) { struct tc956x_data *td = bsp_priv; - tc956x_mac_configure(td, speed); + tc956x_mac_configure(td, td->plat->phy_interface, speed); + if (td->plat->phy_interface == PHY_INTERFACE_MODE_USXGMII) + return; + tc956x_pma_init(td); } @@ -563,6 +605,9 @@ static int tc956x_dwmac_parse_dt(struct tc956x_data *td) static int tc956x_lookup_max_speed(phy_interface_t phy_interface) { switch (phy_interface) { + case PHY_INTERFACE_MODE_USXGMII: + return SPEED_10000; + case PHY_INTERFACE_MODE_SGMII: case PHY_INTERFACE_MODE_2500BASEX: return SPEED_2500; @@ -612,7 +657,9 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) * represents "divide by 62" and gets the best rate under 2.5 MHz. */ plat->clk_csr = 0; /* MDC clock = clk_csr_i / 62 */ - plat->force_sf_dma_mode = 1; + plat->mdio_bus_data->default_an_inband = + phy_interface != PHY_INTERFACE_MODE_USXGMII; + plat->force_sf_dma_mode = true; plat->max_speed = speed; plat->unicast_filter_entries = 32; diff --git a/include/soc/toshiba/tc956x-dwmac.h b/include/soc/toshiba/tc956x-dwmac.h index 5ca39cf764be9..3abec80d012ae 100644 --- a/include/soc/toshiba/tc956x-dwmac.h +++ b/include/soc/toshiba/tc956x-dwmac.h @@ -32,11 +32,18 @@ enum tc956x_clock_id { MAC_CLOCK_RMII = 15, /* eMAC 1 only */ }; +enum tc956x_common_clock_id { + COMMON_CLOCK_PLL = 24, /* POEPLLCEN */ + COMMON_CLOCK_SGMII = 25, /* SGMPCIEN */ + COMMON_CLOCK_REFCLK = 26, /* REFCLKOCEN (PHY refclk output) */ +}; + /** * struct tc956x_dwmac_data - Structure passed to stmmac auxiliary devices. * @chip: Context pointer needed for reset and clock operations * @emac: I/O mapped address used by eMAC * @emac_ctl: I/O mapped address used for eMAC control + * @sfr: I/O mapped address used for TC956X SFR access * @msigen: I/O mapped address used by MSIGEN * @msigen_irq: IRQ number used by MSIGEN * @rev_id: Chip revision ID (for quirks) @@ -48,6 +55,7 @@ struct tc956x_dwmac_data { const struct tc956x_chip *chip; void __iomem *emac; void __iomem *emac_ctl; + void __iomem *sfr; void __iomem *msigen; unsigned int msigen_irq; u8 rev_id; @@ -81,4 +89,16 @@ static inline void tc956x_clock_disable(const struct tc956x_chip *chip, tc956x_reset_clock_set(chip, false, !mac_id, false, (u8)id); } +static inline void tc956x_common_clock_enable(const struct tc956x_chip *chip, + enum tc956x_common_clock_id id) +{ + tc956x_reset_clock_set(chip, false, true, true, (u8)id); +} + +static inline void tc956x_common_clock_disable(const struct tc956x_chip *chip, + enum tc956x_common_clock_id id) +{ + tc956x_reset_clock_set(chip, false, true, false, (u8)id); +} + #endif /* __TOSHIBA_TC956X_DWMAC_H__*/ From 8fc554d5863f7da2be2c84862cb9e7cdef574256 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Wed, 10 Jun 2026 18:25:43 +0800 Subject: [PATCH 06/21] WIP: net: stmmac: tc956x: improve 10G traffic performance Tune the TC956x stmmac platform data for 10G traffic. Use the maximum DMA descriptor ring sizes, expose a single RX/TX queue, and give that queue the available FIFO budget instead of splitting FIFO space across queues. Also disable AXI LPI and steer the shared MSIGEN interrupt away from the last online CPU to reduce contention during throughput tests. This is still a work-in-progress performance tuning patch and needs more validation before submission. Signed-off-by: Junhao Xie --- .../ethernet/stmicro/stmmac/dwmac-tc956x.c | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c index 019ecd2b25581..8afc5181cb362 100644 --- a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c +++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c @@ -32,8 +32,8 @@ #define TC956X_PTP_CLOCK_RATE (250 * HZ_PER_MHZ) -#define TC956X_RX_FIFO_KB 46 /* Shared by all RX queues */ -#define TC956X_TX_FIFO_KB 46 /* Shared by all TX queues */ +#define TC956X_RX_FIFO_KB 32 /* Shared by all RX queues */ +#define TC956X_TX_FIFO_KB 8 /* Shared by all TX queues */ /* Fields and values for the EMACTL registers */ #define EMAC_SP_SEL_MASK GENMASK(3, 0) @@ -216,6 +216,19 @@ static void tc956x_msigen_irq_chip_exit(struct irq_chip_generic *gc) static int tc956x_msigen_irq_domain_init(struct irq_domain *irq_domain) { struct tc956x_data *td = irq_domain->host_data; + unsigned int cpu, last_cpu = 0, prev_cpu = 0; + + for_each_cpu(cpu, cpu_online_mask) { + prev_cpu = last_cpu; + last_cpu = cpu; + } + + if (cpumask_weight(cpu_online_mask) > 1) + cpu = prev_cpu; + else + cpu = last_cpu; + + irq_set_affinity_and_hint(td->auxbus_data->msigen_irq, cpumask_of(cpu)); irq_set_chained_handler_and_data(td->auxbus_data->msigen_irq, tc956x_msigen_irq_handler, @@ -496,6 +509,13 @@ static int tc956x_mac_setup(void *apriv, struct mac_device_info *mac) td = priv->plat->bsp_priv; + /* 10G USXGMII traffic needs the maximum descriptor ring to avoid + * starving the DMA path. Keep this TC956X-specific instead of raising + * stmmac's global defaults for every platform. + */ + priv->dma_conf.dma_rx_size = DMA_MAX_RX_SIZE; + priv->dma_conf.dma_tx_size = DMA_MAX_TX_SIZE; + /* dwxgmac301_dma_ops needs extending to provide DMA address translation */ dma = &td->dma; *dma = dwxgmac301_dma_ops; @@ -664,17 +684,12 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) plat->unicast_filter_entries = 32; /* - * TC956x has 8 RX queues but we observe significantly reduced RX - * bandwidth if we don't have at least 8k FIFO space per queue, so - * by default we avoid using all the queues. - */ - plat->rx_queues_to_use = 4; - - /* - * TX956x has 8 TX queues but only #0 to #3 work for general IP traffic. - * For now we will limit the driver to only these queues. + * Use a single ordinary RX/TX queue and give it the whole FIFO partition. + * TC956X partitions FIFO per function; exposing multiple stmmac queues makes + * the core split this budget and leaves the active queue too small for 10G. */ - plat->tx_queues_to_use = 4; + plat->rx_queues_to_use = 1; + plat->tx_queues_to_use = 1; /* * Oversized FIFOs result in reduced performance in bandwidth tests. @@ -714,7 +729,7 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) /* AXI Configuration */ axi = &td->axi; - axi->axi_lpi_en = 1; + axi->axi_lpi_en = 0; axi->axi_wr_osr_lmt = 31; axi->axi_rd_osr_lmt = 31; /* All sizes (2^2..2^8) are supported */ From 6f0d149d06958c26f1a673e3e038cb154f1e92c8 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Wed, 10 Jun 2026 18:26:47 +0800 Subject: [PATCH 07/21] WIP: net: stmmac: tc956x: deassert MDIO PHY resets before stmmac probe Some QPS615 designs keep external PHYs in reset through GPIOs described under the MDIO node. Deassert those reset GPIOs after the TC956x MAC, PMA and XPCS are enabled, and honor an optional reset-deassert-us delay before handing the device to stmmac. This allows the external PHY to be reachable when stmmac registers the MDIO bus and starts probing PHY devices. This is still a work-in-progress reset sequencing patch and needs binding and lifetime review before submission. Signed-off-by: Junhao Xie --- .../ethernet/stmicro/stmmac/dwmac-tc956x.c | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c index 8afc5181cb362..3d17d1797635a 100644 --- a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c +++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c @@ -10,7 +10,9 @@ #include #include +#include #include +#include #include #include #include @@ -622,6 +624,29 @@ static int tc956x_dwmac_parse_dt(struct tc956x_data *td) return 0; } +static void tc956x_deassert_phy_resets(struct tc956x_data *td) +{ + struct device_node *child; + struct gpio_desc *reset; + u32 delay_us; + + if (!td->plat->mdio_node) + return; + + for_each_available_child_of_node(td->plat->mdio_node, child) { + reset = fwnode_gpiod_get_index(of_fwnode_handle(child), "reset", 0, + GPIOD_OUT_LOW, "phy-reset"); + if (IS_ERR(reset)) + continue; + + gpiod_set_value_cansleep(reset, 0); + gpiod_put(reset); + + if (!of_property_read_u32(child, "reset-deassert-us", &delay_us)) + fsleep(delay_us); + } +} + static int tc956x_lookup_max_speed(phy_interface_t phy_interface) { switch (phy_interface) { @@ -807,6 +832,8 @@ static int tc956x_dwmac_probe(struct auxiliary_device *adev, /* Put the MAC in a known initial state */ tc956x_mac_init_state(td); + tc956x_mac_enable(td); + tc956x_deassert_phy_resets(td); ret = tc956x_stmmac_resources_init(td, &res); if (ret) From e03aab5230da9172c87f4549464e908b19a1356b Mon Sep 17 00:00:00 2001 From: Daniel Thompson Date: Thu, 4 Jun 2026 20:00:11 -0500 Subject: [PATCH 08/21] net: pcs: xpcs: re-order xpcs_pre_config() to update after the reset Currently, on Wangxun platforms, the XPCS is configured during the call to xpcs_switch_interface_mode() and, if the need_reset flag is set, the XPCS is reset and the configuration will be lost. This is harmless at present because need_reset will never actually be set on these platforms. Nevertheless having xpcs_switch_interface_mode() on the wrong side of the reset is an obstacle for future changes where wiping out programmed configuration with a reset could be harmful. Reorder xpcs_pre_config() to allow the reset can happen before we switch interface mode. To make this work we have to hoist the special case logic for SGMII into the parent function. Signed-off-by: Daniel Thompson Signed-off-by: Alex Elder --- drivers/net/pcs/pcs-xpcs.c | 52 ++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/drivers/net/pcs/pcs-xpcs.c b/drivers/net/pcs/pcs-xpcs.c index 1d62d5b31c61c..905b4c0ea501a 100644 --- a/drivers/net/pcs/pcs-xpcs.c +++ b/drivers/net/pcs/pcs-xpcs.c @@ -705,46 +705,50 @@ static void xpcs_get_interfaces(struct dw_xpcs *xpcs, unsigned long *interfaces) static int xpcs_switch_interface_mode(struct dw_xpcs *xpcs, phy_interface_t interface) { - int ret = 0; + /* Wangxun provides a full alternative implementation to handle quirks */ + if (xpcs->info.pma == WX_TXGBE_XPCS_PMA_10G_ID) + return txgbe_xpcs_switch_mode(xpcs, interface); - if (xpcs->info.pma == WX_TXGBE_XPCS_PMA_10G_ID) { - ret = txgbe_xpcs_switch_mode(xpcs, interface); - } else if (xpcs->interface != interface) { - if (interface == PHY_INTERFACE_MODE_SGMII) - xpcs->need_reset = true; - xpcs->interface = interface; - } + xpcs->interface = interface; - return ret; + return 0; } static void xpcs_pre_config(struct phylink_pcs *pcs, phy_interface_t interface) { struct dw_xpcs *xpcs = phylink_pcs_to_xpcs(pcs); const struct dw_xpcs_compat *compat; + bool force_reset; int ret; - ret = xpcs_switch_interface_mode(xpcs, interface); - if (ret) - dev_err(&xpcs->mdiodev->dev, "switch interface failed: %pe\n", - ERR_PTR(ret)); + /* + * According to the XPCS datasheet, a soft reset is required to initiate + * Clause 37 auto-negotiation when the XPCS switches interface modes. + */ + force_reset = interface == PHY_INTERFACE_MODE_SGMII; - if (!xpcs->need_reset) - return; + if (force_reset || xpcs->need_reset) { + compat = xpcs_find_compat(xpcs, interface); + if (!compat) { + dev_err(&xpcs->mdiodev->dev, "unsupported interface %s\n", + phy_modes(interface)); + return; + } - compat = xpcs_find_compat(xpcs, interface); - if (!compat) { - dev_err(&xpcs->mdiodev->dev, "unsupported interface %s\n", - phy_modes(interface)); - return; + ret = xpcs_soft_reset(xpcs, compat); + if (ret) { + dev_err(&xpcs->mdiodev->dev, "soft reset failed: %pe\n", + ERR_PTR(ret)); + return; + } + + xpcs->need_reset = false; } - ret = xpcs_soft_reset(xpcs, compat); + ret = xpcs_switch_interface_mode(xpcs, interface); if (ret) - dev_err(&xpcs->mdiodev->dev, "soft reset failed: %pe\n", + dev_err(&xpcs->mdiodev->dev, "switch interface failed: %pe\n", ERR_PTR(ret)); - - xpcs->need_reset = false; } static int xpcs_config_operating_mode(struct dw_xpcs *xpcs, int an_mode) From 7a6c15b0150982b6d858fa0a4ffdccb0442d94f2 Mon Sep 17 00:00:00 2001 From: Daniel Thompson Date: Thu, 4 Jun 2026 20:00:12 -0500 Subject: [PATCH 09/21] net: pcs: pcs-xpcs: select operating mode for 10G-baseR capable PCS Currently the XPCS found on Toshiba TC9564 (a.k.a. Qualcomm QPS615) is unable to operate at 2500base-X and slower with a PHY connected using SGMII/2500base-X (in our case a Qualcomm QCA8081). The problem arises because this XPCS supports 10Gbase-R. That means that the reset value of SR_XS_PCS_CTRL2:PCS_TYPE_SEL (0) is valid and this suppresses the modal switching based on bit 13 of SR_PMA_CTRL1 or SR_XS_PCS_CTRL1. A fix for this behaviour is already implemented by txgbe_xpcs_switch_mode() as part of the quirks for WangXun devices. Rather than introduce another quirk for TC956x let's attempt so solve this generically by setting SR_XS_PCS_CTRL2:PCS_TYPE_SEL to a reserved value when we detect the right we detect the right combination of phy interface and XPCS feature support. The generic strategy adopted requires the default value of PCS_TYPE_SEL to be 0 on devices that support 10Gbase-R. Based on TC9564 documentation and the logic already implemented for WangXun I believe this is likely to be the case for currently supported XPCS devices. Sadly I don't have access to generic XPCS docs to confirm. However I think the benefits of avoiding a cargo culted quirk outweights the risk of regression. Signed-off-by: Daniel Thompson Signed-off-by: Alex Elder --- drivers/net/pcs/pcs-xpcs.c | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/drivers/net/pcs/pcs-xpcs.c b/drivers/net/pcs/pcs-xpcs.c index 905b4c0ea501a..ba475436100d2 100644 --- a/drivers/net/pcs/pcs-xpcs.c +++ b/drivers/net/pcs/pcs-xpcs.c @@ -705,10 +705,49 @@ static void xpcs_get_interfaces(struct dw_xpcs *xpcs, unsigned long *interfaces) static int xpcs_switch_interface_mode(struct dw_xpcs *xpcs, phy_interface_t interface) { + int mdio_stat2, ret; + /* Wangxun provides a full alternative implementation to handle quirks */ if (xpcs->info.pma == WX_TXGBE_XPCS_PMA_10G_ID) return txgbe_xpcs_switch_mode(xpcs, interface); + mdio_stat2 = xpcs_read(xpcs, MDIO_MMD_PCS, MDIO_STAT2); + if (mdio_stat2 < 0) + return mdio_stat2; + + /* + * If this XPCS supports 10Gbase-R then that will be the default + * operating mode. There are several interface modes where this default + * is unhelpful. Change the operating mode for interfaces were we know + * the default is wrong, and restore the default otherwise. + */ + if (mdio_stat2 & MDIO_PCS_STAT2_10GBR) { + switch (interface) { + case PHY_INTERFACE_MODE_SGMII: + case PHY_INTERFACE_MODE_1000BASEX: + case PHY_INTERFACE_MODE_2500BASEX: + /* + * Why are we writing MDIO_PCS_CTRL2_TYPE + 1? We want + * the modal behaviour that comes when we pick a + * reserved value. XPCS allocates extra bits to this + * field and allocates values from 15 down so + * MDIO_PCS_CTRL2_TYPE + 1 is the value likely to be + * allocated last (and hopefully never). + */ + ret = xpcs_write(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, + MDIO_PCS_CTRL2_TYPE + 1); + if (ret < 0) + return ret; + break; + default: + ret = xpcs_write(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, + MDIO_PCS_CTRL2_10GBR); + if (ret < 0) + return ret; + break; + } + } + xpcs->interface = interface; return 0; From 5dfc454a1db825bcae7cbae0d0b94aacbc178534 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Wed, 10 Jun 2026 18:01:31 +0800 Subject: [PATCH 10/21] net: pcs: xpcs: configure DW XPCS for USXGMII DW XPCS uses 10GBASE-R PCS as the base mode for USXGMII, but simply selecting PHY_INTERFACE_MODE_USXGMII does not program the vendor USXGMII enable and baud mode bits. This can leave the SerDes block-locked while no USXGMII frames are decoded. Add USXGMII-specific configuration to select the 10GBASE-R PCS type, enable USXGMII, select the 10G USXGMII baud mode and apply the setting with a vendor soft reset. Also advertise the copper link modes carried over USXGMII so phylink can keep 100M/1G/2.5G/5G/10G full-duplex Base-T modes instead of masking them out during validation. Signed-off-by: Junhao Xie --- drivers/net/pcs/pcs-xpcs.c | 49 ++++++++++++++++++++++++++++++++++++++ drivers/net/pcs/pcs-xpcs.h | 5 ++++ 2 files changed, 54 insertions(+) diff --git a/drivers/net/pcs/pcs-xpcs.c b/drivers/net/pcs/pcs-xpcs.c index ba475436100d2..ce09cc0ce2f54 100644 --- a/drivers/net/pcs/pcs-xpcs.c +++ b/drivers/net/pcs/pcs-xpcs.c @@ -27,6 +27,11 @@ static const int xpcs_usxgmii_features[] = { ETHTOOL_LINK_MODE_10000baseKX4_Full_BIT, ETHTOOL_LINK_MODE_10000baseKR_Full_BIT, ETHTOOL_LINK_MODE_2500baseX_Full_BIT, + ETHTOOL_LINK_MODE_100baseT_Full_BIT, + ETHTOOL_LINK_MODE_1000baseT_Full_BIT, + ETHTOOL_LINK_MODE_2500baseT_Full_BIT, + ETHTOOL_LINK_MODE_5000baseT_Full_BIT, + ETHTOOL_LINK_MODE_10000baseT_Full_BIT, __ETHTOOL_LINK_MODE_MASK_NBITS, }; @@ -985,6 +990,44 @@ static int xpcs_config_2500basex(struct dw_xpcs *xpcs) BMCR_SPEED1000); } +static int xpcs_config_usxgmii(struct dw_xpcs *xpcs) +{ + int ret, val; + + /* Select the 10GBASE-R PCS type used as the base for USXGMII */ + ret = xpcs_modify(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, + MDIO_PCS_CTRL2_TYPE, MDIO_PCS_CTRL2_10GBR); + if (ret < 0) + return ret; + + /* Enable USXGMII and select the 10G USXGMII baud mode. The DW XPCS + * does not do this on its own for USXGMII, and without it the SerDes + * block-locks but no USXGMII frames are decoded (RX stays dead). + */ + ret = xpcs_modify_vpcs(xpcs, DW_VR_XS_PCS_DIG_CTRL1, + DW_USXGMII_EN, DW_USXGMII_EN); + if (ret < 0) + return ret; + + ret = xpcs_modify_vpcs(xpcs, DW_VR_XS_PCS_KR_CTRL, + DW_USXG_MODE, DW_USXG_MODE_10G); + if (ret < 0) + return ret; + + /* Apply the configuration with a vendor soft reset and wait for it + * to self-clear. + */ + ret = xpcs_modify_vpcs(xpcs, DW_VR_XS_PCS_DIG_CTRL1, + DW_VR_RST, DW_VR_RST); + if (ret < 0) + return ret; + + return read_poll_timeout(xpcs_read_vpcs, val, + val < 0 || !(val & DW_VR_RST), + 1000, 50000, false, + xpcs, DW_VR_XS_PCS_DIG_CTRL1); +} + static int xpcs_do_config(struct dw_xpcs *xpcs, phy_interface_t interface, const unsigned long *advertising, unsigned int neg_mode) @@ -996,6 +1039,12 @@ static int xpcs_do_config(struct dw_xpcs *xpcs, phy_interface_t interface, if (!compat) return -ENODEV; + if (interface == PHY_INTERFACE_MODE_USXGMII) { + ret = xpcs_config_usxgmii(xpcs); + if (ret < 0) + return ret; + } + ret = xpcs_config_operating_mode(xpcs, compat->an_mode); if (ret < 0) return ret; diff --git a/drivers/net/pcs/pcs-xpcs.h b/drivers/net/pcs/pcs-xpcs.h index 929fa238445ed..484981cf5d53c 100644 --- a/drivers/net/pcs/pcs-xpcs.h +++ b/drivers/net/pcs/pcs-xpcs.h @@ -24,6 +24,11 @@ #define DW_PSEQ_ST GENMASK(4, 2) #define DW_PSEQ_ST_GOOD FIELD_PREP(GENMASK(4, 2), 0x4) +/* VR_XS_PCS_KR_CTRL - selects the USXGMII baud/mode (10G/5G/2.5G) */ +#define DW_VR_XS_PCS_KR_CTRL 0x001c +#define DW_USXG_MODE GENMASK(12, 10) +#define DW_USXG_MODE_10G 0x0 + /* SR_MII */ #define DW_USXGMII_FULL BIT(8) #define DW_USXGMII_SS_MASK (BIT(13) | BIT(6) | BIT(5)) From c3be0f8bf0d683dfed394020906a81178b7df358 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Wed, 10 Jun 2026 17:57:59 +0800 Subject: [PATCH 11/21] net: phy: as21xxx: add support for Aeonsemi AS22XXX PHYs Aeonsemi's AS22XXX is the second generation of the AS21xxx family and keeps the generic PHY ID until firmware has been loaded. Add a dedicated driver entry for it, load firmware from probe, and wait for the IPC channel to become ready before continuing. Handle link, autoneg and speed reporting using the AS22XXX vendor registers. The new status path reads the AN state register for link completion and the vendor speed register for 10M/100M/1G/2.5G/5G/10G resolution while reusing the existing LED control helpers. Signed-off-by: Junhao Xie --- drivers/net/phy/as21xxx.c | 215 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) diff --git a/drivers/net/phy/as21xxx.c b/drivers/net/phy/as21xxx.c index d5738117eca68..76a7b495a3360 100644 --- a/drivers/net/phy/as21xxx.c +++ b/drivers/net/phy/as21xxx.c @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,9 @@ BIT(_n)) #define VEND1_FW_START_ADDR 0x100 +#define AS22XXX_AN_STATES1 0x8005 +#define AS22XXX_AN_STATES1_ARB_MASK GENMASK(15, 12) +#define AS22XXX_LINK_GOOD 9 #define VEND1_GLB_REG_MDIO_INDIRECT_ADDRCMD 0x101 #define VEND1_GLB_REG_MDIO_INDIRECT_LOAD 0x102 @@ -139,6 +143,7 @@ #define PHY_ID_AS21510PB1 0x75009472 #define PHY_ID_AS21210JB1 0x75009482 #define PHY_ID_AS21210PB1 0x75009492 +#define PHY_ID_AS22XXX 0x750094a1 #define PHY_VENDOR_AEONSEMI 0x75009400 #define AEON_MAX_LEDS 5 @@ -768,6 +773,139 @@ static int as21xxx_read_status(struct phy_device *phydev) return 0; } +static int as22xxx_read_link(struct phy_device *phydev, int *bmcr) +{ + int status = 0; + bool link_up; + + *bmcr = phy_read_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_BMCR); + if (*bmcr < 0) + return *bmcr; + + if (*bmcr & BMCR_ANRESTART) + goto done; + + status = phy_read_mmd(phydev, MDIO_MMD_AN, AS22XXX_AN_STATES1); + if (status < 0) + return status; + +done: + link_up = FIELD_GET(AS22XXX_AN_STATES1_ARB_MASK, status) == + AS22XXX_LINK_GOOD; + phydev->link = link_up; + phydev->autoneg_complete = link_up; + + if (phydev->autoneg == AUTONEG_ENABLE && !phydev->autoneg_complete) + phydev->link = 0; + + return 0; +} + +static int as22xxx_read_lpa(struct phy_device *phydev) +{ + int lpa, ret; + + if (phydev->autoneg == AUTONEG_ENABLE && !phydev->autoneg_complete) { + mii_stat1000_mod_linkmode_lpa_t(phydev->lp_advertising, 0); + mii_lpa_mod_linkmode_lpa_t(phydev->lp_advertising, 0); + return 0; + } + + ret = as21xxx_read_c22_lpa(phydev); + if (ret) + return ret; + + lpa = phy_read_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_LPA); + if (lpa < 0) + return lpa; + + mii_lpa_mod_linkmode_lpa_t(phydev->lp_advertising, lpa); + + lpa = phy_read_mmd(phydev, MDIO_MMD_AN, MDIO_AN_10GBT_STAT); + if (lpa < 0) + return lpa; + + mii_10gbt_stat_mod_linkmode_lpa_t(phydev->lp_advertising, lpa); + + return 0; +} + +static void as22xxx_read_speed(struct phy_device *phydev, int bmcr) +{ + int speed; + + speed = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_SPEED_STATUS); + if (speed < 0) + return; + + speed &= VEND1_SPEED_MASK; + if (speed == VEND1_SPEED_10000) { + phydev->speed = SPEED_10000; + phydev->duplex = DUPLEX_FULL; + } else if (speed == VEND1_SPEED_5000) { + phydev->speed = SPEED_5000; + phydev->duplex = DUPLEX_FULL; + } else if (speed == VEND1_SPEED_2500) { + phydev->speed = SPEED_2500; + phydev->duplex = DUPLEX_FULL; + } else if (speed == VEND1_SPEED_1000) { + phydev->speed = SPEED_1000; + if (bmcr & BMCR_FULLDPLX) + phydev->duplex = DUPLEX_FULL; + else + phydev->duplex = DUPLEX_HALF; + } else if (speed == VEND1_SPEED_100) { + phydev->speed = SPEED_100; + if (bmcr & BMCR_FULLDPLX) + phydev->duplex = DUPLEX_FULL; + else + phydev->duplex = DUPLEX_HALF; + } else { + phydev->speed = SPEED_10; + phydev->duplex = DUPLEX_FULL; + } +} + +static int as22xxx_read_status(struct phy_device *phydev) +{ + int bmcr, old_link = phydev->link; + int ret; + + ret = as22xxx_read_link(phydev, &bmcr); + if (ret) + return ret; + + if (phydev->autoneg == AUTONEG_ENABLE && old_link && phydev->link) + return 0; + + phydev->speed = SPEED_UNKNOWN; + phydev->duplex = DUPLEX_UNKNOWN; + phydev->pause = 0; + phydev->asym_pause = 0; + + if (phydev->autoneg == AUTONEG_ENABLE) { + ret = genphy_c45_read_lpa(phydev); + if (ret) + return ret; + + ret = as22xxx_read_lpa(phydev); + if (ret) + return ret; + + if (phydev->autoneg_complete) { + as22xxx_read_speed(phydev, bmcr); + phy_resolve_aneg_linkmode(phydev); + } + } else { + linkmode_zero(phydev->lp_advertising); + as22xxx_read_speed(phydev, bmcr); + } + + return 0; +} + static int as21xxx_led_brightness_set(struct phy_device *phydev, u8 index, enum led_brightness value) { @@ -943,6 +1081,71 @@ static int as21xxx_match_phy_device(struct phy_device *phydev, return ret; } +static int as22xxx_match_phy_device(struct phy_device *phydev, + const struct phy_driver *phydrv) +{ + u32 phy_id; + int ret; + + ret = phy_read_mmd(phydev, MDIO_MMD_PMAPMD, MII_PHYSID1); + if (ret < 0) + return ret; + phy_id = ret << 16; + + ret = phy_read_mmd(phydev, MDIO_MMD_PMAPMD, MII_PHYSID2); + if (ret < 0) + return ret; + phy_id |= ret; + + return phy_id == PHY_ID_AS22XXX; +} + +static int as22xxx_probe(struct phy_device *phydev) +{ + struct as21xxx_priv *priv; + int ret; + + /* Gen2 keeps its generic PHY ID before and after firmware load and may + * expose only PMA/PMD initially. Force the MMD bitmap used by generic C45 + * helpers, then load firmware from probe and wait for IPC to come alive. + */ + phydev->c45_ids.mmds_present |= MDIO_DEVS_PMAPMD | MDIO_DEVS_PCS | + MDIO_DEVS_AN; + + priv = devm_kzalloc(&phydev->mdio.dev, + sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + phydev->priv = priv; + + ret = devm_mutex_init(&phydev->mdio.dev, + &priv->ipc_lock); + if (ret) + return ret; + + ret = aeon_firmware_load(phydev); + if (ret) + return ret; + + ret = read_poll_timeout(aeon_ipc_sync_parity, ret, !ret, + 0, 10000000, false, phydev, priv); + if (ret) { + phydev_err(phydev, "timed out waiting for firmware boot\n"); + return ret; + } + + ret = aeon_ipc_get_fw_version(phydev); + if (ret) + return ret; + + ret = phy_set_bits_mmd(phydev, MDIO_MMD_VEND1, VEND1_PTP_CLK, + VEND1_PTP_CLK_EN); + if (ret) + return ret; + + return 0; +} + static struct phy_driver as21xxx_drivers[] = { { /* PHY expose in C45 as 0x7500 0x9410 @@ -1074,6 +1277,18 @@ static struct phy_driver as21xxx_drivers[] = { .led_hw_control_get = as21xxx_led_hw_control_get, .led_polarity_set = as21xxx_led_polarity_set, }, + { + PHY_ID_MATCH_EXACT(PHY_ID_AS22XXX), + .name = "Aeonsemi AS22XXX", + .probe = as22xxx_probe, + .match_phy_device = as22xxx_match_phy_device, + .read_status = as22xxx_read_status, + .led_brightness_set = as21xxx_led_brightness_set, + .led_hw_is_supported = as21xxx_led_hw_is_supported, + .led_hw_control_set = as21xxx_led_hw_control_set, + .led_hw_control_get = as21xxx_led_hw_control_get, + .led_polarity_set = as21xxx_led_polarity_set, + }, }; module_phy_driver(as21xxx_drivers); From 3de70e377fc10e8a37ddf43e58cf1e0c5bff770a Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Thu, 30 Jul 2026 17:18:50 +0800 Subject: [PATCH 12/21] misc: tc956x_pci: gate ASPM L1 disablement on DT quirk Disable ASPM L1 and its substates only when the PCI function node sets `tc956x,aspm-quirk`, limiting the shared-clock workaround to affected boards. Document the opt-in property in the TC956x binding while keeping the AXI rate and lane-strap diagnostics unchanged. Signed-off-by: Junhao Xie --- .../bindings/net/toshiba,tc956x-dwmac.yaml | 7 +++++++ drivers/misc/tc956x_pci.c | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/Documentation/devicetree/bindings/net/toshiba,tc956x-dwmac.yaml b/Documentation/devicetree/bindings/net/toshiba,tc956x-dwmac.yaml index 37dca8c78021e..610bbf03efc9a 100644 --- a/Documentation/devicetree/bindings/net/toshiba,tc956x-dwmac.yaml +++ b/Documentation/devicetree/bindings/net/toshiba,tc956x-dwmac.yaml @@ -35,6 +35,13 @@ properties: gpio-controller: true + tc956x,aspm-quirk: + type: boolean + description: + Disable ASPM L1 and its substates on this PCI function's endpoint link. + Set this only on platforms where TC956x L1 entry gates shared clocks and + stalls a neighbouring device. + # We can't allOf reference Ethernet-controller.yaml because we end up with # contradictory $nodename rules (`ethernet@` versus `pci@`). Happily only a # small number of the properties are useful on TC956x so we can just reference diff --git a/drivers/misc/tc956x_pci.c b/drivers/misc/tc956x_pci.c index b6f022d52fbc9..aac276718d4f2 100644 --- a/drivers/misc/tc956x_pci.c +++ b/drivers/misc/tc956x_pci.c @@ -72,6 +72,11 @@ #define NCID_OFFSET 0x0000 #define NCID_REV_ID_MASK GENMASK(7, 0) +#define NMODESTS_OFFSET 0x0004 +#define NMODESTS_MODE2 BIT(10) /* 0 = USP x4, 1 = USP x2 */ +#define NBUSCTRL_OFFSET 0x1014 +#define NBUSCTRL_FREQ BIT(16) /* 0 = 250 MHz, 1 = 125 MHz */ + /* Reset and clock register offsets. MAC resets and clocks are controlled * by bits in register 0 for MAC0, register 1 for MAC1. Other non-MAC * resets and clocks (whose IDs are defined here) are controlled by bits @@ -680,6 +685,16 @@ tc956x_function_probe(struct pci_dev *pdev, const struct pci_device_id *id) pci_set_master(pdev); + if (of_property_present(dev->of_node, "tc956x,aspm-quirk")) { + ret = pci_disable_link_state(pdev, PCIE_LINK_STATE_L1 | + PCIE_LINK_STATE_L1_1 | + PCIE_LINK_STATE_L1_2 | + PCIE_LINK_STATE_L1_1_PCIPM | + PCIE_LINK_STATE_L1_2_PCIPM); + if (ret) + dev_warn(dev, "failed to disable ASPM L1: %d\n", ret); + } + /* Function 1 gets -EPROBE_DEFER until function 0 sets platform data */ chip = chip_get(pdev); if (IS_ERR(chip)) From 6de1ceac16e32f57ae756347ededfe0a8bb3d5a0 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Thu, 30 Jul 2026 17:18:50 +0800 Subject: [PATCH 13/21] misc: tc956x_pci: allocate all 32 MSI vectors MSIGEN can steer each of its interrupt sources to any of 32 MSI vectors, but only if the host grants more than one, so ask for all 32 instead of one and hand the count to the auxiliary device. The vector number replaces the low five bits of the MSI data, so a smaller and less strictly aligned allocation risks having those bits overwritten. Signed-off-by: Junhao Xie --- drivers/misc/tc956x_pci.c | 13 ++++++++++--- include/soc/toshiba/tc956x-dwmac.h | 4 +++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/drivers/misc/tc956x_pci.c b/drivers/misc/tc956x_pci.c index aac276718d4f2..be1756f6ce780 100644 --- a/drivers/misc/tc956x_pci.c +++ b/drivers/misc/tc956x_pci.c @@ -147,6 +147,7 @@ enum clock_id { */ #define EMAC_CTL_OFFSET(_mac_id) ((_mac_id) ? 0x1074 : 0x1070) #define MSIGEN_OFFSET(_mac_id) ((_mac_id) ? 0xf100 : 0xf000) +#define TC956X_MSI_VECTORS 32 #define DWMAC_OFFSET(_mac_id) ((_mac_id) ? 0x48000 : 0x40000) /* @@ -390,7 +391,8 @@ static int chip_gpio_adev_add(struct tc956x_chip *chip) /* The two embedded XGMAC controllers have an auxiliary device driver */ static int function_xgmac_adev_add(struct pci_dev *pdev, struct tc956x_chip *chip, - unsigned int msigen_irq) + unsigned int msigen_irq, + unsigned int msigen_nvec) { u8 mac_id = PCI_FUNC(pdev->devfn); struct device *dev = &pdev->dev; @@ -417,6 +419,7 @@ static int function_xgmac_adev_add(struct pci_dev *pdev, data->chip = chip; data->msigen = sfr + MSIGEN_OFFSET(mac_id); data->msigen_irq = msigen_irq; + data->msigen_nvec = msigen_nvec; data->emac = sfr + DWMAC_OFFSET(mac_id); data->emac_ctl = sfr + EMAC_CTL_OFFSET(mac_id); data->sfr = sfr; @@ -673,6 +676,7 @@ tc956x_function_probe(struct pci_dev *pdev, const struct pci_device_id *id) struct device *dev = &pdev->dev; struct tc956x_chip *chip; unsigned int msigen_irq; + unsigned int msigen_nvec; int ret; /* Despite being a PCI device, we require devicetree */ @@ -701,16 +705,19 @@ tc956x_function_probe(struct pci_dev *pdev, const struct pci_device_id *id) return dev_err_probe(dev, PTR_ERR(chip), "failed to get chip\n"); /* We called pcim_enable_device() so this will be freed automatically */ - ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); + ret = pci_alloc_irq_vectors(pdev, 1, TC956X_MSI_VECTORS, PCI_IRQ_MSI); if (ret < 1) return dev_err_probe(dev, ret ? : -EIO, "failed to allocate IRQ vectors\n"); + msigen_nvec = ret; ret = pci_irq_vector(pdev, 0); if (ret < 1) return dev_err_probe(dev, ret ? : -EIO, "failed to get IRQ\n"); msigen_irq = ret; + dev_info(dev, "%u MSI vector(s), base IRQ %u\n", msigen_nvec, msigen_irq); + ret = chip_init(chip, pdev); if (ret) return dev_err_probe(dev, ret, "failed to initialize chip\n"); @@ -718,7 +725,7 @@ tc956x_function_probe(struct pci_dev *pdev, const struct pci_device_id *id) /* We're ready; the other function can now probe */ dev->platform_data = chip; - ret = function_xgmac_adev_add(pdev, chip, msigen_irq); + ret = function_xgmac_adev_add(pdev, chip, msigen_irq, msigen_nvec); if (ret) return dev_err_probe(dev, ret, "failed to add xgmap device\n"); diff --git a/include/soc/toshiba/tc956x-dwmac.h b/include/soc/toshiba/tc956x-dwmac.h index 3abec80d012ae..5840c1a9a51d7 100644 --- a/include/soc/toshiba/tc956x-dwmac.h +++ b/include/soc/toshiba/tc956x-dwmac.h @@ -45,7 +45,8 @@ enum tc956x_common_clock_id { * @emac_ctl: I/O mapped address used for eMAC control * @sfr: I/O mapped address used for TC956X SFR access * @msigen: I/O mapped address used by MSIGEN - * @msigen_irq: IRQ number used by MSIGEN + * @msigen_irq: IRQ number of the first MSIGEN vector + * @msigen_nvec: Number of consecutive MSIGEN vectors from @msigen_irq * @rev_id: Chip revision ID (for quirks) * @mac_id: Unique device ID (0 or 1) * @@ -58,6 +59,7 @@ struct tc956x_dwmac_data { void __iomem *sfr; void __iomem *msigen; unsigned int msigen_irq; + unsigned int msigen_nvec; u8 rev_id; u8 mac_id; }; From 7babeedd7bed59fd7e253a9a0897d54d41bb35ef Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Thu, 30 Jul 2026 17:19:49 +0800 Subject: [PATCH 14/21] WIP: net: stmmac: tc956x: reach 10G line rate in both directions Assign each DMA channel a dedicated MSI vector to distribute NAPI load. Keep hardware TSO disabled because software segmentation is faster. Increase the RX FIFO from 8KiB to a safe 32KiB. Set clk_ref_rate so RX interrupt coalescing works correctly. Fix RX queue-to-DMA mapping and keep one RX/TX queue without RSS. Do not disable chip-wide SerDes clocks when one MAC stops. Keep the implementation compatible with the v7.0 stmmac APIs: fix_mac_speed() takes no phy_interface_t argument, so read the interface back from the platform data, while default_an_inband lives in plat->mdio_bus_data rather than plat. Signed-off-by: Junhao Xie --- .../ethernet/stmicro/stmmac/dwmac-tc956x.c | 185 ++++++++++++++---- 1 file changed, 151 insertions(+), 34 deletions(-) diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c index 3d17d1797635a..72bfa21281c4c 100644 --- a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c +++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c @@ -33,9 +33,12 @@ #define DRIVER_NAME "dwmac-tc956x" #define TC956X_PTP_CLOCK_RATE (250 * HZ_PER_MHZ) +#define TC956X_MAC_CLOCK_RATE (125 * HZ_PER_MHZ) #define TC956X_RX_FIFO_KB 32 /* Shared by all RX queues */ #define TC956X_TX_FIFO_KB 8 /* Shared by all TX queues */ +#define TC956X_RX_QUEUES 1 +#define TC956X_TX_QUEUES 1 /* Fields and values for the EMACTL registers */ #define EMAC_SP_SEL_MASK GENMASK(3, 0) @@ -55,9 +58,12 @@ /* MSIGEN Registers */ #define MSI_OUT_EN_OFFSET 0x0000 +#define MSI_MASK_SET_OFFSET 0x0008 #define MSI_MASK_CLR_OFFSET 0x000c -#define MSI_MASK_VALUE BIT(0) #define MSI_INT_STS_OFFSET 0x0010 +#define MSI_VECT_SET_OFFSET(_src) (0x0020 + ((_src) / 4) * 4) +#define MSI_VECT_SET_SHIFT(_src) (((_src) % 4) * 8) +#define MSI_VECT_SET_MASK GENMASK(4, 0) enum msigen_hwirq { HWIRQ_LPI = 0, @@ -72,6 +78,11 @@ enum msigen_hwirq { }; #define HWIRQ_COUNT 25 +#define TC956X_DMA_CHANS 8 +#define MSI_VEC_MISC 1 +#define MSI_VEC_TX(_ch) (2 + (_ch)) +#define MSI_VEC_RX(_ch) (2 + TC956X_DMA_CHANS + (_ch)) +#define MSI_VEC_COUNT (2 + 2 * TC956X_DMA_CHANS) /* Offset to the XPCS memory block, relative to the EMAC address range */ #define DWMAC_XPCS_OFFSET 0x3a00 @@ -107,6 +118,12 @@ enum msigen_hwirq { #define COMM_CFG_WRITE_DATA_MASK GENMASK(7, 0) #define WRITE_DATA_VALUE 0x04 /* Power-on value */ +struct tc956x_msi_vec { + struct tc956x_data *td; + u32 src_mask; /* MSIGEN sources on this vector */ + u8 vec; /* MSI vector number */ +}; + /** * struct tc956x_data - Toshiba-specific platform data * @dev: Device pointer @@ -125,6 +142,9 @@ struct tc956x_data { struct tc956x_dwmac_data *auxbus_data; struct plat_stmmacenet_data *plat; + struct tc956x_msi_vec msi_vec[MSI_VEC_COUNT]; + unsigned int msi_vec_used; + /* These three fields are used by the plat_stmmacenet_data structure */ struct stmmac_dma_cfg dma_cfg; struct stmmac_mdio_bus_data mdio_bus_data; @@ -171,33 +191,95 @@ static const struct regmap_config xpcs_regmap_config = { static void tc956x_msigen_irq_handler(struct irq_desc *desc) { - struct irq_domain *irq_domain = irq_desc_get_handler_data(desc); + struct tc956x_msi_vec *mv = irq_desc_get_handler_data(desc); struct irq_chip *chip = irq_desc_get_chip(desc); struct irq_chip_generic *gc; unsigned long status; unsigned int hwirq; - gc = irq_get_domain_generic_chip(irq_domain, 0); + gc = irq_get_domain_generic_chip(mv->td->irq_domain, 0); chained_irq_enter(chip, desc); - status = irq_reg_readl(gc, MSI_INT_STS_OFFSET); + status = irq_reg_readl(gc, MSI_INT_STS_OFFSET) & mv->src_mask; for_each_set_bit(hwirq, &status, HWIRQ_COUNT) - generic_handle_domain_irq(irq_domain, hwirq); + generic_handle_domain_irq(mv->td->irq_domain, hwirq); /* * Clear the MSI flag. Most interrupts within TC956X are level-high * type. If any interrupts are still asserted then clearing this flag * will cause the (edge-triggered) MSI to be regenerated. */ - irq_reg_writel(gc, MSI_MASK_VALUE, MSI_MASK_CLR_OFFSET); + irq_reg_writel(gc, BIT(mv->vec), MSI_MASK_CLR_OFFSET); chained_irq_exit(chip, desc); } +static void tc956x_msigen_route(struct irq_chip_generic *gc, + const u8 *src_vec, unsigned int nsrc) +{ + unsigned int src, reg; + u32 val; + + irq_reg_writel(gc, 0, MSI_OUT_EN_OFFSET); + + for (reg = 0; reg < DIV_ROUND_UP(nsrc, 4); reg++) { + val = 0; + for (src = reg * 4; src < min(nsrc, (reg + 1) * 4); src++) + val |= (src_vec[src] & MSI_VECT_SET_MASK) << + MSI_VECT_SET_SHIFT(src); + irq_reg_writel(gc, val, MSI_VECT_SET_OFFSET(reg * 4)); + } +} + +static void tc956x_msigen_plan(struct tc956x_data *td, u8 *src_vec) +{ + unsigned int src, ch; + + if (td->auxbus_data->msigen_nvec < MSI_VEC_COUNT) { + for (src = 0; src < HWIRQ_COUNT; src++) + src_vec[src] = 0; + td->msi_vec[0] = (struct tc956x_msi_vec){ + .td = td, .vec = 0, + .src_mask = GENMASK(HWIRQ_COUNT - 1, 0), + }; + td->msi_vec_used = 1; + return; + } + + for (src = 0; src < HWIRQ_COUNT; src++) + src_vec[src] = MSI_VEC_MISC; + for (ch = 0; ch < TC956X_DMA_CHANS; ch++) { + src_vec[HWIRQ_TX0 + ch] = MSI_VEC_TX(ch); + src_vec[HWIRQ_RX0 + ch] = MSI_VEC_RX(ch); + } + + td->msi_vec[0] = (struct tc956x_msi_vec){ + .td = td, .vec = MSI_VEC_MISC, + .src_mask = GENMASK(HWIRQ_EVENT, HWIRQ_LPI) | + GENMASK(HWIRQ_COUNT - 1, HWIRQ_XPCS), + }; + td->msi_vec_used = 1; + + /* Interleaved so that TX and RX of the same channel index differ */ + for (ch = 0; ch < TC956X_DMA_CHANS; ch++) { + td->msi_vec[td->msi_vec_used++] = (struct tc956x_msi_vec){ + .td = td, .vec = MSI_VEC_TX(ch), + .src_mask = BIT(HWIRQ_TX0 + ch), + }; + td->msi_vec[td->msi_vec_used++] = (struct tc956x_msi_vec){ + .td = td, .vec = MSI_VEC_RX(ch), + .src_mask = BIT(HWIRQ_RX0 + ch), + }; + } +} + static int tc956x_msigen_irq_chip_init(struct irq_chip_generic *gc) { struct tc956x_data *td = gc->domain->host_data; + u8 src_vec[HWIRQ_COUNT]; + u32 vec_used = 0; + unsigned int i; gc->reg_base = td->auxbus_data->msigen; gc->chip_types[0].regs.mask = MSI_OUT_EN_OFFSET; @@ -207,6 +289,15 @@ static int tc956x_msigen_irq_chip_init(struct irq_chip_generic *gc) /* Disable all interrupts */ irq_reg_writel(gc, 0, MSI_OUT_EN_OFFSET); + tc956x_msigen_plan(td, src_vec); + tc956x_msigen_route(gc, src_vec, HWIRQ_COUNT); + + for (i = 0; i < td->msi_vec_used; i++) + vec_used |= BIT(td->msi_vec[i].vec); + + irq_reg_writel(gc, ~vec_used & ~BIT(0), MSI_MASK_SET_OFFSET); + irq_reg_writel(gc, vec_used, MSI_MASK_CLR_OFFSET); + return 0; } @@ -218,23 +309,41 @@ static void tc956x_msigen_irq_chip_exit(struct irq_chip_generic *gc) static int tc956x_msigen_irq_domain_init(struct irq_domain *irq_domain) { struct tc956x_data *td = irq_domain->host_data; - unsigned int cpu, last_cpu = 0, prev_cpu = 0; - - for_each_cpu(cpu, cpu_online_mask) { - prev_cpu = last_cpu; - last_cpu = cpu; + unsigned int i, cpu; + int last = -1; + + if (td->msi_vec_used == 1) { + for_each_cpu(cpu, cpu_online_mask) + last = cpu; + irq_set_affinity_and_hint(td->auxbus_data->msigen_irq, + cpumask_of(last > 0 ? last - 1 : 0)); + irq_set_chained_handler_and_data(td->auxbus_data->msigen_irq, + tc956x_msigen_irq_handler, + &td->msi_vec[0]); + dev_info(td->dev, "%u MSI vector(s), sharing one interrupt\n", + td->auxbus_data->msigen_nvec); + return 0; } - if (cpumask_weight(cpu_online_mask) > 1) - cpu = prev_cpu; - else - cpu = last_cpu; + cpu = cpumask_first(cpu_online_mask); + for (i = 0; i < td->msi_vec_used; i++) { + unsigned int irq = td->auxbus_data->msigen_irq + + td->msi_vec[i].vec; - irq_set_affinity_and_hint(td->auxbus_data->msigen_irq, cpumask_of(cpu)); + irq_set_chained_handler_and_data(irq, + tc956x_msigen_irq_handler, + &td->msi_vec[i]); - irq_set_chained_handler_and_data(td->auxbus_data->msigen_irq, - tc956x_msigen_irq_handler, - irq_domain); + /* Leave the shared low-rate vector wherever it lands */ + if (td->msi_vec[i].vec == MSI_VEC_MISC) + continue; + + irq_set_affinity_and_hint(irq, cpumask_of(cpu)); + cpu = cpumask_next_wrap(cpu, cpu_online_mask); + } + + dev_info(td->dev, "%u MSI vectors, one per DMA channel\n", + td->auxbus_data->msigen_nvec); return 0; } @@ -242,11 +351,19 @@ static int tc956x_msigen_irq_domain_init(struct irq_domain *irq_domain) static void tc956x_msigen_irq_domain_exit(struct irq_domain *irq_domain) { struct tc956x_data *td = irq_domain->host_data; + unsigned int i; + + for (i = 0; i < td->msi_vec_used; i++) { + unsigned int irq = td->auxbus_data->msigen_irq; - irq_set_chained_handler_and_data(td->auxbus_data->msigen_irq, - NULL, NULL); + if (td->msi_vec_used > 1) + irq += td->msi_vec[i].vec; + + irq_set_chained_handler_and_data(irq, NULL, NULL); + } } + /* We have one IRQ chip instance with 25 IRQs in its domain */ static struct irq_domain * tc956x_msigen_irq_domain_instantiate(struct tc956x_data *td) @@ -422,9 +539,6 @@ static void tc956x_mac_disable(struct tc956x_data *td) if (plat->phy_interface == PHY_INTERFACE_MODE_USXGMII) { tc956x_clock_disable(chip, id, MAC_CLOCK_125M); tc956x_clock_disable(chip, id, MAC_CLOCK_312_5M); - tc956x_common_clock_disable(chip, COMMON_CLOCK_REFCLK); - tc956x_common_clock_disable(chip, COMMON_CLOCK_SGMII); - tc956x_common_clock_disable(chip, COMMON_CLOCK_PLL); } } @@ -573,9 +687,10 @@ static struct phylink_pcs *tc956x_select_pcs(struct stmmac_priv *priv, static void tc956x_fix_mac_speed(void *bsp_priv, int speed, unsigned int mode) { struct tc956x_data *td = bsp_priv; + phy_interface_t interface = td->plat->phy_interface; - tc956x_mac_configure(td, td->plat->phy_interface, speed); - if (td->plat->phy_interface == PHY_INTERFACE_MODE_USXGMII) + tc956x_mac_configure(td, interface, speed); + if (interface == PHY_INTERFACE_MODE_USXGMII) return; tc956x_pma_init(td); @@ -713,25 +828,26 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) * TC956X partitions FIFO per function; exposing multiple stmmac queues makes * the core split this budget and leaves the active queue too small for 10G. */ - plat->rx_queues_to_use = 1; - plat->tx_queues_to_use = 1; + plat->rx_queues_to_use = TC956X_RX_QUEUES; + plat->tx_queues_to_use = TC956X_TX_QUEUES; + plat->rss_en = TC956X_RX_QUEUES > 1; /* * Oversized FIFOs result in reduced performance in bandwidth tests. * Limit them to 8KiB per queue, or the total available. */ - plat->tx_fifo_size = - min(TC956X_TX_FIFO_KB, 8 * plat->tx_queues_to_use) * SZ_1K; - plat->rx_fifo_size = - min(TC956X_RX_FIFO_KB, 8 * plat->rx_queues_to_use) * SZ_1K; + plat->tx_fifo_size = TC956X_TX_FIFO_KB * SZ_1K; + plat->rx_fifo_size = TC956X_RX_FIFO_KB * SZ_1K; plat->host_dma_width = 36; plat->rx_sched_algorithm = MTL_RX_ALGORITHM_SP; plat->tx_sched_algorithm = MTL_TX_ALGORITHM_WRR; /* Default RX chan is set to queue index (0..rx_queues_to_use-1) */ - for (i = 0; i < plat->rx_queues_to_use; i++) + for (i = 0; i < plat->rx_queues_to_use; i++) { plat->rx_queues_cfg[i].mode_to_use = MTL_QUEUE_DCB; + plat->rx_queues_cfg[i].chan = i; + } for (i = 0; i < plat->tx_queues_to_use; i++) { plat->tx_queues_cfg[i].weight = 12; @@ -751,6 +867,7 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) plat->bsp_priv = td; plat->clk_ptp_rate = TC956X_PTP_CLOCK_RATE; + plat->clk_ref_rate = TC956X_MAC_CLOCK_RATE; /* AXI Configuration */ axi = &td->axi; @@ -762,7 +879,7 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) plat->axi = axi; plat->mac_port_sel_speed = speed; - plat->flags = STMMAC_FLAG_MULTI_MSI_EN | STMMAC_FLAG_TSO_EN; + plat->flags = STMMAC_FLAG_MULTI_MSI_EN; td->plat = plat; From 00b0e51cdf12659087563cb82c1d7c61d56d1087 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Thu, 30 Jul 2026 17:20:18 +0800 Subject: [PATCH 15/21] net: stmmac: only pin segmented traffic to queue 0 when TSO is enabled stmmac_select_queue() sends every skb with a TCP or UDP GSO type to TX queue 0, on the grounds that queue 0 is the one queue guaranteed to be TSO capable. That is sound while the MAC does the segmentation, but queue selection happens before the stack segments the skb, so on a device with hardware TSO disabled it means *all* TCP traffic is funnelled onto a single ring and any multi-queue configuration is pointless. Only force queue 0 when hardware segmentation is actually in use. With hardware TSO off the stack splits the skb after this point anyway, so there is nothing left to confine. On TC956X, which caps hardware TSO at ~6.2 Gbit/s and so runs with it disabled, this lets TX interrupts spread across CPUs and four TX queues reach 9.41 Gbit/s instead of 2.5. Signed-off-by: Junhao Xie --- drivers/net/ethernet/stmicro/stmmac/stmmac_main.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c b/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c index a7bb2f3e02047..abea939fcf967 100644 --- a/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c +++ b/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c @@ -6390,6 +6390,7 @@ static int stmmac_setup_tc(struct net_device *ndev, enum tc_setup_type type, static u16 stmmac_select_queue(struct net_device *dev, struct sk_buff *skb, struct net_device *sb_dev) { + struct stmmac_priv *priv = netdev_priv(dev); int gso = skb_shinfo(skb)->gso_type; if (gso & (SKB_GSO_TCPV4 | SKB_GSO_TCPV6 | SKB_GSO_UDP_L4)) { @@ -6399,7 +6400,8 @@ static u16 stmmac_select_queue(struct net_device *dev, struct sk_buff *skb, * because if TSO/USO is supported then at least this * one will be capable. */ - return 0; + if (priv->dma_cap.tsoen && priv->plat->flags & STMMAC_FLAG_TSO_EN) + return 0; } return netdev_pick_tx(dev, skb, NULL) % dev->real_num_tx_queues; From 4a54beb62ed6319182cef8ab6c31811b8678c372 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Mon, 10 Aug 2026 23:36:39 +0800 Subject: [PATCH 16/21] dt-bindings: net: aeonsemi: add AS22xxx switch quirk AS22xxx PHYs can switch their system-side interface between SGMII and USXGMII when paired with a capable MAC. Document an opt-in property for that wiring and add the AS22010x PHY identifier to the binding. Signed-off-by: Junhao Xie --- .../devicetree/bindings/net/aeonsemi,as21xxx.yaml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/net/aeonsemi,as21xxx.yaml b/Documentation/devicetree/bindings/net/aeonsemi,as21xxx.yaml index 69eb29dc4d7b8..a97871d8785f9 100644 --- a/Documentation/devicetree/bindings/net/aeonsemi,as21xxx.yaml +++ b/Documentation/devicetree/bindings/net/aeonsemi,as21xxx.yaml @@ -4,7 +4,7 @@ $id: http://devicetree.org/schemas/net/aeonsemi,as21xxx.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: Aeonsemi AS21XXX Ethernet PHY +title: Aeonsemi AS21XXX and AS22XXX Ethernet PHY maintainers: - Christian Marangi @@ -54,6 +54,7 @@ select: - ethernet-phy-id7500.9472 - ethernet-phy-id7500.9482 - ethernet-phy-id7500.9492 + - ethernet-phy-id7500.94a1 required: - compatible @@ -65,6 +66,14 @@ properties: description: specify the name of PHY firmware to load maxItems: 1 + sgmii-usxgmii-switch-quirk: + type: boolean + description: + Enable AS22xxx system-side switching between SGMII at speeds up to + 1 Gbit/s and USXGMII at speeds of 2.5 Gbit/s and above. The attached MAC + node must also contain this property. Without it, the PHY does not change + its system-side interface in response to copper link speed. + required: - compatible - reg @@ -73,7 +82,9 @@ if: properties: compatible: contains: - const: ethernet-phy-id7500.9410 + enum: + - ethernet-phy-id7500.9410 + - ethernet-phy-id7500.94a1 then: required: - firmware-name From cb09885b0a8876ffe7e4740b7fb0b98ee5632b32 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Mon, 10 Aug 2026 23:36:50 +0800 Subject: [PATCH 17/21] dt-bindings: net: toshiba: add switch quirk TC956x can coordinate runtime SGMII and USXGMII changes with an attached AS22xxx PHY. Document the opt-in property so existing platforms keep the interface selected by phy-mode. Signed-off-by: Junhao Xie --- .../devicetree/bindings/net/toshiba,tc956x-dwmac.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/devicetree/bindings/net/toshiba,tc956x-dwmac.yaml b/Documentation/devicetree/bindings/net/toshiba,tc956x-dwmac.yaml index 610bbf03efc9a..e371d61086bbc 100644 --- a/Documentation/devicetree/bindings/net/toshiba,tc956x-dwmac.yaml +++ b/Documentation/devicetree/bindings/net/toshiba,tc956x-dwmac.yaml @@ -42,6 +42,14 @@ properties: Set this only on platforms where TC956x L1 entry gates shared clocks and stalls a neighbouring device. + sgmii-usxgmii-switch-quirk: + type: boolean + description: + Enable runtime switching between SGMII at speeds up to 1 Gbit/s and + USXGMII at speeds of 2.5 Gbit/s and above. The attached AS22xxx PHY node + must also contain this property. Without it, the interface remains in + the mode selected by phy-mode. + # We can't allOf reference Ethernet-controller.yaml because we end up with # contradictory $nodename rules (`ethernet@` versus `pci@`). Happily only a # small number of the properties are useful on TC956x so we can just reference From 9cd15a137c3a15151e02e89876f55ba4885b4002 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Mon, 10 Aug 2026 23:37:02 +0800 Subject: [PATCH 18/21] WIP: net: pcs: xpcs: support QPS615 interface switching QPS615 needs explicit PCS type, USXGMII control and Clause 37 programming when changing its serial interface. Add per-instance hardware, out-of-band control and SGMII+ state so the switch quirk and fixed-SGMII users do not share incompatible negotiation policy. Use the documented QPS615 encodings for SGMII, SGMII+ and USXGMII while leaving generic DesignWare XPCS instances unchanged. Signed-off-by: Junhao Xie --- drivers/net/pcs/pcs-xpcs.c | 269 +++++++++++++++++++++++++++++------ drivers/net/pcs/pcs-xpcs.h | 16 ++- include/linux/pcs/pcs-xpcs.h | 2 + 3 files changed, 239 insertions(+), 48 deletions(-) diff --git a/drivers/net/pcs/pcs-xpcs.c b/drivers/net/pcs/pcs-xpcs.c index ce09cc0ce2f54..3f2ce6196c9d5 100644 --- a/drivers/net/pcs/pcs-xpcs.c +++ b/drivers/net/pcs/pcs-xpcs.c @@ -19,6 +19,7 @@ #define phylink_pcs_to_xpcs(pl_pcs) \ container_of((pl_pcs), struct dw_xpcs, pcs) +static int xpcs_usra_reset(struct dw_xpcs *xpcs); static const int xpcs_usxgmii_features[] = { ETHTOOL_LINK_MODE_Pause_BIT, ETHTOOL_LINK_MODE_Asym_Pause_BIT, @@ -360,6 +361,33 @@ static int xpcs_read_fault_c73(struct dw_xpcs *xpcs, return 0; } +static int xpcs_usra_reset(struct dw_xpcs *xpcs) +{ + int ret; + int val; + + ret = xpcs_modify_vpcs(xpcs, DW_VR_XS_PCS_DIG_CTRL1, DW_USXGMII_RST, + DW_USXGMII_RST); + if (ret < 0) + return ret; + + ret = read_poll_timeout(xpcs_read_vpcs, val, + val < 0 || !(val & DW_USXGMII_RST), + 100, 20000, false, + xpcs, DW_VR_XS_PCS_DIG_CTRL1); + if (val < 0) + ret = val; + + if (ret) + return ret < 0 ? ret : -ETIMEDOUT; + + val = xpcs_read_vendor(xpcs, MDIO_MMD_PCS, DW_VR_XS_PCS_DIG_STS); + if (val < 0) + return val; + + return 0; +} + static void xpcs_link_up_usxgmii(struct dw_xpcs *xpcs, int speed) { int ret, speed_sel; @@ -392,13 +420,44 @@ static void xpcs_link_up_usxgmii(struct dw_xpcs *xpcs, int speed) if (ret < 0) goto out; - ret = xpcs_modify(xpcs, MDIO_MMD_VEND2, MII_BMCR, DW_USXGMII_SS_MASK, + ret = xpcs_modify(xpcs, MDIO_MMD_VEND2, MII_BMCR, DW_USXGMII_CTRL_MASK, speed_sel | DW_USXGMII_FULL); if (ret < 0) goto out; - ret = xpcs_modify_vpcs(xpcs, MDIO_CTRL1, DW_USXGMII_RST, - DW_USXGMII_RST); + ret = xpcs_modify_vpcs(xpcs, DW_VR_XS_PCS_DIG_CTRL1, + BIT(2), 0); + if (ret < 0) + goto out; + + ret = xpcs_modify(xpcs, MDIO_MMD_VEND2, DW_VR_MII_DIG_CTRL1, + DW_VR_MII_DIG_CTRL1_MAC_AUTO_SW, + 0); + if (ret < 0) + goto out; + + if (xpcs->qps615_oob) { + ret = xpcs_modify(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_CTRL, + DW_VR_MII_PCS_MODE_MASK | + DW_VR_MII_TX_CONFIG_MASK | + DW_VR_MII_AN_INTR_EN, + xpcs->pcs.poll ? 0 : DW_VR_MII_AN_INTR_EN); + if (ret < 0) + goto out; + + ret = xpcs_write(xpcs, MDIO_MMD_VEND2, + DW_VR_MII_AN_INTR_STS, 0); + if (ret < 0) + goto out; + + ret = xpcs_modify(xpcs, MDIO_MMD_VEND2, MII_BMCR, + BMCR_ANENABLE | BMCR_ANRESTART, + BMCR_ANENABLE | BMCR_ANRESTART); + if (ret < 0) + goto out; + } + + ret = xpcs_usra_reset(xpcs); if (ret < 0) goto out; @@ -678,6 +737,10 @@ static unsigned int xpcs_inband_caps(struct phylink_pcs *pcs, struct dw_xpcs *xpcs = phylink_pcs_to_xpcs(pcs); const struct dw_xpcs_compat *compat; + if (interface == PHY_INTERFACE_MODE_USXGMII && + xpcs->qps615_oob) + return LINK_INBAND_DISABLE; + compat = xpcs_find_compat(xpcs, interface); if (!compat) return 0; @@ -720,6 +783,18 @@ static int xpcs_switch_interface_mode(struct dw_xpcs *xpcs, if (mdio_stat2 < 0) return mdio_stat2; + if (xpcs->qps615 && interface != PHY_INTERFACE_MODE_USXGMII) { + ret = xpcs_modify_vpcs(xpcs, DW_VR_XS_PCS_DIG_CTRL1, + DW_USXGMII_EN | BIT(2), 0); + if (ret < 0) + return ret; + + ret = xpcs_modify_vpcs(xpcs, DW_VR_XS_PCS_KR_CTRL, + DW_USXG_MODE | DW_USXG_2PT5G_GMII, 0); + if (ret < 0) + return ret; + } + /* * If this XPCS supports 10Gbase-R then that will be the default * operating mode. There are several interface modes where this default @@ -731,22 +806,30 @@ static int xpcs_switch_interface_mode(struct dw_xpcs *xpcs, case PHY_INTERFACE_MODE_SGMII: case PHY_INTERFACE_MODE_1000BASEX: case PHY_INTERFACE_MODE_2500BASEX: - /* - * Why are we writing MDIO_PCS_CTRL2_TYPE + 1? We want - * the modal behaviour that comes when we pick a - * reserved value. XPCS allocates extra bits to this - * field and allocates values from 15 down so - * MDIO_PCS_CTRL2_TYPE + 1 is the value likely to be - * allocated last (and hopefully never). - */ - ret = xpcs_write(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, - MDIO_PCS_CTRL2_TYPE + 1); + if (xpcs->qps615) { + u16 type = interface == PHY_INTERFACE_MODE_2500BASEX && + !xpcs->qps615_sgmii_plus ? + QPS615_PCS_CTRL2_2500BASEX : + MDIO_PCS_CTRL2_10GBX; + + ret = xpcs_modify(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, + QPS615_PCS_CTRL2_TYPE_MASK, + type); + } else { + ret = xpcs_write(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, + MDIO_PCS_CTRL2_TYPE + 1); + } if (ret < 0) return ret; break; default: - ret = xpcs_write(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, - MDIO_PCS_CTRL2_10GBR); + if (xpcs->qps615) + ret = xpcs_modify(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, + QPS615_PCS_CTRL2_TYPE_MASK, + MDIO_PCS_CTRL2_10GBR); + else + ret = xpcs_write(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, + MDIO_PCS_CTRL2_10GBR); if (ret < 0) return ret; break; @@ -762,6 +845,7 @@ static void xpcs_pre_config(struct phylink_pcs *pcs, phy_interface_t interface) { struct dw_xpcs *xpcs = phylink_pcs_to_xpcs(pcs); const struct dw_xpcs_compat *compat; + bool switch_first; bool force_reset; int ret; @@ -770,6 +854,16 @@ static void xpcs_pre_config(struct phylink_pcs *pcs, phy_interface_t interface) * Clause 37 auto-negotiation when the XPCS switches interface modes. */ force_reset = interface == PHY_INTERFACE_MODE_SGMII; + switch_first = xpcs->qps615 && force_reset; + + if (switch_first) { + ret = xpcs_switch_interface_mode(xpcs, interface); + if (ret) { + dev_err(&xpcs->mdiodev->dev, + "switch interface failed: %pe\n", ERR_PTR(ret)); + return; + } + } if (force_reset || xpcs->need_reset) { compat = xpcs_find_compat(xpcs, interface); @@ -789,10 +883,12 @@ static void xpcs_pre_config(struct phylink_pcs *pcs, phy_interface_t interface) xpcs->need_reset = false; } - ret = xpcs_switch_interface_mode(xpcs, interface); - if (ret) - dev_err(&xpcs->mdiodev->dev, "switch interface failed: %pe\n", - ERR_PTR(ret)); + if (!switch_first) { + ret = xpcs_switch_interface_mode(xpcs, interface); + if (ret) + dev_err(&xpcs->mdiodev->dev, + "switch interface failed: %pe\n", ERR_PTR(ret)); + } } static int xpcs_config_operating_mode(struct dw_xpcs *xpcs, int an_mode) @@ -807,19 +903,20 @@ static int xpcs_config_operating_mode(struct dw_xpcs *xpcs, int an_mode) if (mdio_stat2 < 0) return mdio_stat2; - /* - * If this XPCS supports 10Gbase-R then it will be the default - * which prevents 1000base-X and slower from working correctly. - * - * Why are we writing MDIO_PCS_CTRL2_TYPE + 1? We want the modal - * behaviour that comes when we pick a reserved value. XPCS - * allocates extra bits to this field and allocates values from - * 15 down so MDIO_PCS_CTRL2_TYPE + 1 is the value likely to - * be allocated last (and hopefully never). - */ if (mdio_stat2 & MDIO_PCS_STAT2_10GBR) { - ret = xpcs_write(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, - MDIO_PCS_CTRL2_TYPE + 1); + if (xpcs->qps615) { + u16 type = an_mode == DW_2500BASEX && + !xpcs->qps615_sgmii_plus ? + QPS615_PCS_CTRL2_2500BASEX : + MDIO_PCS_CTRL2_10GBX; + + ret = xpcs_modify(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, + QPS615_PCS_CTRL2_TYPE_MASK, + type); + } else { + ret = xpcs_write(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, + MDIO_PCS_CTRL2_TYPE + 1); + } if (ret < 0) return ret; } @@ -832,9 +929,13 @@ static int xpcs_config_operating_mode(struct dw_xpcs *xpcs, int an_mode) static int xpcs_config_aneg_c37_sgmii(struct dw_xpcs *xpcs, unsigned int neg_mode) { + bool use_inband_status; int ret, mdio_ctrl, tx_conf; u16 mask, val; + use_inband_status = neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED || + xpcs->qps615_oob; + /* For AN for C37 SGMII mode, the settings are :- * 1) VR_MII_MMD_CTRL Bit(12) [AN_ENABLE] = 0b (Disable SGMII AN in case it is already enabled) @@ -864,6 +965,10 @@ static int xpcs_config_aneg_c37_sgmii(struct dw_xpcs *xpcs, return ret; } + if (xpcs->qps615_oob) + mdio_ctrl = (mdio_ctrl & ~DW_USXGMII_CTRL_MASK) | + DW_USXGMII_1000 | DW_USXGMII_FULL; + mask = DW_VR_MII_PCS_MODE_MASK | DW_VR_MII_TX_CONFIG_MASK; val = FIELD_PREP(DW_VR_MII_PCS_MODE_MASK, DW_VR_MII_PCS_MODE_C37_SGMII); @@ -886,7 +991,7 @@ static int xpcs_config_aneg_c37_sgmii(struct dw_xpcs *xpcs, val = 0; mask = DW_VR_MII_DIG_CTRL1_2G5_EN | DW_VR_MII_DIG_CTRL1_MAC_AUTO_SW; - if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) + if (use_inband_status && !xpcs->qps615_oob) val = DW_VR_MII_DIG_CTRL1_MAC_AUTO_SW; if (xpcs->info.pma == WX_TXGBE_XPCS_PMA_10G_ID) { @@ -898,9 +1003,15 @@ static int xpcs_config_aneg_c37_sgmii(struct dw_xpcs *xpcs, if (ret < 0) return ret; - if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) + if (use_inband_status) { + ret = xpcs_write(xpcs, MDIO_MMD_VEND2, + DW_VR_MII_AN_INTR_STS, 0); + if (ret < 0) + return ret; + ret = xpcs_write(xpcs, MDIO_MMD_VEND2, MII_BMCR, mdio_ctrl | BMCR_ANENABLE); + } return ret; } @@ -978,6 +1089,16 @@ static int xpcs_config_2500basex(struct dw_xpcs *xpcs) { int ret; + if (xpcs->qps615_sgmii_plus) { + ret = xpcs_modify(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_CTRL, + DW_VR_MII_PCS_MODE_MASK | + DW_VR_MII_TX_CONFIG_MASK, + FIELD_PREP(DW_VR_MII_PCS_MODE_MASK, + DW_VR_MII_PCS_MODE_C37_SGMII)); + if (ret < 0) + return ret; + } + ret = xpcs_modify(xpcs, MDIO_MMD_VEND2, DW_VR_MII_DIG_CTRL1, DW_VR_MII_DIG_CTRL1_2G5_EN | DW_VR_MII_DIG_CTRL1_MAC_AUTO_SW, @@ -996,21 +1117,19 @@ static int xpcs_config_usxgmii(struct dw_xpcs *xpcs) /* Select the 10GBASE-R PCS type used as the base for USXGMII */ ret = xpcs_modify(xpcs, MDIO_MMD_PCS, MDIO_CTRL2, + xpcs->qps615 ? QPS615_PCS_CTRL2_TYPE_MASK : MDIO_PCS_CTRL2_TYPE, MDIO_PCS_CTRL2_10GBR); if (ret < 0) return ret; - /* Enable USXGMII and select the 10G USXGMII baud mode. The DW XPCS - * does not do this on its own for USXGMII, and without it the SerDes - * block-locks but no USXGMII frames are decoded (RX stays dead). - */ ret = xpcs_modify_vpcs(xpcs, DW_VR_XS_PCS_DIG_CTRL1, DW_USXGMII_EN, DW_USXGMII_EN); if (ret < 0) return ret; ret = xpcs_modify_vpcs(xpcs, DW_VR_XS_PCS_KR_CTRL, - DW_USXG_MODE, DW_USXG_MODE_10G); + DW_USXG_MODE | DW_USXG_2PT5G_GMII, + 0); if (ret < 0) return ret; @@ -1022,10 +1141,14 @@ static int xpcs_config_usxgmii(struct dw_xpcs *xpcs) if (ret < 0) return ret; - return read_poll_timeout(xpcs_read_vpcs, val, - val < 0 || !(val & DW_VR_RST), - 1000, 50000, false, - xpcs, DW_VR_XS_PCS_DIG_CTRL1); + ret = read_poll_timeout(xpcs_read_vpcs, val, + val < 0 || !(val & DW_VR_RST), + 1000, 50000, false, + xpcs, DW_VR_XS_PCS_DIG_CTRL1); + if (val < 0) + return val; + + return ret; } static int xpcs_do_config(struct dw_xpcs *xpcs, phy_interface_t interface, @@ -1356,12 +1479,13 @@ static void xpcs_link_up_sgmii_1000basex(struct dw_xpcs *xpcs, int speed, int duplex) { u16 an_enable; - int ret; + int an_speed, an_sts, ret; if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) return; - an_enable = (interface == PHY_INTERFACE_MODE_SGMII ? BMCR_ANENABLE : 0); + an_enable = interface == PHY_INTERFACE_MODE_SGMII && xpcs->qps615_oob ? + BMCR_ANENABLE : 0; if (interface == PHY_INTERFACE_MODE_1000BASEX) { if (speed != SPEED_1000) { @@ -1379,9 +1503,55 @@ static void xpcs_link_up_sgmii_1000basex(struct dw_xpcs *xpcs, ret = xpcs_write(xpcs, MDIO_MMD_VEND2, MII_BMCR, mii_bmcr_encode_fixed(speed, duplex) | an_enable); - if (ret) + if (ret) { dev_err(&xpcs->mdiodev->dev, "%s: xpcs_write returned %pe\n", __func__, ERR_PTR(ret)); + return; + } + + if (!xpcs->qps615_oob || interface != PHY_INTERFACE_MODE_SGMII) + return; + + switch (speed) { + case SPEED_10: + an_speed = DW_VR_MII_C37_ANSGM_SP_10; + break; + case SPEED_100: + an_speed = DW_VR_MII_C37_ANSGM_SP_100; + break; + case SPEED_1000: + an_speed = DW_VR_MII_C37_ANSGM_SP_1000; + break; + default: + dev_err(&xpcs->mdiodev->dev, + "QPS615 SGMII cannot resolve unsupported speed %d\n", + speed); + return; + } + + ret = read_poll_timeout(xpcs_read, an_sts, + an_sts < 0 || + ((an_sts & (DW_VR_MII_C37_ANSGM_SP_LNKSTS | + DW_VR_MII_AN_STS_C37_ANSGM_FD | + DW_VR_MII_AN_STS_C37_ANCMPLT_INTR)) == + (DW_VR_MII_C37_ANSGM_SP_LNKSTS | + (duplex == DUPLEX_FULL ? + DW_VR_MII_AN_STS_C37_ANSGM_FD : 0) | + DW_VR_MII_AN_STS_C37_ANCMPLT_INTR) && + FIELD_GET(DW_VR_MII_AN_STS_C37_ANSGM_SP, an_sts) == + an_speed), + 1000, 60000000, false, xpcs, MDIO_MMD_VEND2, + DW_VR_MII_AN_INTR_STS); + if (an_sts < 0) + ret = an_sts; + if (ret) { + dev_err(&xpcs->mdiodev->dev, + "QPS615 SGMII Clause 37 did not resolve speed %d duplex %d: status=%04x, error=%pe\n", + speed, duplex, an_sts < 0 ? 0 : an_sts, + ERR_PTR(ret)); + return; + } + } static void xpcs_link_up(struct phylink_pcs *pcs, unsigned int neg_mode, @@ -1470,6 +1640,15 @@ void xpcs_config_eee_mult_fact(struct dw_xpcs *xpcs, u8 mult_fact) } EXPORT_SYMBOL_GPL(xpcs_config_eee_mult_fact); +void xpcs_config_qps615(struct dw_xpcs *xpcs, bool out_of_band, + bool sgmii_plus) +{ + xpcs->qps615 = true; + xpcs->qps615_oob = out_of_band; + xpcs->qps615_sgmii_plus = sgmii_plus; +} +EXPORT_SYMBOL_GPL(xpcs_config_qps615); + static int xpcs_read_ids(struct dw_xpcs *xpcs) { int ret; diff --git a/drivers/net/pcs/pcs-xpcs.h b/drivers/net/pcs/pcs-xpcs.h index 484981cf5d53c..0617dbff4f18c 100644 --- a/drivers/net/pcs/pcs-xpcs.h +++ b/drivers/net/pcs/pcs-xpcs.h @@ -16,6 +16,8 @@ #define DW_USXGMII_RST BIT(10) #define DW_USXGMII_EN BIT(9) #define DW_VR_XS_PCS_DIG_CTRL1 0x0000 +#define QPS615_PCS_CTRL2_TYPE_MASK GENMASK(3, 0) +#define QPS615_PCS_CTRL2_2500BASEX 0x000e #define DW_VR_RST BIT(15) #define DW_EN_VSMMD1 BIT(13) #define DW_CL37_BP BIT(12) @@ -24,14 +26,18 @@ #define DW_PSEQ_ST GENMASK(4, 2) #define DW_PSEQ_ST_GOOD FIELD_PREP(GENMASK(4, 2), 0x4) -/* VR_XS_PCS_KR_CTRL - selects the USXGMII baud/mode (10G/5G/2.5G) */ -#define DW_VR_XS_PCS_KR_CTRL 0x001c +#define DW_VR_XS_PCS_KR_CTRL 0x0007 + +#define DW_USXG_2PT5G_GMII BIT(13) #define DW_USXG_MODE GENMASK(12, 10) -#define DW_USXG_MODE_10G 0x0 +#define DW_USXG_MODE_10G FIELD_PREP(DW_USXG_MODE, 0x0) /* SR_MII */ #define DW_USXGMII_FULL BIT(8) #define DW_USXGMII_SS_MASK (BIT(13) | BIT(6) | BIT(5)) + +#define DW_USXGMII_CTRL_MASK (DW_USXGMII_SS_MASK | \ + DW_USXGMII_FULL | BMCR_ANENABLE) #define DW_USXGMII_10000 (BIT(13) | BIT(6)) #define DW_USXGMII_5000 (BIT(13) | BIT(5)) #define DW_USXGMII_2500 (BIT(5)) @@ -118,6 +124,10 @@ struct dw_xpcs { struct phylink_pcs pcs; phy_interface_t interface; bool need_reset; + + bool qps615; + bool qps615_oob; + bool qps615_sgmii_plus; u8 eee_mult_fact; }; diff --git a/include/linux/pcs/pcs-xpcs.h b/include/linux/pcs/pcs-xpcs.h index 36073f7b6bb40..1950d93b350b9 100644 --- a/include/linux/pcs/pcs-xpcs.h +++ b/include/linux/pcs/pcs-xpcs.h @@ -53,6 +53,8 @@ struct dw_xpcs; struct phylink_pcs *xpcs_to_phylink_pcs(struct dw_xpcs *xpcs); int xpcs_get_an_mode(struct dw_xpcs *xpcs, phy_interface_t interface); void xpcs_config_eee_mult_fact(struct dw_xpcs *xpcs, u8 mult_fact); +void xpcs_config_qps615(struct dw_xpcs *xpcs, bool out_of_band, + bool sgmii_plus); struct dw_xpcs *xpcs_create_mdiodev(struct mii_bus *bus, int addr); struct dw_xpcs *xpcs_create_fwnode(struct fwnode_handle *fwnode); void xpcs_destroy(struct dw_xpcs *xpcs); From 707ed01888e122e2864b9ee7b599710e0040a233 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Mon, 10 Aug 2026 23:37:12 +0800 Subject: [PATCH 19/21] WIP: net: stmmac: add MAC prepare and post-link callbacks Some stmmac platforms must sequence external PHY and PCS changes around phylink MAC reconfiguration. Add platform callbacks before MAC configuration and after link-up. Program the GMAC speed fields for sub-2.5G links on XGMAC hardware. Signed-off-by: Junhao Xie --- .../net/ethernet/stmicro/stmmac/stmmac_main.c | 27 +++++++++++++++++++ include/linux/stmmac.h | 7 +++++ 2 files changed, 34 insertions(+) diff --git a/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c b/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c index abea939fcf967..30a87d046512d 100644 --- a/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c +++ b/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c @@ -971,6 +971,19 @@ static void stmmac_mac_config(struct phylink_config *config, unsigned int mode, /* Nothing to do, xpcs_config() handles everything */ } +static int stmmac_mac_prepare(struct phylink_config *config, unsigned int mode, + phy_interface_t interface) +{ + struct net_device *ndev = to_net_dev(config->dev); + struct stmmac_priv *priv = netdev_priv(ndev); + + if (priv->plat->mac_prepare) + return priv->plat->mac_prepare(ndev, priv->plat->bsp_priv, mode, + interface); + + return 0; +} + static int stmmac_mac_finish(struct phylink_config *config, unsigned int mode, phy_interface_t interface) { @@ -1024,6 +1037,15 @@ static void stmmac_mac_link_up(struct phylink_config *config, case SPEED_2500: ctrl |= priv->hw->link.xgmii.speed2500; break; + case SPEED_1000: + ctrl |= priv->hw->link.speed1000; + break; + case SPEED_100: + ctrl |= priv->hw->link.speed100; + break; + case SPEED_10: + ctrl |= priv->hw->link.speed10; + break; default: return; } @@ -1106,6 +1128,10 @@ static void stmmac_mac_link_up(struct phylink_config *config, } stmmac_mac_set(priv, priv->ioaddr, true); + if (priv->plat->post_mac_link_up) + priv->plat->post_mac_link_up(priv->dev, priv->plat->bsp_priv, + phy, mode, interface, speed); + if (priv->dma_cap.eee) stmmac_set_eee_pls(priv, priv->hw, true); @@ -1193,6 +1219,7 @@ static int stmmac_mac_wol_set(struct phylink_config *config, u32 wolopts, static const struct phylink_mac_ops stmmac_phylink_mac_ops = { .mac_get_caps = stmmac_mac_get_caps, .mac_select_pcs = stmmac_mac_select_pcs, + .mac_prepare = stmmac_mac_prepare, .mac_config = stmmac_mac_config, .mac_finish = stmmac_mac_finish, .mac_link_down = stmmac_mac_link_down, diff --git a/include/linux/stmmac.h b/include/linux/stmmac.h index 76dd6b4ad997f..0150e33ac18a7 100644 --- a/include/linux/stmmac.h +++ b/include/linux/stmmac.h @@ -258,9 +258,16 @@ struct plat_stmmacenet_data { int (*set_clk_tx_rate)(void *priv, struct clk *clk_tx_i, phy_interface_t interface, int speed); void (*fix_mac_speed)(void *priv, int speed, unsigned int mode); + void (*post_mac_link_up)(struct net_device *ndev, void *priv, + struct phy_device *phydev, unsigned int mode, + phy_interface_t interface, int speed); int (*fix_soc_reset)(struct stmmac_priv *priv); int (*serdes_powerup)(struct net_device *ndev, void *priv); void (*serdes_powerdown)(struct net_device *ndev, void *priv); + int (*mac_prepare)(struct net_device *ndev, + void *priv, + unsigned int mode, + phy_interface_t interface); int (*mac_finish)(struct net_device *ndev, void *priv, unsigned int mode, From a44435e7c0f303e7a5d07f046a4026946b91de6e Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Mon, 10 Aug 2026 23:37:46 +0800 Subject: [PATCH 20/21] WIP: net: stmmac: tc956x: support serial interface switching Coordinate QPS615 PMA, XPCS and MAC programming around phylink interface changes. The DT quirk selects SGMII through 1 Gbit/s and USXGMII from 2.5 Gbit/s while keeping the eMAC core out of runtime reset. QCA8081 reports 2500BASE-X when its fixed SGMII host link enters SGMII+ at 2.5 Gbit/s. Preserve the SGMII PCS personality for that case, expose the 2.5 Gbit/s capability, and reset only the QPS615 PMA/XPCS domains during an interface-family change. Signed-off-by: Junhao Xie --- .../ethernet/stmicro/stmmac/dwmac-tc956x.c | 322 ++++++++++++++---- 1 file changed, 249 insertions(+), 73 deletions(-) diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c index 72bfa21281c4c..c4cff5cada096 100644 --- a/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c +++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-tc956x.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -42,19 +43,35 @@ /* Fields and values for the EMACTL registers */ #define EMAC_SP_SEL_MASK GENMASK(3, 0) + #define SP_SEL_2500BASEX 4 #define SP_SEL_SGMII_1000M 5 #define SP_SEL_SGMII_100M 6 #define SP_SEL_SGMII_10M 7 -#define SP_SEL_USXGMII_10G 8 -#define SP_SEL_USXGMII_100M_10G 9 -#define SP_SEL_USXGMII_5G 10 -#define SP_SEL_USXGMII_2500M 13 +#define SP_SEL_USXGMII_10G_10G 8 +#define SP_SEL_USXGMII_5G_10G 9 +#define SP_SEL_USXGMII_2500M_10G 11 +#define SP_SEL_USXGMII_2500M_2500M 13 #define EMAC_PHY_INF_SEL_MASK GENMASK(5, 4) #define PCS_CLK_PHY 1 /* Clock from PHY */ #define EMAC_INV_SGM_SIG_DET BIT(6) /* 1 = polarity inverted */ #define EMAC_LPIHWCLKEN BIT(8) /* 1 = low power mode */ -#define EMAC_INIT_DONE BIT(21) +#define MISC_CTL_OFFSET 0x1800 +#define MISC_SP_ETH0_MASK GENMASK(18, 16) +#define MISC_SP_ETH1_MASK GENMASK(26, 24) +#define MISC_SP_ETH_1G 1 +#define MISC_SP_ETH_100M 3 +#define MISC_SP_ETH_10M 7 + +#define NRSTCTRL_OFFSET(_id) (0x1008 + (_id) * 0x8) + +#define RST_MAC_MACRST BIT(7) + +#define MAC_TX_CONFIG_OFFSET 0x0000 +#define MAC_TX_CONFIG_TE BIT(0) + +#define MAC_RX_CONFIG_OFFSET 0x0004 +#define MAC_RX_CONFIG_RE BIT(0) /* MSIGEN Registers */ #define MSI_OUT_EN_OFFSET 0x0000 @@ -90,12 +107,9 @@ enum msigen_hwirq { /* Offset to the PMATOP memory block, relative to the EMAC address range */ #define DWMAC_PMATOP_OFFSET 0x4000 -/* TC956X SFR offsets used for USXGMII PMA/lane setup */ -#define TC956X_GLUE_PHY_REG_ACCESS_CTRL 0x2c030 -#define TC956X_PHY_CORE0_GL_LANE_ACCESS 0x28000 -#define TC956X_PMA_LN_PCS2PMA_PHYMODE_R2 0x2b268 - #define PMA_CML_GL_PM_CFG0 0x01b8 +#define PMA_PCS_GL_PC_ST0 0x0168 +#define PMA_PCS_GL_PC_ST0_PLL_VALID BIT(0) /* * Five sets three registers must be configured for PMA. The HWT_REFCLK @@ -141,10 +155,15 @@ struct tc956x_data { struct irq_domain *irq_domain; struct tc956x_dwmac_data *auxbus_data; struct plat_stmmacenet_data *plat; + bool mode_switch; struct tc956x_msi_vec msi_vec[MSI_VEC_COUNT]; unsigned int msi_vec_used; + phy_interface_t interface; + + int boot_speed; + /* These three fields are used by the plat_stmmacenet_data structure */ struct stmmac_dma_cfg dma_cfg; struct stmmac_mdio_bus_data mdio_bus_data; @@ -162,18 +181,14 @@ struct tc956x_mac_speed { }; static struct tc956x_mac_speed mac_speed[] = { - { PHY_INTERFACE_MODE_USXGMII, SPEED_10000, SP_SEL_USXGMII_10G }, - { PHY_INTERFACE_MODE_USXGMII, SPEED_5000, SP_SEL_USXGMII_5G }, - { PHY_INTERFACE_MODE_USXGMII, SPEED_2500, SP_SEL_USXGMII_2500M }, - { PHY_INTERFACE_MODE_USXGMII, SPEED_1000, SP_SEL_USXGMII_10G }, - { PHY_INTERFACE_MODE_USXGMII, SPEED_100, SP_SEL_USXGMII_100M_10G }, - { PHY_INTERFACE_MODE_USXGMII, SPEED_10, SP_SEL_USXGMII_100M_10G }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_10000, SP_SEL_USXGMII_10G_10G }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_5000, SP_SEL_USXGMII_5G_10G }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_2500, SP_SEL_USXGMII_2500M_10G }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_1000, SP_SEL_USXGMII_10G_10G }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_100, SP_SEL_USXGMII_5G_10G }, + { PHY_INTERFACE_MODE_USXGMII, SPEED_10, SP_SEL_USXGMII_5G_10G }, { PHY_INTERFACE_MODE_2500BASEX, SPEED_2500, SP_SEL_2500BASEX }, - /* - * QCA808x switches its host interface to 2500BASE-X at 2.5G, but older - * stmmac only passes speed to fix_mac_speed(). Legacy DTs therefore - * still arrive here as SGMII, so program the 2500BASE-X selector. - */ + { PHY_INTERFACE_MODE_1000BASEX, SPEED_1000, SP_SEL_SGMII_1000M }, { PHY_INTERFACE_MODE_SGMII, SPEED_2500, SP_SEL_2500BASEX }, { PHY_INTERFACE_MODE_SGMII, SPEED_1000, SP_SEL_SGMII_1000M }, { PHY_INTERFACE_MODE_SGMII, SPEED_100, SP_SEL_SGMII_100M }, @@ -363,7 +378,6 @@ static void tc956x_msigen_irq_domain_exit(struct irq_domain *irq_domain) } } - /* We have one IRQ chip instance with 25 IRQs in its domain */ static struct irq_domain * tc956x_msigen_irq_domain_instantiate(struct tc956x_data *td) @@ -394,19 +408,17 @@ tc956x_msigen_irq_domain_instantiate(struct tc956x_data *td) * tc956x_pma_init() - Initialize PMA * @td: bsp_priv pointer * - * Initialize (or re-initialize) the PMA, configure the clocks and wait for the - * eMAC to be ready. */ -static void tc956x_pma_init(struct tc956x_data *td) +static int tc956x_pma_init(struct tc956x_data *td) { const struct tc956x_chip *chip = td->auxbus_data->chip; - void __iomem *emac_ctl = td->auxbus_data->emac_ctl; - void __iomem *sfr = td->auxbus_data->sfr; u32 id = td->auxbus_data->mac_id; void __iomem *pmatop; + int ret; u32 val; u32 i; + pmatop = td->auxbus_data->emac + DWMAC_PMATOP_OFFSET; /* * When we re-initialize the PMA then the reset will already have * been deasserted. We must make sure the PMA reset is asserted before @@ -414,8 +426,6 @@ static void tc956x_pma_init(struct tc956x_data *td) */ tc956x_reset_assert(chip, id, MAC_RESET_PMA); - pmatop = td->auxbus_data->emac + DWMAC_PMATOP_OFFSET; - /* Power on CML buffer (0 = normal mode, 1 = power down) */ writel(0, pmatop + PMA_CML_GL_PM_CFG0); @@ -438,19 +448,22 @@ static void tc956x_pma_init(struct tc956x_data *td) tc956x_reset_deassert(chip, id, MAC_RESET_PMA); - if (td->plat->phy_interface == PHY_INTERFACE_MODE_USXGMII) { - writel(0x0f, sfr + TC956X_GLUE_PHY_REG_ACCESS_CTRL); - writel(0x0f, sfr + TC956X_PHY_CORE0_GL_LANE_ACCESS); - writel(0x02, sfr + TC956X_PMA_LN_PCS2PMA_PHYMODE_R2); + ret = readl_poll_timeout(pmatop + PMA_PCS_GL_PC_ST0, val, + val & PMA_PCS_GL_PC_ST0_PLL_VALID, + 10, 100000); + if (ret) { + dev_err(td->dev, + "Ethernet PMA PLL did not become valid (PCS_GL_PC_ST0=%08x)\n", + val); + return ret; } - WARN_ON(readl_poll_timeout(emac_ctl, val, val & EMAC_INIT_DONE, 50, 1000000)); + return 0; } static int tc956x_mac_speed_select(struct tc956x_data *td, phy_interface_t phy_interface, int speed) { - struct net_device *netdev; int i; for (i = 0; i < ARRAY_SIZE(mac_speed); i++) { @@ -460,9 +473,8 @@ static int tc956x_mac_speed_select(struct tc956x_data *td, if (mac_speed[i].phy_interface == phy_interface) return mac_speed[i].sp_sel; } - netdev = dev_get_drvdata(td->dev); - netdev_err(netdev, "%s/%d unsupported\n", - phy_modes(phy_interface), speed); + dev_err(td->dev, "%s/%d unsupported\n", + phy_modes(phy_interface), speed); return -EOPNOTSUPP; } @@ -471,6 +483,9 @@ static int tc956x_mac_configure(struct tc956x_data *td, phy_interface_t phy_interface, int speed) { void __iomem *emac_ctl = td->auxbus_data->emac_ctl; + void __iomem *misc_ctl = td->auxbus_data->sfr + MISC_CTL_OFFSET; + u32 id = td->auxbus_data->mac_id; + u32 rate = 0; int sp_sel; u32 val; @@ -485,13 +500,33 @@ static int tc956x_mac_configure(struct tc956x_data *td, val = u32_replace_bits(val, sp_sel, EMAC_SP_SEL_MASK); writel(val, emac_ctl); + if (phy_interface == PHY_INTERFACE_MODE_USXGMII) { + switch (speed) { + case SPEED_1000: + rate = MISC_SP_ETH_1G; + break; + case SPEED_100: + rate = MISC_SP_ETH_100M; + break; + case SPEED_10: + rate = MISC_SP_ETH_10M; + break; + } + } + + val = readl(misc_ctl); + if (id) + val = u32_replace_bits(val, rate, MISC_SP_ETH1_MASK); + else + val = u32_replace_bits(val, rate, MISC_SP_ETH0_MASK); + writel(val, misc_ctl); + return 0; } static int tc956x_mac_enable(struct tc956x_data *td) { const struct tc956x_chip *chip = td->auxbus_data->chip; - struct plat_stmmacenet_data *plat = td->plat; u32 id = td->auxbus_data->mac_id; int ret; @@ -501,20 +536,21 @@ static int tc956x_mac_enable(struct tc956x_data *td) if (id) tc956x_clock_enable(chip, id, MAC_CLOCK_RMII); - if (plat->phy_interface == PHY_INTERFACE_MODE_USXGMII) { - tc956x_common_clock_enable(chip, COMMON_CLOCK_PLL); - tc956x_common_clock_enable(chip, COMMON_CLOCK_SGMII); - tc956x_common_clock_enable(chip, COMMON_CLOCK_REFCLK); - tc956x_clock_enable(chip, id, MAC_CLOCK_125M); - tc956x_clock_enable(chip, id, MAC_CLOCK_312_5M); - } + tc956x_common_clock_enable(chip, COMMON_CLOCK_PLL); + tc956x_common_clock_enable(chip, COMMON_CLOCK_SGMII); + tc956x_common_clock_enable(chip, COMMON_CLOCK_REFCLK); + tc956x_clock_enable(chip, id, MAC_CLOCK_125M); + tc956x_clock_enable(chip, id, MAC_CLOCK_312_5M); - ret = tc956x_mac_configure(td, plat->phy_interface, plat->max_speed); + ret = tc956x_mac_configure(td, td->interface, td->boot_speed); if (ret) - dev_warn(td->dev, "failed to configure MAC speed: %d\n", ret); + return dev_err_probe(td->dev, ret, + "failed to configure MAC speed\n"); tc956x_reset_deassert(chip, id, MAC_RESET_MAC); - tc956x_pma_init(td); + ret = tc956x_pma_init(td); + if (ret) + return ret; tc956x_reset_deassert(chip, id, MAC_RESET_XPCS); return 0; @@ -523,9 +559,7 @@ static int tc956x_mac_enable(struct tc956x_data *td) static void tc956x_mac_disable(struct tc956x_data *td) { const struct tc956x_chip *chip = td->auxbus_data->chip; - struct plat_stmmacenet_data *plat = td->plat; u32 id = td->auxbus_data->mac_id; - tc956x_reset_assert(chip, id, MAC_RESET_MAC); tc956x_reset_assert(chip, id, MAC_RESET_PMA); tc956x_reset_assert(chip, id, MAC_RESET_XPCS); @@ -536,10 +570,8 @@ static void tc956x_mac_disable(struct tc956x_data *td) if (id) tc956x_clock_disable(chip, id, MAC_CLOCK_RMII); - if (plat->phy_interface == PHY_INTERFACE_MODE_USXGMII) { - tc956x_clock_disable(chip, id, MAC_CLOCK_125M); - tc956x_clock_disable(chip, id, MAC_CLOCK_312_5M); - } + tc956x_clock_disable(chip, id, MAC_CLOCK_125M); + tc956x_clock_disable(chip, id, MAC_CLOCK_312_5M); } static void tc956x_mac_init_state(struct tc956x_data *td) @@ -654,6 +686,7 @@ static int tc956x_mac_setup(void *apriv, struct mac_device_info *mac) static int tc956x_pcs_init(struct stmmac_priv *priv) { struct xpcs_regmap_config xpcs_regmap_cfg; + struct tc956x_data *td = priv->plat->bsp_priv; void __iomem *emac = priv->ioaddr; struct regmap *xpcs_regmap; void __iomem *xpcs_addr; @@ -672,6 +705,8 @@ static int tc956x_pcs_init(struct stmmac_priv *priv) if (IS_ERR(xpcs)) return PTR_ERR(xpcs); + xpcs_config_qps615(xpcs, td->mode_switch, + td->plat->phy_interface == PHY_INTERFACE_MODE_SGMII); xpcs_config_eee_mult_fact(xpcs, priv->plat->mult_fact_100ns); priv->hw->phylink_pcs = xpcs_to_phylink_pcs(xpcs); @@ -684,16 +719,114 @@ static struct phylink_pcs *tc956x_select_pcs(struct stmmac_priv *priv, return priv->hw->phylink_pcs; } +static int tc956x_mac_finish(struct net_device *ndev, void *bsp_priv, + unsigned int mode, phy_interface_t interface) +{ + struct tc956x_data *td = bsp_priv; + + td->interface = interface; + + return 0; +} + +static int tc956x_mac_prepare(struct net_device *ndev, void *bsp_priv, + unsigned int mode, + phy_interface_t interface) +{ + struct tc956x_data *td = bsp_priv; + const struct tc956x_chip *chip = td->auxbus_data->chip; + u32 id = td->auxbus_data->mac_id; + void __iomem *rst = td->auxbus_data->sfr + NRSTCTRL_OFFSET(id); + u32 rst_before, rst_after; + int speed, ret; + + if (td->interface == interface) + return 0; + if (!td->mode_switch && + td->plat->phy_interface != PHY_INTERFACE_MODE_SGMII) + return -EOPNOTSUPP; + + speed = ndev->phydev ? ndev->phydev->speed : SPEED_UNKNOWN; + if (tc956x_mac_speed_select(td, interface, speed) < 0) { + switch (interface) { + case PHY_INTERFACE_MODE_USXGMII: + speed = SPEED_10000; + break; + case PHY_INTERFACE_MODE_2500BASEX: + speed = SPEED_2500; + break; + case PHY_INTERFACE_MODE_SGMII: + case PHY_INTERFACE_MODE_1000BASEX: + speed = SPEED_1000; + break; + default: + return -EOPNOTSUPP; + } + } + + rst_before = readl(rst); + + tc956x_reset_assert(chip, id, MAC_RESET_XPCS); + + ret = tc956x_mac_configure(td, interface, speed); + if (ret) + return ret; + + ret = tc956x_pma_init(td); + if (ret) + return ret; + td->interface = interface; + tc956x_reset_deassert(chip, id, MAC_RESET_XPCS); + + rst_after = readl(rst); + if ((rst_before | rst_after) & RST_MAC_MACRST) { + netdev_err(ndev, + "eMAC core reset unexpectedly asserted during PMA/XPCS-only switch: NRSTCTRL %08x -> %08x\n", + rst_before, rst_after); + return -EIO; + } + return 0; +} + static void tc956x_fix_mac_speed(void *bsp_priv, int speed, unsigned int mode) { struct tc956x_data *td = bsp_priv; - phy_interface_t interface = td->plat->phy_interface; + phy_interface_t interface = td->interface; + int ret; - tc956x_mac_configure(td, interface, speed); - if (interface == PHY_INTERFACE_MODE_USXGMII) + ret = tc956x_mac_configure(td, interface, speed); + if (ret) { + dev_err(td->dev, "failed to configure %s at %d Mbps: %d\n", + phy_modes(interface), speed, ret); return; + } +} - tc956x_pma_init(td); +static void tc956x_post_mac_link_up(struct net_device *ndev, void *bsp_priv, + struct phy_device *phydev, + unsigned int mode, + phy_interface_t interface, int speed) +{ + struct tc956x_data *td = bsp_priv; + void __iomem *mac = td->auxbus_data->emac; + u32 tx, rx; + int ret; + + if (td->mode_switch && interface == PHY_INTERFACE_MODE_USXGMII) { + tx = readl(mac + MAC_TX_CONFIG_OFFSET); + rx = readl(mac + MAC_RX_CONFIG_OFFSET); + if (!(tx & MAC_TX_CONFIG_TE) || !(rx & MAC_RX_CONFIG_RE)) + return; + + if (!phydev) + return; + + ret = phy_config_inband(phydev, LINK_INBAND_BYPASS); + if (ret) + netdev_err(ndev, + "post-MAC-enable USXGMII PHY synchronization failed: %d\n", + ret); + } } static int tc956x_dwmac_suspend(struct device *dev, void *bsp_priv) @@ -749,8 +882,9 @@ static void tc956x_deassert_phy_resets(struct tc956x_data *td) return; for_each_available_child_of_node(td->plat->mdio_node, child) { - reset = fwnode_gpiod_get_index(of_fwnode_handle(child), "reset", 0, - GPIOD_OUT_LOW, "phy-reset"); + reset = fwnode_gpiod_get_index(of_fwnode_handle(child), + "reset", 0, GPIOD_OUT_LOW, + "phy-reset"); if (IS_ERR(reset)) continue; @@ -772,11 +906,51 @@ static int tc956x_lookup_max_speed(phy_interface_t phy_interface) case PHY_INTERFACE_MODE_2500BASEX: return SPEED_2500; + case PHY_INTERFACE_MODE_1000BASEX: + return SPEED_1000; + default: return -EOPNOTSUPP; } } +static const phy_interface_t tc956x_serdes_interfaces[] = { + PHY_INTERFACE_MODE_USXGMII, + PHY_INTERFACE_MODE_2500BASEX, + PHY_INTERFACE_MODE_SGMII, + PHY_INTERFACE_MODE_1000BASEX, +}; + +static bool tc956x_is_serdes_interface(phy_interface_t phy_interface) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(tc956x_serdes_interfaces); i++) + if (tc956x_serdes_interfaces[i] == phy_interface) + return true; + + return false; +} + +static int tc956x_max_speed(struct tc956x_data *td, + phy_interface_t phy_interface) +{ + int max, ret, i; + + max = tc956x_lookup_max_speed(phy_interface); + if (max < 0 || !td->mode_switch || + !tc956x_is_serdes_interface(phy_interface)) + return max; + + for (i = 0; i < ARRAY_SIZE(tc956x_serdes_interfaces); i++) { + ret = tc956x_lookup_max_speed(tc956x_serdes_interfaces[i]); + if (ret > max) + max = ret; + } + + return max; +} + /* Called by tc956x_dwmac_probe(); return errors with dev_err_probe() */ static int tc956x_plat_dat_init(struct tc956x_data *td) { @@ -792,6 +966,9 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) if (phy_interface < 0) return -ENODEV; + td->mode_switch = device_property_read_bool(dev, + "sgmii-usxgmii-switch-quirk"); + /* The platform structure is allocated with devm_kzalloc() */ plat = stmmac_plat_dat_alloc(dev); if (!plat) @@ -802,6 +979,9 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) return dev_err_probe(dev, ret, "unsupported phy speed\n"); speed = ret; + td->interface = phy_interface; + td->boot_speed = speed; + plat->core_type = DWMAC_CORE_XGMAC; plat->bus_id = to_auxiliary_dev(dev)->id; plat->phy_interface = phy_interface; @@ -818,16 +998,11 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) */ plat->clk_csr = 0; /* MDC clock = clk_csr_i / 62 */ plat->mdio_bus_data->default_an_inband = - phy_interface != PHY_INTERFACE_MODE_USXGMII; + !td->mode_switch && phy_interface != PHY_INTERFACE_MODE_USXGMII; plat->force_sf_dma_mode = true; - plat->max_speed = speed; + plat->max_speed = tc956x_max_speed(td, phy_interface); plat->unicast_filter_entries = 32; - /* - * Use a single ordinary RX/TX queue and give it the whole FIFO partition. - * TC956X partitions FIFO per function; exposing multiple stmmac queues makes - * the core split this budget and leaves the active queue too small for 10G. - */ plat->rx_queues_to_use = TC956X_RX_QUEUES; plat->tx_queues_to_use = TC956X_TX_QUEUES; plat->rss_en = TC956X_RX_QUEUES > 1; @@ -859,6 +1034,9 @@ static int tc956x_plat_dat_init(struct tc956x_data *td) } plat->fix_mac_speed = tc956x_fix_mac_speed; + plat->post_mac_link_up = tc956x_post_mac_link_up; + plat->mac_prepare = tc956x_mac_prepare; + plat->mac_finish = tc956x_mac_finish; plat->suspend = tc956x_dwmac_suspend; plat->resume = tc956x_dwmac_resume; plat->mac_setup = tc956x_mac_setup; @@ -949,7 +1127,9 @@ static int tc956x_dwmac_probe(struct auxiliary_device *adev, /* Put the MAC in a known initial state */ tc956x_mac_init_state(td); - tc956x_mac_enable(td); + ret = tc956x_mac_enable(td); + if (ret) + return dev_err_probe(dev, ret, "failed initial MAC enable\n"); tc956x_deassert_phy_resets(td); ret = tc956x_stmmac_resources_init(td, &res); @@ -961,10 +1141,6 @@ static int tc956x_dwmac_probe(struct auxiliary_device *adev, if (ret == -EPROBE_DEFER) return dev_err_probe(dev, ret, "failed to get MAC address\n"); - ret = tc956x_mac_enable(td); - if (ret) - return dev_err_probe(dev, ret, "failed to enable MAC\n"); - ret = stmmac_dvr_probe(dev, td->plat, &res); if (ret) return dev_err_probe(dev, ret, "failed stmmac probe\n"); From 665233bc56c5d6dcb9f40f874a55349c926a6c02 Mon Sep 17 00:00:00 2001 From: Junhao Xie Date: Mon, 10 Aug 2026 23:38:06 +0800 Subject: [PATCH 21/21] WIP: net: phy: as21xxx: support AS22xxx interface switching Teach the AS22010x driver to program the firmware and system-side SerDes for SGMII and USXGMII. Gate the behavior by DT and select the mode from the copper speed. Defer upshift completion across the firmware-induced link cycle until the documented PCS and MII readiness conditions are observed. Signed-off-by: Junhao Xie --- drivers/net/phy/as21xxx.c | 1486 ++++++++++++++++++++++++++++++++++--- 1 file changed, 1366 insertions(+), 120 deletions(-) diff --git a/drivers/net/phy/as21xxx.c b/drivers/net/phy/as21xxx.c index 76a7b495a3360..c7b0eda729ac0 100644 --- a/drivers/net/phy/as21xxx.c +++ b/drivers/net/phy/as21xxx.c @@ -11,6 +11,11 @@ #include #include #include +#include + +#define VEND1_CHIP_CTRL 0x2 +#define VEND1_CHIP_CTRL_XFI_ACCESS BIT(15) +#define VEND1_CHIP_CTRL_SXGMII_MODE BIT(13) #define VEND1_GLB_REG_CPU_RESET_ADDR_LO_BASEADDR 0x3 #define VEND1_GLB_REG_CPU_RESET_ADDR_HI_BASEADDR 0x4 @@ -68,6 +73,37 @@ #define IPC_CMD_TEMP_MON 0x15 /* Temperature monitoring function */ #define IPC_CMD_SET_LED 0x23 /* Set led */ +#define IPC_OPCODE_DBGCMD 0x16 +#define IPC_OPCODE_POLL 0x17 +#define IPC_OPCODE_WBUF 0x18 +#define IPC_OPCODE_RBUF 0x19 + +#define IPC_DBGCMD_DPC 0x8b + +#define IPC_DBGCMD_SDS 0x96 + +#define IPC_SDS_RST 26 + +#define IPC_DPC_SDS_SET_CFG 0x2 +#define IPC_DPC_SDS_GET_CFG 0x3 +#define IPC_DPC_ETH_STS_HW_UPD_CFG 0x8 +#define IPC_DPC_FC_SET 0xd + +#define AEON_SDS_PCS_SEL_NORMAL 0 +#define AEON_SDS_PCS_SEL_64B66B 1 +#define AEON_SDS_SPD_1G 0 +#define AEON_SDS_SPD_10G 3 + +#define AEON_CU_AN_TOP_SPD_10M 0x02 +#define AEON_CU_AN_TOP_SPD_100M 0x04 +#define AEON_CU_AN_TOP_SPD_1G 0x08 +#define AEON_CU_AN_TOP_SPD_2500M 0x10 +#define AEON_CU_AN_TOP_SPD_5G 0x20 +#define AEON_CU_AN_TOP_SPD_10G 0x40 + +#define AEON_DPC_MAX_POLL 100 +#define AEON_DPC_RBUF_MAX 144 + #define VEND1_IPC_STS 0x5802 #define AEON_IPC_STS_PARITY BIT(15) #define AEON_IPC_STS_SIZE GENMASK(14, 10) @@ -105,6 +141,8 @@ /* CFG DIRECT sub command */ #define IPC_CFG_PARAM_DIRECT_NG_PHYCTRL 0x1 #define IPC_CFG_PARAM_DIRECT_CU_AN 0x2 +#define IPC_CMD_CFG_CU_AN_RESTART 0xa +#define IPC_CMD_CFG_CU_AN_TOP_SPD 0xc #define IPC_CFG_PARAM_DIRECT_SDS_PCS 0x3 #define IPC_CFG_PARAM_DIRECT_AUTO_EEE 0x4 #define IPC_CFG_PARAM_DIRECT_SDS_PMA 0x5 @@ -121,6 +159,30 @@ #define AS21XXX_MDIO_AN_C22 0xffe0 +#define AS22XXX_SDS_MII_ADV_LINK BIT(15) +#define AS22XXX_SDS_MII_ADV_ACK BIT(14) +#define AS22XXX_SDS_MII_ADV_REMOTE_FAULT BIT(13) +#define AS22XXX_SDS_MII_ADV_FULL BIT(12) +#define AS22XXX_SDS_MII_ADV_SPEED GENMASK(11, 10) +#define AS22XXX_SDS_MII_ADV_EEE BIT(9) +#define AS22XXX_SDS_MII_ADV_EEE_CLK_STOP BIT(8) +#define AS22XXX_SDS_MII_ADV_RESERVED_7 BIT(7) +#define AS22XXX_SDS_MII_ADV_SELECTOR BIT(0) +#define AS22XXX_SDS_MII_ADV_CONTROL_MASK (AS22XXX_SDS_MII_ADV_LINK | \ + AS22XXX_SDS_MII_ADV_ACK | \ + AS22XXX_SDS_MII_ADV_REMOTE_FAULT | \ + AS22XXX_SDS_MII_ADV_FULL | \ + AS22XXX_SDS_MII_ADV_SPEED | \ + AS22XXX_SDS_MII_ADV_RESERVED_7 | \ + AS22XXX_SDS_MII_ADV_SELECTOR) + +#define AS22XXX_SDS_PCS_STS20 0x0020 +#define AS22XXX_SDS_PCS_LINK_UP BIT(12) +#define AS22XXX_SDS_PCS_HIGH_BER BIT(1) +#define AS22XXX_SDS_PCS_BLOCK_LOCK BIT(0) +#define AS22XXX_SDS_READY_POLL_US 100000 +#define AS22XXX_SDS_READY_TIMEOUT_US 10000000 + #define PHY_ID_AS21XXX 0x75009410 /* AS21xxx ID Legend * AS21x1xxB1 @@ -182,6 +244,12 @@ enum as21xxx_led_event { VEND1_LED_REG_A_EVENT_OFF, }; +enum as22xxx_system_sync_state { + AS22XXX_SYSTEM_SYNC_IDLE, + AS22XXX_SYSTEM_SYNC_NEEDS_PHY_ENABLE, + AS22XXX_SYSTEM_SYNC_WAIT_PHY_ENABLE, +}; + struct as21xxx_led_pattern_info { unsigned int pattern; u16 val; @@ -189,8 +257,16 @@ struct as21xxx_led_pattern_info { struct as21xxx_priv { bool parity_status; + bool mode_switch; /* Protect concurrent IPC access */ struct mutex ipc_lock; + + struct mutex sds_lock; + + phy_interface_t sds_interface; + int sds_speed; + + enum as22xxx_system_sync_state system_sync_state; }; static struct as21xxx_led_pattern_info as21xxx_led_supported_pattern[] = { @@ -457,9 +533,9 @@ static int aeon_ipc_send_cmd(struct phy_device *phydev, * If data not NULL, return number of Bytes received from IPC or * a negative error. */ -static int aeon_ipc_send_msg(struct phy_device *phydev, - u16 opcode, u16 *data, unsigned int data_len, - u16 *ret_data) +static int aeon_ipc_send_msg_locked(struct phy_device *phydev, u16 opcode, + u16 *data, unsigned int data_len, + u16 *ret_data) { struct as21xxx_priv *priv = phydev->priv; unsigned int ret_size; @@ -470,17 +546,18 @@ static int aeon_ipc_send_msg(struct phy_device *phydev, /* IPC have a max of 8 register to transfer data, * make sure we never exceed this. */ - if (data_len > AEON_IPC_DATA_MAX) + if (data_len > AEON_IPC_DATA_MAX || (data_len && !data)) return -EINVAL; - for (i = 0; i < data_len / sizeof(u16); i++) - phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_IPC_DATA(i), - data[i]); - cmd = FIELD_PREP(AEON_IPC_CMD_SIZE, data_len) | FIELD_PREP(AEON_IPC_CMD_OPCODE, opcode); - mutex_lock(&priv->ipc_lock); + for (i = 0; i < DIV_ROUND_UP(data_len, sizeof(u16)); i++) { + ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_IPC_DATA(i), + data[i]); + if (ret) + goto out; + } ret = aeon_ipc_send_cmd(phydev, priv, cmd, &ret_sts); if (ret) { @@ -489,7 +566,7 @@ static int aeon_ipc_send_msg(struct phy_device *phydev, goto out; } - if (!data) + if (!ret_data) goto out; if ((ret_sts & AEON_IPC_STS_STATUS) == AEON_IPC_STS_STATUS_ERROR) { @@ -519,11 +596,183 @@ static int aeon_ipc_send_msg(struct phy_device *phydev, ret = ret_size; out: + return ret; +} + +static int aeon_ipc_send_msg(struct phy_device *phydev, u16 opcode, u16 *data, + unsigned int data_len, u16 *ret_data) +{ + struct as21xxx_priv *priv = phydev->priv; + int ret; + + mutex_lock(&priv->ipc_lock); + ret = aeon_ipc_send_msg_locked(phydev, opcode, data, data_len, + ret_data); mutex_unlock(&priv->ipc_lock); return ret; } +static int aeon_ipc_sync_parity(struct phy_device *phydev, + struct as21xxx_priv *priv); +static int aeon_ipc_sync_parity_locked(struct phy_device *phydev, + struct as21xxx_priv *priv); + +static int aeon_cfg_xfer(struct phy_device *phydev, u16 section, u16 sub, + const void *payload, unsigned int payload_len, + void *rbuf, unsigned int rbytes) +{ + u16 wbuf[AEON_IPC_DATA_NUM_REGISTERS] = {}; + u16 data[AEON_IPC_DATA_NUM_REGISTERS] = {}; + struct as21xxx_priv *priv = phydev->priv; + unsigned int i, done, chunk; + u16 cmd, hdr[3], ret_sts = 0; + u8 *out = rbuf; + int ret, iter; + + if (payload_len > sizeof(wbuf) || rbytes > AEON_DPC_RBUF_MAX) + return -EINVAL; + + hdr[0] = (section << 8) | sub; + hdr[1] = payload_len; + hdr[2] = 0; + + mutex_lock(&priv->ipc_lock); + + ret = aeon_ipc_send_msg_locked(phydev, IPC_OPCODE_DBGCMD, hdr, + sizeof(hdr), data); + if (ret < 0) { + phydev_err(phydev, + "firmware command %02x/%02x start failed: %d\n", + section, sub, ret); + goto out; + } + + ret = aeon_ipc_sync_parity_locked(phydev, priv); + if (ret) + goto out; + + if (payload && payload_len) { + memcpy(wbuf, payload, payload_len); + ret = aeon_ipc_send_msg_locked(phydev, IPC_OPCODE_WBUF, wbuf, + payload_len, data); + if (ret < 0) { + phydev_err(phydev, + "firmware command %02x/%02x payload failed: %d\n", + section, sub, ret); + goto out; + } + + ret = aeon_ipc_sync_parity_locked(phydev, priv); + if (ret) + goto out; + } + + for (iter = 0; iter < AEON_DPC_MAX_POLL; iter++) { + cmd = FIELD_PREP(AEON_IPC_CMD_OPCODE, IPC_OPCODE_POLL); + ret = aeon_ipc_send_cmd(phydev, priv, cmd, &ret_sts); + if (ret) { + phydev_err(phydev, "firmware command %02x/%02x poll failed: %d sts %04x\n", + section, sub, ret, ret_sts); + goto out; + } + + ret = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_IPC_DATA(0)); + if (ret < 0) + goto out; + if (ret) + break; + } + + if (iter == AEON_DPC_MAX_POLL) { + phydev_err(phydev, "firmware command %02x/%02x timed out\n", + section, sub); + ret = -ETIMEDOUT; + goto out; + } + + ret = 0; + if (!rbuf || !rbytes) + goto out_sync; + + for (done = 0; done < rbytes; done += chunk) { + chunk = min_t(unsigned int, rbytes - done, AEON_IPC_DATA_MAX); + + cmd = FIELD_PREP(AEON_IPC_CMD_OPCODE, IPC_OPCODE_RBUF); + ret = aeon_ipc_send_cmd(phydev, priv, cmd, &ret_sts); + if (ret) { + phydev_err(phydev, "firmware command %02x/%02x result failed: %d sts %04x\n", + section, sub, ret, ret_sts); + goto out; + } + + for (i = 0; i < AEON_IPC_DATA_NUM_REGISTERS; i++) { + ret = phy_read_mmd(phydev, MDIO_MMD_VEND1, + VEND1_IPC_DATA(i)); + if (ret < 0) + goto out; + + data[i] = ret; + } + + memcpy(out + done, data, chunk); + } + + ret = 0; + +out_sync: + ret = aeon_ipc_sync_parity_locked(phydev, priv); + +out: + mutex_unlock(&priv->ipc_lock); + return ret; +} + +static int aeon_dpc_cmd(struct phy_device *phydev, u16 sub, + const void *payload, unsigned int payload_len) +{ + return aeon_cfg_xfer(phydev, IPC_DBGCMD_DPC, sub, payload, payload_len, + NULL, 0); +} + +static int aeon_dpc_query(struct phy_device *phydev, u16 sub, void *rbuf, + unsigned int rbytes) +{ + return aeon_cfg_xfer(phydev, IPC_DBGCMD_DPC, sub, NULL, 0, rbuf, + rbytes); +} + +static int aeon_dpc_fc_apply_mode(struct phy_device *phydev, u8 pcs_sel) +{ + u8 enable = pcs_sel == AEON_SDS_PCS_SEL_NORMAL; + + return aeon_dpc_cmd(phydev, IPC_DPC_FC_SET, &enable, sizeof(enable)); +} + +static int aeon_dpc_sds_get(struct phy_device *phydev, u8 *pcs_sel, + u8 *sds_spd) +{ + u8 cfg[AEON_IPC_DATA_MAX] = {}; + int ret; + + ret = aeon_dpc_query(phydev, IPC_DPC_SDS_GET_CFG, cfg, sizeof(cfg)); + if (ret) + return ret; + + *pcs_sel = cfg[0]; + *sds_spd = cfg[1]; + + return 0; +} + +static int aeon_dpc_sds_set(struct phy_device *phydev, u8 pcs_sel, u8 sds_spd) +{ + u16 payload = pcs_sel | (sds_spd << 8); + + return aeon_dpc_cmd(phydev, IPC_DPC_SDS_SET_CFG, &payload, + sizeof(payload)); +} + static int aeon_ipc_noop(struct phy_device *phydev, struct as21xxx_priv *priv, u16 *ret_sts) { @@ -540,16 +789,16 @@ static int aeon_ipc_noop(struct phy_device *phydev, * to handle the packet only for the second one. This way * we make sure we are sync for every next cmd. */ -static int aeon_ipc_sync_parity(struct phy_device *phydev, - struct as21xxx_priv *priv) +static int aeon_ipc_sync_parity_locked(struct phy_device *phydev, + struct as21xxx_priv *priv) { - u16 ret_sts; + u16 ret_sts = 0; int ret; - mutex_lock(&priv->ipc_lock); - /* Send NOP with no parity */ - aeon_ipc_noop(phydev, priv, NULL); + ret = aeon_ipc_noop(phydev, priv, NULL); + if (ret) + return ret; /* Reset packet parity */ priv->parity_status = false; @@ -557,8 +806,6 @@ static int aeon_ipc_sync_parity(struct phy_device *phydev, /* Send second NOP with no parity */ ret = aeon_ipc_noop(phydev, priv, &ret_sts); - mutex_unlock(&priv->ipc_lock); - /* We expect to return -EINVAL */ if (ret != -EINVAL) return ret; @@ -572,26 +819,43 @@ static int aeon_ipc_sync_parity(struct phy_device *phydev, return 0; } -static int aeon_ipc_get_fw_version(struct phy_device *phydev) +static int aeon_ipc_sync_parity(struct phy_device *phydev, + struct as21xxx_priv *priv) { - u16 ret_data[AEON_IPC_DATA_NUM_REGISTERS], data[1]; - char fw_version[AEON_IPC_DATA_MAX + 1]; int ret; - data[0] = IPC_INFO_VERSION; + mutex_lock(&priv->ipc_lock); + ret = aeon_ipc_sync_parity_locked(phydev, priv); + mutex_unlock(&priv->ipc_lock); + + return ret; +} + +static int aeon_ipc_get_fw_version(struct phy_device *phydev) +{ + u16 ret_data[AEON_IPC_DATA_NUM_REGISTERS], data[] = { IPC_INFO_VERSION }; + int ret; ret = aeon_ipc_send_msg(phydev, IPC_CMD_INFO, data, sizeof(data), ret_data); - if (ret < 0) - return ret; - /* Make sure FW version is NULL terminated */ - memcpy(fw_version, ret_data, ret); - fw_version[ret] = '\0'; + return ret < 0 ? ret : 0; +} + +static int aeon_ipc_phy_enable_mode(struct phy_device *phydev, bool enable) +{ + struct as21xxx_priv *priv = phydev->priv; + u16 data[] = { IPC_SYS_CPU_PHY_ENABLE, enable }; + int ret; - phydev_info(phydev, "Firmware Version: %s\n", fw_version); + mutex_lock(&priv->ipc_lock); + ret = aeon_ipc_sync_parity_locked(phydev, priv); + if (!ret) + ret = aeon_ipc_send_msg_locked(phydev, IPC_CMD_SYS_CPU, data, + sizeof(data), NULL); + mutex_unlock(&priv->ipc_lock); - return 0; + return ret; } static int aeon_dpc_ra_enable(struct phy_device *phydev) @@ -605,6 +869,74 @@ static int aeon_dpc_ra_enable(struct phy_device *phydev) sizeof(data), NULL); } +static int aeon_dpc_eth_sts_hw_update(struct phy_device *phydev, bool enable) +{ + u8 value = enable ? 1 : 0; + + return aeon_dpc_cmd(phydev, IPC_DPC_ETH_STS_HW_UPD_CFG, &value, + sizeof(value)); +} + +static int as22xxx_restart_an(struct phy_device *phydev) +{ + u16 data[3]; + + data[0] = IPC_CFG_PARAM_DIRECT; + data[1] = IPC_CFG_PARAM_DIRECT_CU_AN; + data[2] = IPC_CMD_CFG_CU_AN_RESTART; + + return aeon_ipc_send_msg(phydev, IPC_CMD_CFG_PARAM, data, + sizeof(data), NULL); +} + +static int as22xxx_set_top_speed(struct phy_device *phydev) +{ + u16 data[4] = { + IPC_CFG_PARAM_DIRECT, + IPC_CFG_PARAM_DIRECT_CU_AN, + IPC_CMD_CFG_CU_AN_TOP_SPD, + AEON_CU_AN_TOP_SPD_10M, + }; + unsigned long *advertising = phydev->advertising; + + if (linkmode_test_bit(ETHTOOL_LINK_MODE_10000baseT_Full_BIT, + advertising)) + data[3] = AEON_CU_AN_TOP_SPD_10G; + else if (linkmode_test_bit(ETHTOOL_LINK_MODE_5000baseT_Full_BIT, + advertising)) + data[3] = AEON_CU_AN_TOP_SPD_5G; + else if (linkmode_test_bit(ETHTOOL_LINK_MODE_2500baseT_Full_BIT, + advertising)) + data[3] = AEON_CU_AN_TOP_SPD_2500M; + else if (linkmode_test_bit(ETHTOOL_LINK_MODE_1000baseT_Full_BIT, + advertising)) + data[3] = AEON_CU_AN_TOP_SPD_1G; + else if (linkmode_test_bit(ETHTOOL_LINK_MODE_100baseT_Full_BIT, + advertising)) + data[3] = AEON_CU_AN_TOP_SPD_100M; + + return aeon_ipc_send_msg(phydev, IPC_CMD_CFG_PARAM, data, + sizeof(data), NULL); +} + +static int as22xxx_config_aneg(struct phy_device *phydev) +{ + int ret; + + if (phydev->autoneg == AUTONEG_DISABLE) + return genphy_c45_pma_setup_forced(phydev); + + ret = genphy_c45_an_config_aneg(phydev); + if (ret < 0) + return ret; + + ret = as22xxx_set_top_speed(phydev); + if (ret) + return ret; + + return as22xxx_restart_an(phydev); +} + static int as21xxx_probe(struct phy_device *phydev) { struct as21xxx_priv *priv; @@ -620,6 +952,9 @@ static int as21xxx_probe(struct phy_device *phydev) &priv->ipc_lock); if (ret) return ret; + ret = devm_mutex_init(&phydev->mdio.dev, &priv->sds_lock); + if (ret) + return ret; ret = aeon_ipc_sync_parity(phydev, priv); if (ret) @@ -868,103 +1203,958 @@ static void as22xxx_read_speed(struct phy_device *phydev, int bmcr) } } -static int as22xxx_read_status(struct phy_device *phydev) +static phy_interface_t as22xxx_speed_interface(int speed) { - int bmcr, old_link = phydev->link; - int ret; - - ret = as22xxx_read_link(phydev, &bmcr); - if (ret) - return ret; + switch (speed) { + case SPEED_10: + case SPEED_100: + case SPEED_1000: + return PHY_INTERFACE_MODE_SGMII; + + case SPEED_2500: + case SPEED_5000: + case SPEED_10000: + return PHY_INTERFACE_MODE_USXGMII; + + default: + return PHY_INTERFACE_MODE_NA; + } +} - if (phydev->autoneg == AUTONEG_ENABLE && old_link && phydev->link) +static int as22xxx_interface_sds(phy_interface_t interface, u8 *pcs_sel, + u8 *sds_spd) +{ + switch (interface) { + case PHY_INTERFACE_MODE_SGMII: + *pcs_sel = AEON_SDS_PCS_SEL_NORMAL; + *sds_spd = AEON_SDS_SPD_1G; return 0; - phydev->speed = SPEED_UNKNOWN; - phydev->duplex = DUPLEX_UNKNOWN; - phydev->pause = 0; - phydev->asym_pause = 0; - - if (phydev->autoneg == AUTONEG_ENABLE) { - ret = genphy_c45_read_lpa(phydev); - if (ret) - return ret; + case PHY_INTERFACE_MODE_USXGMII: - ret = as22xxx_read_lpa(phydev); - if (ret) - return ret; + *pcs_sel = AEON_SDS_PCS_SEL_64B66B; + *sds_spd = AEON_SDS_SPD_10G; + return 0; - if (phydev->autoneg_complete) { - as22xxx_read_speed(phydev, bmcr); - phy_resolve_aneg_linkmode(phydev); - } - } else { - linkmode_zero(phydev->lp_advertising); - as22xxx_read_speed(phydev, bmcr); + default: + return -EOPNOTSUPP; } - - return 0; } -static int as21xxx_led_brightness_set(struct phy_device *phydev, - u8 index, enum led_brightness value) -{ - u16 val = VEND1_LED_REG_A_EVENT_OFF; - - if (index > AEON_MAX_LEDS) - return -EINVAL; +static int as22xxx_program_system_side(struct phy_device *phydev, + phy_interface_t interface, + u8 pcs_sel, u8 sds_spd); +static int as22xxx_sds_mii_config(struct phy_device *phydev, + phy_interface_t interface, int speed); - if (value) - val = VEND1_LED_REG_A_EVENT_ON; +static bool as22xxx_update_interface(struct phy_device *phydev) +{ + struct as21xxx_priv *priv = phydev->priv; + phy_interface_t interface; + u8 pcs_sel, sds_spd; + + if (!priv->mode_switch || !phydev->link) + return true; + + interface = as22xxx_speed_interface(phydev->speed); + if (interface == PHY_INTERFACE_MODE_NA) + return true; + + if (interface == priv->sds_interface) { + if (interface == PHY_INTERFACE_MODE_SGMII && + phydev->speed != priv->sds_speed) { + if (as22xxx_sds_mii_config(phydev, interface, + phydev->speed)) + return false; + if (aeon_dpc_ra_enable(phydev)) + return false; + + priv->sds_speed = phydev->speed; + } - return phy_modify_mmd(phydev, MDIO_MMD_VEND1, - VEND1_LED_REG(index), - VEND1_LED_REG_A_EVENT, - FIELD_PREP(VEND1_LED_REG_A_EVENT, val)); -} + phydev->interface = interface; + return true; + } -static int as21xxx_led_hw_is_supported(struct phy_device *phydev, u8 index, - unsigned long rules) -{ - int i; + if (as22xxx_interface_sds(interface, &pcs_sel, &sds_spd)) + return true; - if (index > AEON_MAX_LEDS) - return -EINVAL; + if (as22xxx_program_system_side(phydev, interface, pcs_sel, sds_spd)) + return false; - for (i = 0; i < ARRAY_SIZE(as21xxx_led_supported_pattern); i++) - if (rules == as21xxx_led_supported_pattern[i].pattern) - return 0; + priv->sds_interface = interface; + priv->sds_speed = phydev->speed; + phydev->interface = interface; - return -EOPNOTSUPP; + return true; } -static int as21xxx_led_hw_control_get(struct phy_device *phydev, u8 index, - unsigned long *rules) +static int as22xxx_select_system_side(struct phy_device *phydev, + phy_interface_t interface); +static int as22xxx_select_system_side(struct phy_device *phydev, + phy_interface_t interface) { - int i, val; + struct as21xxx_priv *priv = phydev->priv; + u16 want = interface == PHY_INTERFACE_MODE_USXGMII ? + VEND1_CHIP_CTRL_SXGMII_MODE : + 0; + int ret, result = 0; - if (index > AEON_MAX_LEDS) - return -EINVAL; + mutex_lock(&priv->sds_lock); - val = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_LED_REG(index)); - if (val < 0) - return val; + ret = phy_modify_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, + VEND1_CHIP_CTRL_SXGMII_MODE, want); + if (ret < 0) { + result = ret; + goto out; + } - val = FIELD_GET(VEND1_LED_REG_A_EVENT, val); - for (i = 0; i < ARRAY_SIZE(as21xxx_led_supported_pattern); i++) - if (val == as21xxx_led_supported_pattern[i].val) { - *rules = as21xxx_led_supported_pattern[i].pattern; - return 0; - } + ret = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (ret < 0) { + result = ret; + goto out; + } + if ((ret & VEND1_CHIP_CTRL_SXGMII_MODE) != want) { + phydev_err(phydev, + "system-side Chip Control personality did not take for %s (chip_ctrl=%04x)\n", + phy_modes(interface), ret); + result = -EIO; + goto out; + } - /* Should be impossible */ - return -EINVAL; +out: + mutex_unlock(&priv->sds_lock); + return result; } -static int as21xxx_led_hw_control_set(struct phy_device *phydev, u8 index, - unsigned long rules) +static int as22xxx_sds_mii_config(struct phy_device *phydev, + phy_interface_t interface, int speed) { - u16 val = 0; + struct as21xxx_priv *priv = phydev->priv; + u16 mask = BMCR_ANENABLE | BMCR_ANRESTART | BMCR_SPEED1000 | + BMCR_SPEED100 | BMCR_FULLDPLX; + u16 want, adv_speed = 0, adv_want = 0; + int orig, paged, val, adv, adv_readback, restored; + int ret = 0; + + if (interface == PHY_INTERFACE_MODE_SGMII) { + switch (speed) { + case SPEED_10: + adv_speed = FIELD_PREP(AS22XXX_SDS_MII_ADV_SPEED, 0); + break; + case SPEED_100: + adv_speed = FIELD_PREP(AS22XXX_SDS_MII_ADV_SPEED, 1); + break; + case SPEED_1000: + adv_speed = FIELD_PREP(AS22XXX_SDS_MII_ADV_SPEED, 2); + break; + default: + return -EOPNOTSUPP; + } + want = mii_bmcr_encode_fixed(speed, DUPLEX_FULL); + } else if (interface == PHY_INTERFACE_MODE_USXGMII) { + switch (speed) { + case SPEED_2500: + adv_speed = MDIO_USXGMII_2500; + break; + case SPEED_5000: + adv_speed = MDIO_USXGMII_5000; + break; + case SPEED_10000: + adv_speed = MDIO_USXGMII_10G; + break; + default: + return -EOPNOTSUPP; + } + + want = BMCR_SPEED100 | BMCR_SPEED1000 | BMCR_FULLDPLX; + + adv_want = MDIO_USXGMII_LINK | MDIO_USXGMII_FULL_DUPLEX | + adv_speed | AS22XXX_SDS_MII_ADV_SELECTOR; + } else { + return -EOPNOTSUPP; + } + + mutex_lock(&priv->sds_lock); + + orig = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (orig < 0) { + ret = orig; + goto out_unlock; + } + + ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, + orig | VEND1_CHIP_CTRL_XFI_ACCESS); + if (ret) + goto out_restore; + + paged = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (paged < 0) { + ret = paged; + goto out_restore; + } + if (!(paged & VEND1_CHIP_CTRL_XFI_ACCESS)) { + ret = -EIO; + goto out_restore; + } + + ret = phy_modify_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_BMCR, + BMCR_ANENABLE | BMCR_ANRESTART, 0); + if (ret < 0) + goto out_restore; + + if (interface == PHY_INTERFACE_MODE_SGMII) { + adv = phy_read_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_ADVERTISE); + if (adv < 0) { + ret = adv; + goto out_restore; + } + adv_want = (adv & ~AS22XXX_SDS_MII_ADV_CONTROL_MASK) | + AS22XXX_SDS_MII_ADV_LINK | + AS22XXX_SDS_MII_ADV_FULL | + adv_speed | + AS22XXX_SDS_MII_ADV_SELECTOR; + + ret = phy_modify_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_ADVERTISE, + AS22XXX_SDS_MII_ADV_CONTROL_MASK, + adv_want); + if (ret < 0) + goto out_restore; + + adv_readback = phy_read_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_ADVERTISE); + if (adv_readback < 0) { + ret = adv_readback; + goto out_restore; + } + if ((adv_readback & AS22XXX_SDS_MII_ADV_CONTROL_MASK) != + (adv_want & AS22XXX_SDS_MII_ADV_CONTROL_MASK)) { + phydev_err(phydev, + "SGMII MII advertisement did not take (read %04x, want %04x)\n", + adv_readback, adv_want); + ret = -EIO; + goto out_restore; + } + } else { + ret = phy_write_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_ADVERTISE, + adv_want); + if (ret) + goto out_restore; + + adv_readback = phy_read_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_ADVERTISE); + if (adv_readback < 0) { + ret = adv_readback; + goto out_restore; + } + if (adv_readback != adv_want) { + phydev_err(phydev, + "USXGMII MII advertisement did not take for %d Mbps (read %04x, want %04x)\n", + speed, adv_readback, adv_want); + ret = -EIO; + goto out_restore; + } + } + + ret = phy_modify_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_BMCR, mask, want); + if (ret < 0) + goto out_restore; + + val = phy_read_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_BMCR); + if (val < 0) { + ret = val; + goto out_restore; + } + if ((val & mask) != want) { + phydev_err(phydev, + "SerDes MII fixed control did not take for %d Mbps (read %04x, want %04x)\n", + speed, val, want); + ret = -EIO; + goto out_restore; + } + + want |= BMCR_ANENABLE; + ret = phy_modify_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_BMCR, mask, + want | BMCR_ANRESTART); + if (ret < 0) + goto out_restore; + + ret = read_poll_timeout(phy_read_mmd, val, + val < 0 || !(val & BMCR_ANRESTART), + 1000, 100000, false, phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_BMCR); + if (val < 0) { + ret = val; + goto out_restore; + } + if (ret) { + phydev_err(phydev, + "SerDes MII AN restart did not self-clear for %d Mbps (read %04x)\n", + speed, val); + goto out_restore; + } + if ((val & mask) != want) { + phydev_err(phydev, + "SerDes MII control did not take for %d Mbps (read %04x, want %04x)\n", + speed, val, want); + ret = -EIO; + goto out_restore; + } + + adv_readback = phy_read_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_ADVERTISE); + if (adv_readback < 0) { + ret = adv_readback; + goto out_restore; + } + if ((interface == PHY_INTERFACE_MODE_SGMII && + (adv_readback & AS22XXX_SDS_MII_ADV_CONTROL_MASK) != + (adv_want & AS22XXX_SDS_MII_ADV_CONTROL_MASK)) || + (interface == PHY_INTERFACE_MODE_USXGMII && + adv_readback != adv_want)) { + phydev_err(phydev, + "%s MII advertisement changed after AN restart (read %04x, want %04x)\n", + phy_modes(interface), adv_readback, adv_want); + ret = -EIO; + goto out_restore; + } + +out_restore: + restored = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, + orig); + if (!restored) + restored = phy_read_mmd(phydev, MDIO_MMD_VEND1, + VEND1_CHIP_CTRL); + if (restored != orig) { + phydev_err(phydev, + "SerDes MII config failed to restore chip_ctrl %04x (readback/err %d)\n", + orig, restored); + if (!ret) + ret = restored < 0 ? restored : -EIO; + } + +out_unlock: + mutex_unlock(&priv->sds_lock); + return ret; +} + +static int as22xxx_sds_pcs_reset(struct phy_device *phydev) +{ + struct as21xxx_priv *priv = phydev->priv; + int orig, paged, after, restored; + int ret = 0; + + mutex_lock(&priv->sds_lock); + + orig = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (orig < 0) { + ret = orig; + goto out_unlock; + } + + ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, + orig | VEND1_CHIP_CTRL_XFI_ACCESS); + if (ret) + goto out_restore; + + paged = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (paged < 0) { + ret = paged; + goto out_restore; + } + if (!(paged & VEND1_CHIP_CTRL_XFI_ACCESS)) { + ret = -EIO; + goto out_restore; + } + + ret = phy_modify_mmd(phydev, MDIO_MMD_PCS, MDIO_CTRL1, + MDIO_CTRL1_RESET, MDIO_CTRL1_RESET); + if (ret < 0) + goto out_restore; + + ret = phy_read_mmd_poll_timeout(phydev, MDIO_MMD_PCS, MDIO_CTRL1, after, + !(after & MDIO_CTRL1_RESET), 5000, + 1000000, true); + if (ret) + goto out_restore; + +out_restore: + restored = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, orig); + if (!restored) + restored = + phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (restored != orig) { + phydev_err(phydev, + "SerDes PCS reset failed to restore chip_ctrl %04x (readback/err %d)\n", + orig, restored); + if (!ret) + ret = restored < 0 ? restored : -EIO; + } + +out_unlock: + mutex_unlock(&priv->sds_lock); + return ret; +} + +static int as22xxx_sds_pma_reset(struct phy_device *phydev) +{ + struct as21xxx_priv *priv = phydev->priv; + int orig, paged, after, restored; + int ret = 0; + + mutex_lock(&priv->sds_lock); + + orig = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (orig < 0) { + ret = orig; + goto out_unlock; + } + + ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, + orig | VEND1_CHIP_CTRL_XFI_ACCESS); + if (ret) + goto out_restore; + + paged = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (paged < 0) { + ret = paged; + goto out_restore; + } + if (!(paged & VEND1_CHIP_CTRL_XFI_ACCESS)) { + ret = -EIO; + goto out_restore; + } + + ret = phy_modify_mmd(phydev, MDIO_MMD_PMAPMD, MDIO_CTRL1, + MDIO_CTRL1_RESET, MDIO_CTRL1_RESET); + if (ret < 0) + goto out_restore; + + ret = phy_read_mmd_poll_timeout(phydev, MDIO_MMD_PMAPMD, MDIO_CTRL1, + after, + !(after & MDIO_CTRL1_RESET), + 5000, 1000000, true); + if (ret) + goto out_restore; + +out_restore: + restored = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, orig); + if (!restored) + restored = + phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (restored != orig) { + phydev_err(phydev, + "SerDes PMA reset failed to restore chip_ctrl %04x (readback/err %d)\n", + orig, restored); + if (!ret) + ret = restored < 0 ? restored : -EIO; + } + +out_unlock: + mutex_unlock(&priv->sds_lock); + return ret; +} + +static int as22xxx_sds_10g_ctrl_restore(struct phy_device *phydev) +{ + struct as21xxx_priv *priv = phydev->priv; + int orig, paged, pma_after, pcs_after, restored; + int ret = 0; + + mutex_lock(&priv->sds_lock); + + orig = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (orig < 0) { + ret = orig; + goto out_unlock; + } + + ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, + orig | VEND1_CHIP_CTRL_XFI_ACCESS); + if (ret) + goto out_restore; + + paged = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (paged < 0) { + ret = paged; + goto out_restore; + } + if (!(paged & VEND1_CHIP_CTRL_XFI_ACCESS)) { + ret = -EIO; + goto out_restore; + } + + ret = phy_modify_mmd(phydev, MDIO_MMD_PMAPMD, MDIO_CTRL1, + MDIO_CTRL1_SPEEDSEL, MDIO_CTRL1_SPEED10G); + if (ret < 0) + goto out_restore; + ret = phy_modify_mmd(phydev, MDIO_MMD_PCS, MDIO_CTRL1, + MDIO_CTRL1_SPEEDSEL, MDIO_CTRL1_SPEED10G); + if (ret < 0) + goto out_restore; + + pma_after = phy_read_mmd(phydev, MDIO_MMD_PMAPMD, MDIO_CTRL1); + if (pma_after < 0) { + ret = pma_after; + goto out_restore; + } + pcs_after = phy_read_mmd(phydev, MDIO_MMD_PCS, MDIO_CTRL1); + if (pcs_after < 0) { + ret = pcs_after; + goto out_restore; + } + if ((pma_after & MDIO_CTRL1_SPEEDSEL) != MDIO_CTRL1_SPEED10G || + (pcs_after & MDIO_CTRL1_SPEEDSEL) != MDIO_CTRL1_SPEED10G) { + phydev_err(phydev, + "SerDes 10G controls did not take: PMA %04x, PCS %04x\n", + pma_after, pcs_after); + ret = -EIO; + goto out_restore; + } + +out_restore: + restored = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, orig); + if (!restored) + restored = + phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (restored != orig) { + phydev_err(phydev, + "SerDes 10G control restore failed to restore chip_ctrl %04x (readback/err %d)\n", + orig, restored); + if (!ret) + ret = restored < 0 ? restored : -EIO; + } + +out_unlock: + mutex_unlock(&priv->sds_lock); + return ret; +} + +static int as22xxx_check_sds_config(struct phy_device *phydev, u8 pcs_sel, + u8 sds_spd) +{ + u8 rb_pcs_sel, rb_sds_spd; + int ret; + + ret = aeon_dpc_sds_get(phydev, &rb_pcs_sel, &rb_sds_spd); + if (ret) + return ret; + + if (rb_pcs_sel != pcs_sel || rb_sds_spd != sds_spd) { + phydev_err(phydev, + "SDS config mismatch: expected %u/%u, read %u/%u\n", + pcs_sel, sds_spd, rb_pcs_sel, rb_sds_spd); + return -EIO; + } + + return 0; +} + +static int as22xxx_stage_system_side(struct phy_device *phydev, + phy_interface_t interface, + u8 pcs_sel, u8 sds_spd) +{ + int ret; + + ret = aeon_dpc_sds_set(phydev, pcs_sel, sds_spd); + if (ret) { + phydev_err(phydev, "failed to stage SDS config for %s: %d\n", + phy_modes(interface), ret); + return ret; + } + + return as22xxx_check_sds_config(phydev, pcs_sel, sds_spd); +} + +static int as22xxx_program_system_side(struct phy_device *phydev, + phy_interface_t interface, + u8 pcs_sel, u8 sds_spd) +{ + struct as21xxx_priv *priv = phydev->priv; + bool upshift = interface == PHY_INTERFACE_MODE_USXGMII && + priv->sds_interface == PHY_INTERFACE_MODE_SGMII; + u8 sds_id = 0; + int ret; + + ret = as22xxx_select_system_side(phydev, interface); + if (ret) + return ret; + + ret = as22xxx_stage_system_side(phydev, interface, pcs_sel, sds_spd); + if (ret) + return ret; + + if (interface == PHY_INTERFACE_MODE_SGMII || upshift) { + ret = as22xxx_sds_mii_config(phydev, interface, phydev->speed); + if (ret) + return ret; + } + + if (interface == PHY_INTERFACE_MODE_USXGMII) { + ret = as22xxx_select_system_side(phydev, interface); + if (ret) + return ret; + + if (upshift) { + ret = as22xxx_sds_pma_reset(phydev); + if (ret) + return ret; + + ret = as22xxx_check_sds_config(phydev, pcs_sel, sds_spd); + if (ret) + return ret; + } + + ret = aeon_cfg_xfer(phydev, IPC_DBGCMD_SDS, IPC_SDS_RST, + &sds_id, sizeof(sds_id), NULL, 0); + if (ret) + return ret; + + ret = aeon_dpc_ra_enable(phydev); + if (ret) { + phydev_err(phydev, + "failed to enable USXGMII rate adaptor: %d\n", + ret); + return ret; + } + + ret = as22xxx_check_sds_config(phydev, pcs_sel, sds_spd); + if (ret) + return ret; + } + + ret = aeon_dpc_fc_apply_mode(phydev, pcs_sel); + if (ret) { + phydev_err(phydev, "failed to set system-side flow control: %d\n", + ret); + return ret; + } + + ret = as22xxx_select_system_side(phydev, interface); + if (ret) + return ret; + + if (upshift) { + ret = as22xxx_sds_10g_ctrl_restore(phydev); + if (ret) + return ret; + + ret = as22xxx_sds_pcs_reset(phydev); + if (ret) + return ret; + + ret = as22xxx_check_sds_config(phydev, pcs_sel, sds_spd); + if (ret) + return ret; + + ret = aeon_dpc_ra_enable(phydev); + if (ret) { + phydev_err(phydev, + "failed to restore USXGMII rate adaptor: %d\n", + ret); + return ret; + } + + ret = aeon_dpc_fc_apply_mode(phydev, pcs_sel); + if (ret) + return ret; + + ret = as22xxx_select_system_side(phydev, interface); + if (ret) + return ret; + + ret = as22xxx_check_sds_config(phydev, pcs_sel, sds_spd); + if (ret) + return ret; + + ret = as22xxx_sds_10g_ctrl_restore(phydev); + if (ret) + return ret; + + ret = as22xxx_sds_mii_config(phydev, interface, phydev->speed); + if (ret) + return ret; + + priv->system_sync_state = + AS22XXX_SYSTEM_SYNC_NEEDS_PHY_ENABLE; + } else if (interface == PHY_INTERFACE_MODE_SGMII) { + priv->system_sync_state = AS22XXX_SYSTEM_SYNC_IDLE; + } + + return 0; +} + +static int as22xxx_usxgmii_ready_paged(struct phy_device *phydev, int *pcs_sts, + int *mii_sts) +{ + int val; + + *pcs_sts = phy_read_mmd(phydev, MDIO_MMD_PCS, AS22XXX_SDS_PCS_STS20); + if (*pcs_sts < 0) + return *pcs_sts; + + val = phy_read_mmd(phydev, MDIO_MMD_AN, AS21XXX_MDIO_AN_C22 + MII_BMSR); + if (val < 0) + return val; + *mii_sts = phy_read_mmd(phydev, MDIO_MMD_AN, + AS21XXX_MDIO_AN_C22 + MII_BMSR); + if (*mii_sts < 0) + return *mii_sts; + + return ((*pcs_sts & + (AS22XXX_SDS_PCS_LINK_UP | AS22XXX_SDS_PCS_HIGH_BER | + AS22XXX_SDS_PCS_BLOCK_LOCK)) == + (AS22XXX_SDS_PCS_LINK_UP | + AS22XXX_SDS_PCS_BLOCK_LOCK) && + (*mii_sts & (BMSR_ANEGCOMPLETE | BMSR_LSTATUS | BMSR_RFAULT)) == + (BMSR_ANEGCOMPLETE | BMSR_LSTATUS)); +} + +static int as22xxx_usxgmii_ready_once(struct phy_device *phydev, int *pcs_sts, + int *mii_sts) +{ + struct as21xxx_priv *priv = phydev->priv; + int orig, paged, chip_ctrl, restore, restored; + int restore_err = 0, ret; + + mutex_lock(&priv->sds_lock); + + orig = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (orig < 0) { + ret = orig; + goto unlock; + } + + ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, + orig | VEND1_CHIP_CTRL_XFI_ACCESS); + if (ret) + goto out_restore; + + paged = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (paged < 0) { + ret = paged; + goto out_restore; + } + if (!(paged & VEND1_CHIP_CTRL_XFI_ACCESS)) { + ret = -EIO; + goto out_restore; + } + + ret = as22xxx_usxgmii_ready_paged(phydev, pcs_sts, mii_sts); + +out_restore: + + chip_ctrl = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (chip_ctrl < 0) { + restore_err = chip_ctrl; + restore = orig; + } else { + restore = (chip_ctrl & ~VEND1_CHIP_CTRL_XFI_ACCESS) | + (orig & VEND1_CHIP_CTRL_XFI_ACCESS); + } + + restored = + phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL, restore); + if (!restored) + restored = + phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_CHIP_CTRL); + if (restored < 0 || ((restored ^ orig) & VEND1_CHIP_CTRL_XFI_ACCESS)) { + phydev_err(phydev, + "USXGMII readiness sample failed to restore XFI gate from chip_ctrl %04x (readback/err %d)\n", + orig, restored); + if (ret >= 0) + ret = restored < 0 ? restored : -EIO; + } else if (restore_err && ret >= 0) { + ret = restore_err; + } + +unlock: + mutex_unlock(&priv->sds_lock); + return ret; +} + +static int as22xxx_wait_usxgmii_ready(struct phy_device *phydev) +{ + int pcs_sts = 0, mii_sts = 0; + int ready = 0, ret; + + ret = read_poll_timeout(as22xxx_usxgmii_ready_once, ready, ready, + AS22XXX_SDS_READY_POLL_US, + AS22XXX_SDS_READY_TIMEOUT_US, false, + phydev, &pcs_sts, &mii_sts); + if (ready < 0) + ret = ready; + + if (ret) + phydev_err(phydev, + "system-side USXGMII did not become ready: PCS_STS20=%04x MII_STS=%04x: %d\n", + pcs_sts, mii_sts, ret); + + return ret; +} + +static int as22xxx_config_inband(struct phy_device *phydev, unsigned int modes) +{ + struct as21xxx_priv *priv = phydev->priv; + int ret; + + if (modes != LINK_INBAND_BYPASS) + return -EOPNOTSUPP; + + if (!priv->mode_switch || + priv->system_sync_state == AS22XXX_SYSTEM_SYNC_IDLE) + return 0; + + if (phydev->interface != PHY_INTERFACE_MODE_USXGMII || + priv->sds_interface != PHY_INTERFACE_MODE_USXGMII) + return -EINVAL; + + if (priv->system_sync_state == + AS22XXX_SYSTEM_SYNC_NEEDS_PHY_ENABLE) { + ret = aeon_ipc_phy_enable_mode(phydev, false); + if (ret) + return ret; + + ret = aeon_ipc_phy_enable_mode(phydev, true); + if (ret) + return ret; + + priv->system_sync_state = + AS22XXX_SYSTEM_SYNC_WAIT_PHY_ENABLE; + + ret = aeon_ipc_get_fw_version(phydev); + if (ret) + return ret; + + return as22xxx_stage_system_side(phydev, + PHY_INTERFACE_MODE_USXGMII, + AEON_SDS_PCS_SEL_64B66B, + AEON_SDS_SPD_10G); + } + + ret = as22xxx_wait_usxgmii_ready(phydev); + if (ret) + return ret; + + ret = as22xxx_check_sds_config(phydev, + AEON_SDS_PCS_SEL_64B66B, + AEON_SDS_SPD_10G); + if (ret) + return ret; + + priv->system_sync_state = AS22XXX_SYSTEM_SYNC_IDLE; + return 0; +} + +static int as22xxx_read_status(struct phy_device *phydev) +{ + struct as21xxx_priv *priv = phydev->priv; + int bmcr, old_link = phydev->link; + int ret; + + ret = as22xxx_read_link(phydev, &bmcr); + if (ret) + return ret; + + if (!priv->mode_switch && phydev->autoneg == AUTONEG_ENABLE && old_link && + phydev->link && + priv->sds_interface != PHY_INTERFACE_MODE_NA) + return 0; + + phydev->speed = SPEED_UNKNOWN; + phydev->duplex = DUPLEX_UNKNOWN; + phydev->pause = 0; + phydev->asym_pause = 0; + + if (phydev->autoneg == AUTONEG_ENABLE) { + ret = genphy_c45_read_lpa(phydev); + if (ret) + return ret; + + ret = as22xxx_read_lpa(phydev); + if (ret) + return ret; + + if (phydev->autoneg_complete) { + as22xxx_read_speed(phydev, bmcr); + phy_resolve_aneg_linkmode(phydev); + } + } else { + linkmode_zero(phydev->lp_advertising); + as22xxx_read_speed(phydev, bmcr); + } + + if (!as22xxx_update_interface(phydev)) + phydev->link = 0; + + return 0; +} + +static int as21xxx_led_brightness_set(struct phy_device *phydev, + u8 index, enum led_brightness value) +{ + u16 val = VEND1_LED_REG_A_EVENT_OFF; + + if (index > AEON_MAX_LEDS) + return -EINVAL; + + if (value) + val = VEND1_LED_REG_A_EVENT_ON; + + return phy_modify_mmd(phydev, MDIO_MMD_VEND1, + VEND1_LED_REG(index), + VEND1_LED_REG_A_EVENT, + FIELD_PREP(VEND1_LED_REG_A_EVENT, val)); +} + +static int as21xxx_led_hw_is_supported(struct phy_device *phydev, u8 index, + unsigned long rules) +{ + int i; + + if (index > AEON_MAX_LEDS) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(as21xxx_led_supported_pattern); i++) + if (rules == as21xxx_led_supported_pattern[i].pattern) + return 0; + + return -EOPNOTSUPP; +} + +static int as21xxx_led_hw_control_get(struct phy_device *phydev, u8 index, + unsigned long *rules) +{ + int i, val; + + if (index > AEON_MAX_LEDS) + return -EINVAL; + + val = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_LED_REG(index)); + if (val < 0) + return val; + + val = FIELD_GET(VEND1_LED_REG_A_EVENT, val); + for (i = 0; i < ARRAY_SIZE(as21xxx_led_supported_pattern); i++) + if (val == as21xxx_led_supported_pattern[i].val) { + *rules = as21xxx_led_supported_pattern[i].pattern; + return 0; + } + + return -EINVAL; +} + +static int as21xxx_led_hw_control_set(struct phy_device *phydev, u8 index, + unsigned long rules) +{ + u16 val = 0; int i; if (index > AEON_MAX_LEDS) @@ -1100,28 +2290,14 @@ static int as22xxx_match_phy_device(struct phy_device *phydev, return phy_id == PHY_ID_AS22XXX; } -static int as22xxx_probe(struct phy_device *phydev) +static int as22xxx_bringup(struct phy_device *phydev) { - struct as21xxx_priv *priv; + struct as21xxx_priv *priv = phydev->priv; int ret; - /* Gen2 keeps its generic PHY ID before and after firmware load and may - * expose only PMA/PMD initially. Force the MMD bitmap used by generic C45 - * helpers, then load firmware from probe and wait for IPC to come alive. - */ - phydev->c45_ids.mmds_present |= MDIO_DEVS_PMAPMD | MDIO_DEVS_PCS | - MDIO_DEVS_AN; - - priv = devm_kzalloc(&phydev->mdio.dev, - sizeof(*priv), GFP_KERNEL); - if (!priv) - return -ENOMEM; - phydev->priv = priv; - - ret = devm_mutex_init(&phydev->mdio.dev, - &priv->ipc_lock); - if (ret) - return ret; + priv->sds_interface = PHY_INTERFACE_MODE_NA; + priv->sds_speed = SPEED_UNKNOWN; + priv->system_sync_state = AS22XXX_SYSTEM_SYNC_IDLE; ret = aeon_firmware_load(phydev); if (ret) @@ -1143,9 +2319,76 @@ static int as22xxx_probe(struct phy_device *phydev) if (ret) return ret; + if (priv->mode_switch) { + ret = as22xxx_program_system_side(phydev, + PHY_INTERFACE_MODE_USXGMII, + AEON_SDS_PCS_SEL_64B66B, + AEON_SDS_SPD_10G); + if (ret) + return ret; + + priv->sds_interface = PHY_INTERFACE_MODE_USXGMII; + priv->sds_speed = SPEED_UNKNOWN; + } + + if (priv->mode_switch) { + ret = aeon_dpc_eth_sts_hw_update(phydev, true); + if (ret) { + phydev_err(phydev, + "failed to enable DPC Ethernet-status hardware follow: %d\n", + ret); + return ret; + } + } + return 0; } +static int as22xxx_config_init(struct phy_device *phydev) +{ + struct as21xxx_priv *priv = phydev->priv; + + if (priv->mode_switch) { + phy_interface_zero(phydev->possible_interfaces); + __set_bit(PHY_INTERFACE_MODE_USXGMII, + phydev->possible_interfaces); + __set_bit(PHY_INTERFACE_MODE_SGMII, + phydev->possible_interfaces); + } + + if (!aeon_ipc_sync_parity(phydev, priv)) + return 0; + + return as22xxx_bringup(phydev); +} + +static int as22xxx_probe(struct phy_device *phydev) +{ + struct as21xxx_priv *priv; + int ret; + + phydev->c45_ids.mmds_present |= MDIO_DEVS_PMAPMD | MDIO_DEVS_PCS | + MDIO_DEVS_AN; + + priv = devm_kzalloc(&phydev->mdio.dev, + sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + phydev->priv = priv; + priv->mode_switch = device_property_read_bool(&phydev->mdio.dev, + "sgmii-usxgmii-switch-quirk"); + + ret = devm_mutex_init(&phydev->mdio.dev, + &priv->ipc_lock); + if (ret) + return ret; + ret = devm_mutex_init(&phydev->mdio.dev, &priv->sds_lock); + if (ret) + return ret; + + return as22xxx_bringup(phydev); +} + static struct phy_driver as21xxx_drivers[] = { { /* PHY expose in C45 as 0x7500 0x9410 @@ -1282,6 +2525,9 @@ static struct phy_driver as21xxx_drivers[] = { .name = "Aeonsemi AS22XXX", .probe = as22xxx_probe, .match_phy_device = as22xxx_match_phy_device, + .config_init = as22xxx_config_init, + .config_aneg = as22xxx_config_aneg, + .config_inband = as22xxx_config_inband, .read_status = as22xxx_read_status, .led_brightness_set = as21xxx_led_brightness_set, .led_hw_is_supported = as21xxx_led_hw_is_supported,