diff options
| author | Jakub Kicinski <kuba@kernel.org> | 2026-10-21 16:41:39 -0700 |
|---|---|---|
| committer | Jakub Kicinski <kuba@kernel.org> | 2026-10-21 16:41:39 -0700 |
| commit | 68bc53d9bdd201f762a22f67e61449f2bf63f35a (patch) | |
| tree | e9120dd25982518990761fffd5c6917b0cd83b5e | |
| parent | de4aa72d6bb84d1ddb50669ffb7f62164fe49887 (diff) | |
| parent | 5e91edfb0e4512e9b84ffa0264d5872fa600e050 (diff) | |
Merge branch 'net-pcs-add-basic-support-for-rk3568-xpcs'
Coia Prant says:
====================
net: pcs: add basic support for RK3568 XPCS
This series adds proper SGMII support for the Rockchip RK3568 SoC
using the integrated Synopsys DesignWare XPCS, along with necessary
fixes and refactoring in the stmmac core and XPCS driver.
Motivation
==========
The RK3568 integrates a DW XPCS accessed via APB3 and connected to
a Naneng Combo SerDes PHY. Several boards (e.g., Ariaboard
Photonicat) use this interface for Gigabit Ethernet. However, the
current upstream stmmac driver does not support this configuration,
and the XPCS driver has issues in SGMII poll mode that cause the
link to be reported incorrectly.
This series addresses these issues by:
- Adding ANRESTART support for SGMII link recovery
- Adding a Rockchip XPCS platform glue driver and wiring it up in
dwmac-rk
Series overview
===============
RK3568 XPCS/SGMII:
Patch 1: add ANRESTART support for SGMII link recovery
Patch 2: DT binding for Rockchip RK3568 XPCS
Patch 3: implement the Rockchip XPCS platform glue driver
Patch 4: DT binding for Rockchip DWMAC PCS
Patch 5: wire up SGMII support in dwmac-rk
Patch 6: update MAINTAINERS
Dependencies
============
- Generic PM domain (genpd) changes:
https://lore.kernel.org/all/20260925041751.495818-1-coiaprant@gmail.com/
- PHY binding+driver (Naneng Combo PHY SGMII MAC selection):
https://lore.kernel.org/all/20261006162807.2843858-1-coiaprant@gmail.com/
The DTS changes using the PHY property are sent separately to
Heiko/linux-rockchip. There is no hard build dependency; the pieces
can converge during the merge window.
Notes
=====
- When testing out-band mode with set_clk_tx_rate, only 1000Mbps
works on both TX/RX; 10/100Mbps only works on TX side.
Acknowledgments
===============
This work was inspired by and builds upon the excellent work of others:
- Serge Semin's Synopsys DesignWare XPCS platform driver (pcs-xpcs-plat.c)
- Clément Léger's Renesas MIIC driver (pcs-rzn1-miic.c)
- The Rockchip TRM and downstream OEM drivers
====================
Link: https://patch.msgid.link/20261017054406.1828061-1-coiaprant@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
| -rw-r--r-- | Documentation/devicetree/bindings/net/pcs/rockchip,rk3568-xpcs.yaml | 69 | ||||
| -rw-r--r-- | Documentation/devicetree/bindings/net/rockchip-dwmac.yaml | 27 | ||||
| -rw-r--r-- | MAINTAINERS | 9 | ||||
| -rw-r--r-- | drivers/net/ethernet/stmicro/stmmac/Kconfig | 1 | ||||
| -rw-r--r-- | drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c | 131 | ||||
| -rw-r--r-- | drivers/net/pcs/Kconfig | 23 | ||||
| -rw-r--r-- | drivers/net/pcs/Makefile | 5 | ||||
| -rw-r--r-- | drivers/net/pcs/pcs-xpcs-rk.c | 598 | ||||
| -rw-r--r-- | drivers/net/pcs/pcs-xpcs.c | 35 | ||||
| -rw-r--r-- | include/linux/pcs/pcs-xpcs-rk.h | 11 |
10 files changed, 881 insertions, 28 deletions
diff --git a/Documentation/devicetree/bindings/net/pcs/rockchip,rk3568-xpcs.yaml b/Documentation/devicetree/bindings/net/pcs/rockchip,rk3568-xpcs.yaml new file mode 100644 index 000000000000..a331880eb11c --- /dev/null +++ b/Documentation/devicetree/bindings/net/pcs/rockchip,rk3568-xpcs.yaml @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/net/pcs/rockchip,rk3568-xpcs.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Rockchip RK3568 Synopsys DesignWare Ethernet PCS + +maintainers: + - Coia Prant <coiaprant@gmail.com> + +description: | + Rockchip RK3568 SoC integrates a Synopsys DesignWare Ethernet Physical + Coding Sublayer (XPCS). + The PCS provides an interface between the Media Access Control (MAC) + and the Physical Medium Attachment (PMA) sublayer through a Media + Independent Interface (GMII). + + The XPCS is accessed over the APB3 bus and internally connected to a + Naneng Combo SerDes PHY. + It supports 1000BASE-X, SGMII and QSGMII modes. + +properties: + compatible: + const: rockchip,rk3568-xpcs + + reg: + maxItems: 1 + + clocks: + items: + - description: APB3 bus interface clock (clk_csr_i), required for register access + - description: EEE clock (clk_eee_i), required for Energy Efficient Ethernet operation + + clock-names: + items: + - const: csr + - const: eee + + phys: + maxItems: 1 + + power-domains: + maxItems: 1 + +required: + - compatible + - reg + - clocks + - clock-names + - phys + - power-domains + +additionalProperties: false + +examples: + - | + #include <dt-bindings/clock/rk3568-cru.h> + #include <dt-bindings/power/rk3568-power.h> + #include <dt-bindings/phy/phy.h> + + ethernet-pcs@fda00000 { + compatible = "rockchip,rk3568-xpcs"; + reg = <0xfda00000 0x200000>; + clocks = <&cru PCLK_XPCS>, <&cru CLK_XPCS_EEE>; + clock-names = "csr", "eee"; + phys = <&combphy2 PHY_TYPE_SGMII>; + power-domains = <&power RK3568_PD_PIPE>; + }; diff --git a/Documentation/devicetree/bindings/net/rockchip-dwmac.yaml b/Documentation/devicetree/bindings/net/rockchip-dwmac.yaml index 3ec3f6dc2a12..e8dda1961ab3 100644 --- a/Documentation/devicetree/bindings/net/rockchip-dwmac.yaml +++ b/Documentation/devicetree/bindings/net/rockchip-dwmac.yaml @@ -120,6 +120,12 @@ properties: maximum: 0x7F default: 0x10 + pcs-handle: + description: + Specifies a reference to a node representing the PCS device + connected to this GMAC. Required when phy-mode is "sgmii". + maxItems: 1 + phy-supply: description: PHY regulator @@ -162,6 +168,27 @@ allOf: clocks: minItems: 5 + - if: + properties: + compatible: + contains: + const: rockchip,rk3568-gmac + then: + required: + - phy-mode + + - if: + properties: + compatible: + contains: + const: rockchip,rk3568-gmac + phy-mode: + contains: + const: sgmii + then: + required: + - pcs-handle + unevaluatedProperties: false examples: diff --git a/MAINTAINERS b/MAINTAINERS index e7640995959c..a069b893f4d4 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23777,6 +23777,15 @@ S: Maintained F: Documentation/devicetree/bindings/sound/rockchip,rk3576-sai.yaml F: sound/soc/rockchip/rockchip_sai.* +ROCKCHIP XPCS DRIVER +M: Coia Prant <coiaprant@gmail.com> +L: netdev@vger.kernel.org +L: linux-rockchip@lists.infradead.org +S: Maintained +F: Documentation/devicetree/bindings/net/pcs/rockchip,rk3568-xpcs.yaml +F: drivers/net/pcs/pcs-xpcs-rk.c +F: include/linux/pcs/pcs-xpcs-rk.h + ROCKER DRIVER M: Jiri Pirko <jiri@resnulli.us> L: netdev@vger.kernel.org diff --git a/drivers/net/ethernet/stmicro/stmmac/Kconfig b/drivers/net/ethernet/stmicro/stmmac/Kconfig index ab3c4cf96423..c6559e749c0c 100644 --- a/drivers/net/ethernet/stmicro/stmmac/Kconfig +++ b/drivers/net/ethernet/stmicro/stmmac/Kconfig @@ -170,6 +170,7 @@ config DWMAC_ROCKCHIP default ARCH_ROCKCHIP depends on OF && (ARCH_ROCKCHIP || COMPILE_TEST) select MFD_SYSCON + select PCS_XPCS_ROCKCHIP help Support for Ethernet controller on Rockchip RK3288 SoC. diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c index 72bdbcb5e863..c4db919ec164 100644 --- a/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c +++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c @@ -20,6 +20,7 @@ #include <linux/delay.h> #include <linux/mfd/syscon.h> #include <linux/regmap.h> +#include <linux/pcs/pcs-xpcs-rk.h> #include <linux/pm_runtime.h> #include "stmmac_platform.h" @@ -47,6 +48,7 @@ struct rk_gmac_ops { void (*set_to_rgmii)(struct rk_priv_data *bsp_priv, int tx_delay, int rx_delay); void (*set_to_rmii)(struct rk_priv_data *bsp_priv); + void (*set_to_sgmii)(struct rk_priv_data *bsp_priv); int (*set_speed)(struct rk_priv_data *bsp_priv, phy_interface_t interface, int speed); void (*integrated_phy_powerup)(struct rk_priv_data *bsp_priv); @@ -63,6 +65,7 @@ struct rk_gmac_ops { bool clock_grf_reg_in_php; bool supports_rgmii; bool supports_rmii; + bool supports_sgmii; bool php_grf_required; bool regs_valid; u32 regs[]; @@ -98,6 +101,7 @@ struct rk_priv_data { bool integrated_phy; bool supports_rgmii; bool supports_rmii; + bool supports_sgmii; struct clk_bulk_data *clks; int num_clks; @@ -809,6 +813,8 @@ static const struct rk_gmac_ops rk3528_ops = { #define RK3568_GRF_GMAC1_CON1 0x038c /* RK3568_GRF_GMAC0_CON1 && RK3568_GRF_GMAC1_CON1 */ +#define RK3568_GMAC_MODE_RMII_RGMII GRF_CLR_BIT(7) +#define RK3568_GMAC_MODE_SGMII_QSGMII GRF_BIT(7) #define RK3568_GMAC_FLOW_CTRL GRF_BIT(3) #define RK3568_GMAC_FLOW_CTRL_CLR GRF_CLR_BIT(3) #define RK3568_GMAC_RXCLK_DLY_ENABLE GRF_BIT(1) @@ -836,6 +842,16 @@ static int rk3568_init(struct rk_priv_data *bsp_priv) } } +static void rk3568_set_to_rmii(struct rk_priv_data *bsp_priv) +{ + u32 con1; + + con1 = (bsp_priv->id == 1) ? RK3568_GRF_GMAC1_CON1 : + RK3568_GRF_GMAC0_CON1; + + regmap_write(bsp_priv->grf, con1, RK3568_GMAC_MODE_RMII_RGMII); +} + static void rk3568_set_to_rgmii(struct rk_priv_data *bsp_priv, int tx_delay, int rx_delay) { @@ -851,19 +867,31 @@ static void rk3568_set_to_rgmii(struct rk_priv_data *bsp_priv, RK3568_GMAC_CLK_TX_DL_CFG(tx_delay)); regmap_write(bsp_priv->grf, con1, + RK3568_GMAC_MODE_RMII_RGMII | RK3568_GMAC_RXCLK_DLY_ENABLE | RK3568_GMAC_TXCLK_DLY_ENABLE); } +static void rk3568_set_to_sgmii(struct rk_priv_data *bsp_priv) +{ + u32 con1; + + con1 = (bsp_priv->id == 1) ? RK3568_GRF_GMAC1_CON1 : + RK3568_GRF_GMAC0_CON1; + + regmap_write(bsp_priv->grf, con1, RK3568_GMAC_MODE_SGMII_QSGMII); +} + static const struct rk_gmac_ops rk3568_ops = { .init = rk3568_init, + .set_to_rmii = rk3568_set_to_rmii, .set_to_rgmii = rk3568_set_to_rgmii, + .set_to_sgmii = rk3568_set_to_sgmii, + .set_speed = rk_set_clk_mac_speed, .gmac_phy_intf_sel_mask = GENMASK_U16(6, 4), - .supports_rmii = true, - .regs_valid = true, .regs = { 0xfe2a0000, /* gmac0 */ @@ -1211,6 +1239,44 @@ static void rk_phy_powerdown(struct rk_priv_data *bsp_priv) dev_err(bsp_priv->dev, "fail to disable phy-supply\n"); } +static int rk_pcs_init(struct stmmac_priv *priv) +{ + struct rk_priv_data *bsp_priv = priv->plat->bsp_priv; + struct device_node *node = priv->device->of_node; + struct device_node *pcs_node; + struct dw_xpcs *xpcs; + + pcs_node = of_parse_phandle(node, "pcs-handle", 0); + if (!pcs_node) + return -ENODEV; + + xpcs = xpcs_rk_create(priv->device, pcs_node, bsp_priv->id); + of_node_put(pcs_node); + if (IS_ERR(xpcs)) + return PTR_ERR(xpcs); + + priv->hw->xpcs = xpcs; + return 0; +} + +static void rk_pcs_exit(struct stmmac_priv *priv) +{ + if (!priv->hw->xpcs) + return; + + xpcs_destroy(priv->hw->xpcs); + priv->hw->xpcs = NULL; +} + +static struct phylink_pcs *rk_select_pcs(struct stmmac_priv *priv, + phy_interface_t interface) +{ + if (!priv->hw->xpcs) + return NULL; + + return xpcs_to_phylink_pcs(priv->hw->xpcs); +} + static struct rk_priv_data *rk_gmac_setup(struct platform_device *pdev, struct plat_stmmacenet_data *plat, const struct rk_gmac_ops *ops) @@ -1333,6 +1399,7 @@ static struct rk_priv_data *rk_gmac_setup(struct platform_device *pdev, bsp_priv->supports_rgmii = ops->supports_rgmii || !!ops->set_to_rgmii; bsp_priv->supports_rmii = ops->supports_rmii || !!ops->set_to_rmii; + bsp_priv->supports_sgmii = ops->supports_sgmii || !!ops->set_to_sgmii; if (ops->init) { ret = ops->init(bsp_priv); @@ -1364,6 +1431,10 @@ static int rk_gmac_check_ops(struct rk_priv_data *bsp_priv) if (!bsp_priv->supports_rmii) return -EINVAL; break; + case PHY_INTERFACE_MODE_SGMII: + if (!bsp_priv->supports_sgmii) + return -EINVAL; + break; default: dev_err(bsp_priv->dev, "unsupported interface %d", bsp_priv->phy_iface); @@ -1382,16 +1453,19 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv) if (ret) return ret; + ret = gmac_clk_enable(bsp_priv, true); + if (ret) + return ret; + + if (bsp_priv->phy_iface == PHY_INTERFACE_MODE_SGMII) + goto set_mode; + ret = rk_get_phy_intf_sel(bsp_priv->phy_iface); if (ret < 0) - return ret; + goto clk_disable; intf = ret; - ret = gmac_clk_enable(bsp_priv, true); - if (ret) - return ret; - if (bsp_priv->gmac_phy_intf_sel_mask || bsp_priv->gmac_rmii_mode_mask) { /* If defined, encode the phy_intf_sel value */ @@ -1402,10 +1476,8 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv) bsp_priv->gmac_rmii_mode_mask); ret = rk_write_gmac_grf_reg(bsp_priv, val); - if (ret < 0) { - gmac_clk_enable(bsp_priv, false); - return ret; - } + if (ret < 0) + goto clk_disable; } if (bsp_priv->clock.rmii_mode_mask) { @@ -1413,13 +1485,12 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv) bsp_priv->clock.rmii_mode_mask); ret = rk_write_clock_grf_reg(bsp_priv, val); - if (ret < 0) { - gmac_clk_enable(bsp_priv, false); - return ret; - } + if (ret < 0) + goto clk_disable; } - /*rmii or rgmii*/ +set_mode: + /* rmii, rgmii, sgmii */ switch (bsp_priv->phy_iface) { case PHY_INTERFACE_MODE_RGMII: dev_info(dev, "init for RGMII\n"); @@ -1450,15 +1521,20 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv) if (bsp_priv->ops->set_to_rmii) bsp_priv->ops->set_to_rmii(bsp_priv); break; + case PHY_INTERFACE_MODE_SGMII: + dev_info(dev, "init for SGMII\n"); + if (bsp_priv->ops->set_to_sgmii) + bsp_priv->ops->set_to_sgmii(bsp_priv); + break; default: dev_err(dev, "NO interface defined!\n"); + ret = -EINVAL; + goto clk_disable; } ret = rk_phy_powerup(bsp_priv); - if (ret) { - gmac_clk_enable(bsp_priv, false); - return ret; - } + if (ret) + goto clk_disable; pm_runtime_get_sync(dev); @@ -1466,6 +1542,10 @@ static int rk_gmac_powerup(struct rk_priv_data *bsp_priv) bsp_priv->ops->integrated_phy_powerup(bsp_priv); return 0; + +clk_disable: + gmac_clk_enable(bsp_priv, false); + return ret; } static void rk_gmac_powerdown(struct rk_priv_data *gmac) @@ -1605,6 +1685,17 @@ static int rk_gmac_probe(struct platform_device *pdev) plat_dat->suspend = rk_gmac_suspend; plat_dat->resume = rk_gmac_resume; + if (plat_dat->phy_interface == PHY_INTERFACE_MODE_SGMII) { + /* SGMII clock always runs at 125 MHz */ + plat_dat->set_clk_tx_rate = NULL; + + /* SGMII requires a PCS */ + plat_dat->default_an_inband = true; + plat_dat->pcs_init = rk_pcs_init; + plat_dat->pcs_exit = rk_pcs_exit; + plat_dat->select_pcs = rk_select_pcs; + } + plat_dat->bsp_priv = rk_gmac_setup(pdev, plat_dat, data); if (IS_ERR(plat_dat->bsp_priv)) return PTR_ERR(plat_dat->bsp_priv); diff --git a/drivers/net/pcs/Kconfig b/drivers/net/pcs/Kconfig index e417fd66f660..108d47257bee 100644 --- a/drivers/net/pcs/Kconfig +++ b/drivers/net/pcs/Kconfig @@ -12,6 +12,29 @@ config PCS_XPCS This module provides a driver and helper functions for Synopsys DesignWare XPCS controllers. +if PCS_XPCS + +config PCS_XPCS_PLATFORM + tristate "Generic XPCS controller support" + help + Generic DWXPCS driver for platforms that don't require any + platform specific code to function or is using platform + data for setup. + + If you have a controller with this interface, say Y or M here. + +config PCS_XPCS_ROCKCHIP + tristate "Rockchip XPCS controller support" + depends on OF && (ARCH_ROCKCHIP || COMPILE_TEST) + select GENERIC_PHY + select PM_GENERIC_DOMAINS if PM + help + Support for XPCS controller on Rockchip RK356x SoC. + + If you have a Rockchip SoC with this interface, say Y or M here. + +endif # PCS_XPCS + config PCS_LYNX tristate help diff --git a/drivers/net/pcs/Makefile b/drivers/net/pcs/Makefile index 4f7920618b90..f9f6cf2578d7 100644 --- a/drivers/net/pcs/Makefile +++ b/drivers/net/pcs/Makefile @@ -1,10 +1,11 @@ # SPDX-License-Identifier: GPL-2.0 # Makefile for Linux PCS drivers -pcs_xpcs-$(CONFIG_PCS_XPCS) := pcs-xpcs.o pcs-xpcs-plat.o \ - pcs-xpcs-nxp.o pcs-xpcs-wx.o +pcs_xpcs-$(CONFIG_PCS_XPCS) := pcs-xpcs.o pcs-xpcs-nxp.o pcs-xpcs-wx.o obj-$(CONFIG_PCS_XPCS) += pcs_xpcs.o +obj-$(CONFIG_PCS_XPCS_PLATFORM) += pcs-xpcs-plat.o +obj-$(CONFIG_PCS_XPCS_ROCKCHIP) += pcs-xpcs-rk.o obj-$(CONFIG_PCS_LYNX) += pcs-lynx.o obj-$(CONFIG_PCS_MTK_LYNXI) += pcs-mtk-lynxi.o obj-$(CONFIG_PCS_RZN1_MIIC) += pcs-rzn1-miic.o diff --git a/drivers/net/pcs/pcs-xpcs-rk.c b/drivers/net/pcs/pcs-xpcs-rk.c new file mode 100644 index 000000000000..698a43b75334 --- /dev/null +++ b/drivers/net/pcs/pcs-xpcs-rk.c @@ -0,0 +1,598 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Rockchip XPCS platform device driver + * + * Based on the Synopsys DesignWare XPCS platform driver. + * Copyright (C) 2024 Serge Semin + * + * Adapted for Rockchip SoCs, with reference to the Rockchip OEM driver. + * Copyright (C) 2026 Coia Prant + */ + +#include <linux/atomic.h> +#include <linux/bitfield.h> +#include <linux/clk.h> +#include <linux/device.h> +#include <linux/io.h> +#include <linux/iopoll.h> +#include <linux/math.h> +#include <linux/mdio.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/pcs/pcs-xpcs-rk.h> +#include <linux/phy.h> +#include <linux/phy/phy.h> +#include <linux/platform_device.h> +#include <linux/pm_domain.h> +#include <linux/pm_runtime.h> +#include <linux/property.h> +#include <linux/sizes.h> +#include <linux/time.h> + +#include "pcs-xpcs.h" + +struct dw_xpcs_rk { + struct platform_device *pdev; + struct mii_bus *bus; + void __iomem *reg_base; + struct phy *serdes_phy; + struct clk *csr_clk; + struct clk *eee_clk; + u8 eee_mult_fact; +}; + +static ptrdiff_t xpcs_rk_addr_format(int dev, int reg) +{ + return FIELD_PREP(0x70000, dev) | FIELD_PREP(0xffff, reg); +} + +static int xpcs_rk_read_reg(struct dw_xpcs_rk *pxpcs, int dev, int reg) +{ + ptrdiff_t csr; + int ret; + + csr = xpcs_rk_addr_format(dev, reg); + + ret = pm_runtime_resume_and_get(&pxpcs->pdev->dev); + if (ret) + return ret; + + ret = readl(pxpcs->reg_base + (csr << 2)) & 0xffff; + + pm_runtime_put(&pxpcs->pdev->dev); + return ret; +} + +static int xpcs_rk_write_reg(struct dw_xpcs_rk *pxpcs, int dev, int reg, u16 val) +{ + ptrdiff_t csr; + int ret; + + csr = xpcs_rk_addr_format(dev, reg); + + ret = pm_runtime_resume_and_get(&pxpcs->pdev->dev); + if (ret) + return ret; + + writel(val, pxpcs->reg_base + (csr << 2)); + + pm_runtime_put(&pxpcs->pdev->dev); + return 0; +} + +#define ROCKCHIP_MMD_MII1 2 +#define ROCKCHIP_MMD_MII2 3 +#define ROCKCHIP_MMD_MII3 4 +#define ROCKCHIP_MMD_PMAPMD 6 +#define ROCKCHIP_MMD_MII 7 + +static bool xpcs_rk_mdio_addr_validate(int addr) +{ + return !(addr < 0 || addr > 3); +} + +static int xpcs_rk_mdio_read_remapping(int addr, int dev) +{ + switch (dev) { + case MDIO_MMD_PMAPMD: + return ROCKCHIP_MMD_PMAPMD; + case MDIO_MMD_VEND2: + break; + default: + return -ENXIO; + } + + /* + * Reads are redirected by hardware to the port's read-only mirror; + * only writes have to be targeted at MII (see the write path). + */ + switch (addr) { + case 0: + return ROCKCHIP_MMD_MII; + case 1: + return ROCKCHIP_MMD_MII1; + case 2: + return ROCKCHIP_MMD_MII2; + case 3: + return ROCKCHIP_MMD_MII3; + default: + return -ENODEV; + } +} + +static int xpcs_rk_mdio_write_remapping(int addr, int dev, int reg) +{ + switch (dev) { + case MDIO_MMD_PMAPMD: + return ROCKCHIP_MMD_PMAPMD; + case MDIO_MMD_VEND2: + break; + default: + return -ENXIO; + } + + /* + * These registers physically live only in MII (the management port). + * Ports 1-3 expose read-only mirrors of these bits, so writes must + * always target MII; the read path remaps per address and the + * hardware redirects to the port's mirror. + */ + switch (reg) { + case DW_VR_MII_AN_CTRL: + case DW_VR_MII_AN_INTR_STS: + case DW_VR_MII_EEE_MCTRL0: + case DW_VR_MII_EEE_MCTRL1: + case DW_VR_MII_DIG_CTRL2: + return ROCKCHIP_MMD_MII; + default: + break; + } + + switch (addr) { + case 0: + return ROCKCHIP_MMD_MII; + case 1: + return ROCKCHIP_MMD_MII1; + case 2: + return ROCKCHIP_MMD_MII2; + case 3: + return ROCKCHIP_MMD_MII3; + default: + return -ENODEV; + } +} + +static int xpcs_rk_read_c22(struct mii_bus *bus, int addr, int reg) +{ + struct dw_xpcs_rk *pxpcs = bus->priv; + int dev; + + if (!xpcs_rk_mdio_addr_validate(addr)) + return -ENODEV; + + dev = xpcs_rk_mdio_read_remapping(addr, MDIO_MMD_VEND2); + if (dev < 0) + return 0xffff; + + return xpcs_rk_read_reg(pxpcs, dev, reg); +} + +static int xpcs_rk_write_c22(struct mii_bus *bus, int addr, int reg, u16 val) +{ + struct dw_xpcs_rk *pxpcs = bus->priv; + int dev; + + if (!xpcs_rk_mdio_addr_validate(addr)) + return -ENODEV; + + dev = xpcs_rk_mdio_write_remapping(addr, MDIO_MMD_VEND2, reg); + if (dev < 0) + return 0; + + return xpcs_rk_write_reg(pxpcs, dev, reg, val); +} + +static int xpcs_rk_read_c45(struct mii_bus *bus, int addr, int dev, int reg) +{ + struct dw_xpcs_rk *pxpcs = bus->priv; + + if (!xpcs_rk_mdio_addr_validate(addr)) + return -ENODEV; + + dev = xpcs_rk_mdio_read_remapping(addr, dev); + if (dev < 0) + return 0xffff; + + return xpcs_rk_read_reg(pxpcs, dev, reg); +} + +static int xpcs_rk_write_c45(struct mii_bus *bus, int addr, int dev, int reg, u16 val) +{ + struct dw_xpcs_rk *pxpcs = bus->priv; + + if (!xpcs_rk_mdio_addr_validate(addr)) + return -ENODEV; + + dev = xpcs_rk_mdio_write_remapping(addr, dev, reg); + if (dev < 0) + return 0; + + return xpcs_rk_write_reg(pxpcs, dev, reg, val); +} + +static struct dw_xpcs_rk *xpcs_rk_create_data(struct platform_device *pdev) +{ + struct dw_xpcs_rk *pxpcs; + + pxpcs = devm_kzalloc(&pdev->dev, sizeof(*pxpcs), GFP_KERNEL); + if (!pxpcs) + return ERR_PTR(-ENOMEM); + + pxpcs->pdev = pdev; + + dev_set_drvdata(&pdev->dev, pxpcs); + + return pxpcs; +} + +static int xpcs_rk_serdes_phy_init(struct dw_xpcs_rk *pxpcs) +{ + struct device *dev = &pxpcs->pdev->dev; + + pxpcs->serdes_phy = devm_phy_get(dev, NULL); + if (IS_ERR(pxpcs->serdes_phy)) + return dev_err_probe(dev, PTR_ERR(pxpcs->serdes_phy), + "Failed to get SerDes PHY\n"); + + return 0; +} + +static void xpcs_rk_serdes_phy_poweroff(void *data) +{ + struct dw_xpcs_rk *pxpcs = data; + struct device *dev = &pxpcs->pdev->dev; + + phy_power_off(pxpcs->serdes_phy); + phy_exit(pxpcs->serdes_phy); + + dev_pm_genpd_rpm_always_on(dev, false); +} + +static int xpcs_rk_serdes_phy_poweron(struct dw_xpcs_rk *pxpcs) +{ + struct device *dev = &pxpcs->pdev->dev; + int ret; + + /* + * The power domain is required and must be enabled, which allows us to + * dynamically turn the CSR clock on/off using PM while keeping the PCS + * powered on. + */ + ret = dev_pm_genpd_rpm_always_on(dev, true); + if (ret) { + dev_err(dev, "Failed to power on power-domains\n"); + return ret; + } + + ret = phy_init(pxpcs->serdes_phy); + if (ret) { + dev_err(dev, "Failed to init SerDes PHY\n"); + goto pm_domain; + } + + ret = phy_power_on(pxpcs->serdes_phy); + if (ret) { + dev_err(dev, "Failed to power on SerDes PHY\n"); + goto serdes_phy; + } + + ret = devm_add_action_or_reset(dev, xpcs_rk_serdes_phy_poweroff, pxpcs); + if (ret) { + dev_err(dev, "Failed to register devm for SerDes PHY: %d\n", ret); + return ret; + } + + return 0; + +serdes_phy: + phy_exit(pxpcs->serdes_phy); +pm_domain: + dev_pm_genpd_rpm_always_on(dev, false); + return ret; +} + +static int xpcs_rk_init_res(struct dw_xpcs_rk *pxpcs) +{ + struct platform_device *pdev = pxpcs->pdev; + struct device *dev = &pdev->dev; + struct resource *res; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) { + dev_err(dev, "No reg-space found\n"); + return -EINVAL; + } + + if (resource_size(res) < SZ_2M) { + dev_err(dev, "Invalid reg-space size\n"); + return -EINVAL; + } + + pxpcs->reg_base = devm_ioremap_resource(dev, res); + if (IS_ERR(pxpcs->reg_base)) { + dev_err(dev, "Failed to map reg-space\n"); + return PTR_ERR(pxpcs->reg_base); + } + + return 0; +} + +static void xpcs_rk_exit_clk(void *data) +{ + struct dw_xpcs_rk *pxpcs = data; + struct device *dev = &pxpcs->pdev->dev; + + pm_runtime_force_suspend(dev); + clk_disable_unprepare(pxpcs->eee_clk); +} + +static int xpcs_rk_init_clk(struct dw_xpcs_rk *pxpcs) +{ + struct device *dev = &pxpcs->pdev->dev; + unsigned long rate; + u64 mult; + int ret; + + pxpcs->csr_clk = devm_clk_get(dev, "csr"); + if (IS_ERR(pxpcs->csr_clk)) + return dev_err_probe(dev, PTR_ERR(pxpcs->csr_clk), + "Failed to get CSR clock\n"); + + pxpcs->eee_clk = devm_clk_get(dev, "eee"); + if (IS_ERR(pxpcs->eee_clk)) + return dev_err_probe(dev, PTR_ERR(pxpcs->eee_clk), + "Failed to get EEE clock\n"); + + ret = clk_prepare_enable(pxpcs->eee_clk); + if (ret) { + dev_err(dev, "Failed to enable EEE clock\n"); + return ret; + } + + pm_runtime_set_suspended(dev); + pm_runtime_enable(dev); + + ret = devm_add_action_or_reset(dev, xpcs_rk_exit_clk, pxpcs); + if (ret) { + dev_err(dev, "Failed to register devm for EEE clock: %d\n", ret); + return ret; + } + + /* + * Compute the multiplier for the EEE clock so that + * clk_eee_period * (mult_fact + 1) falls within 80..120 ns. + * + * On RK3568, clk_xpcs_eee is muxed between gpll200 (200 MHz, 5 ns) + * and cpll125 (125 MHz, 8 ns), selected by CRU_CLKSEL_CON29 bit 13. + * The reset value is 0 (200 MHz), but derive the value at runtime to + * stay correct if the mux is changed by a board. + * + * Use a 64-bit intermediate: on 32-bit builds, 100 * 200000000 + * does not fit in unsigned long. Clamp to the 4-bit + * DW_VR_MII_EEE_MULT_FACT_100NS field. The mux only provides + * 125 MHz or 200 MHz, so the rate cannot drop below the 5 MHz + * threshold where DIV_ROUND_CLOSEST_ULL() would return 0 and the + * subtraction below would underflow. + */ + rate = clk_get_rate(pxpcs->eee_clk); + if (!rate) + return dev_err_probe(dev, -EINVAL, "Invalid EEE clock rate\n"); + + mult = DIV_ROUND_CLOSEST_ULL(100ULL * rate, NSEC_PER_SEC) - 1; + pxpcs->eee_mult_fact = min_t(u64, mult, 15); + return 0; +} + +static int xpcs_rk_init_bus(struct dw_xpcs_rk *pxpcs) +{ + struct device *dev = &pxpcs->pdev->dev; + static atomic_t id = ATOMIC_INIT(-1); + struct mii_bus *bus; + int ret; + + bus = devm_mdiobus_alloc_size(dev, 0); + if (!bus) + return -ENOMEM; + + bus->name = "Rockchip DW XPCS MCI/APB3"; + bus->read = xpcs_rk_read_c22; + bus->write = xpcs_rk_write_c22; + bus->read_c45 = xpcs_rk_read_c45; + bus->write_c45 = xpcs_rk_write_c45; + bus->phy_mask = ~0; + bus->parent = dev; + bus->priv = pxpcs; + + snprintf(bus->id, MII_BUS_ID_SIZE, + "rockchip_dwxpcs-%x", atomic_inc_return(&id)); + + /* + * MDIO-bus here serves as just a back-end engine abstracting out + * the MDIO and MCI/APB3 IO interfaces utilized for the Rockchip DWXPCS CSRs + * access. + */ + ret = devm_mdiobus_register(dev, bus); + if (ret) { + dev_err(dev, "Failed to create MDIO bus\n"); + return ret; + } + + pxpcs->bus = bus; + return 0; +} + +static int xpcs_rk_probe(struct platform_device *pdev) +{ + struct dw_xpcs_rk *pxpcs; + int ret; + + pxpcs = xpcs_rk_create_data(pdev); + if (IS_ERR(pxpcs)) + return PTR_ERR(pxpcs); + + /* + * The XPCS lives in the PD_PIPE power domain. The domain must be + * powered on before any register access, otherwise the SoC will + * trigger a synchronous external abort (SError). + * + * Accessing the XPCS registers also requires a TX clock from the + * SerDes, which is needed for the soft reset. + */ + ret = xpcs_rk_serdes_phy_init(pxpcs); + if (ret) + return ret; + + ret = xpcs_rk_serdes_phy_poweron(pxpcs); + if (ret) + return ret; + + ret = xpcs_rk_init_res(pxpcs); + if (ret) + return ret; + + ret = xpcs_rk_init_clk(pxpcs); + if (ret) + return ret; + + ret = xpcs_rk_init_bus(pxpcs); + if (ret) + return ret; + + return 0; +} + +static const struct of_device_id xpcs_rk_of_ids[] = { + { .compatible = "rockchip,rk3568-xpcs" }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, xpcs_rk_of_ids); + +struct dw_xpcs *xpcs_rk_create(struct device *dev, struct device_node *pcs, int port) +{ + struct platform_device *pdev; + struct dw_xpcs_rk *pxpcs; + struct dw_xpcs *xpcs; + + if (!of_device_is_available(pcs)) + return ERR_PTR(-ENODEV); + + if (!of_match_node(xpcs_rk_of_ids, pcs)) + return ERR_PTR(-EINVAL); + + if (!xpcs_rk_mdio_addr_validate(port)) + return ERR_PTR(-EINVAL); + + pdev = of_find_device_by_node(pcs); + if (!pdev) + return ERR_PTR(-EPROBE_DEFER); + + /* + * Establish the device link before reading the supplier's drvdata. + * device_link_add() does not fail on a supplier that is unbinding; + * it creates the link in DL_STATE_SUPPLIER_UNBIND. + * + * The check below only confirms that the supplier is bound at this + * point. It does not fully protect against a concurrent unbind + * between here and xpcs_create_mdiodev(); closing that window would + * require reading a managed link's status or device_lock(), neither + * of which is acceptable on this path. The window is small and + * requires an explicit unbind from userspace. Discussed on + * driver-core. + * + * The link is released automatically when the consumer device is + * destroyed (DL_FLAG_AUTOREMOVE_CONSUMER), so no explicit + * device_link_remove() is needed on the failure paths. + */ + if (!device_link_add(dev, &pdev->dev, DL_FLAG_AUTOREMOVE_CONSUMER)) { + put_device(&pdev->dev); + return ERR_PTR(-EPROBE_DEFER); + } + + if (READ_ONCE(pdev->dev.links.status) != DL_DEV_DRIVER_BOUND) { + put_device(&pdev->dev); + return ERR_PTR(-EPROBE_DEFER); + } + + pxpcs = platform_get_drvdata(pdev); + if (!pxpcs || !pxpcs->bus) { + put_device(&pdev->dev); + return ERR_PTR(-EPROBE_DEFER); + } + + xpcs = xpcs_create_mdiodev(pxpcs->bus, port); + if (IS_ERR(xpcs)) { + put_device(&pdev->dev); + return xpcs; + } + + xpcs_config_eee_mult_fact(xpcs, pxpcs->eee_mult_fact); + put_device(&pdev->dev); + return xpcs; +} +EXPORT_SYMBOL_GPL(xpcs_rk_create); + +static int xpcs_rk_pm_runtime_suspend(struct device *dev) +{ + struct dw_xpcs_rk *pxpcs = dev_get_drvdata(dev); + + clk_disable_unprepare(pxpcs->csr_clk); + + return 0; +} + +static int xpcs_rk_pm_runtime_resume(struct device *dev) +{ + struct dw_xpcs_rk *pxpcs = dev_get_drvdata(dev); + + return clk_prepare_enable(pxpcs->csr_clk); +} + +static int xpcs_rk_system_suspend(struct device *dev) +{ + /* + * Keep the PD_PIPE power domain on during system suspend. + * + * PD_PIPE is shared with SATA/PCIe and would be powered down by + * genpd once all its consumers are suspended, killing the SerDes + * and breaking MAC WoL. Mark the XPCS as part of the wakeup path + * so genpd keeps the domain on. Unconditional because the XPCS + * core has no callback to convey the MAC WoL state. + */ + device_set_wakeup_path(dev); + return 0; +} + +static int xpcs_rk_system_resume(struct device *dev) +{ + return 0; +} + +static _DEFINE_DEV_PM_OPS(xpcs_rk_pm_ops, + xpcs_rk_system_suspend, xpcs_rk_system_resume, + xpcs_rk_pm_runtime_suspend, xpcs_rk_pm_runtime_resume, + NULL); + +static struct platform_driver xpcs_rk_driver = { + .probe = xpcs_rk_probe, + .driver = { + .name = "rk_xpcs-dwxpcs", + .pm = pm_ptr(&xpcs_rk_pm_ops), + .of_match_table = xpcs_rk_of_ids, + }, +}; +module_platform_driver(xpcs_rk_driver); + +MODULE_DESCRIPTION("Rockchip XPCS platform device driver"); +MODULE_AUTHOR("Coia Prant <coiaprant@gmail.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/net/pcs/pcs-xpcs.c b/drivers/net/pcs/pcs-xpcs.c index b415b93d77c1..6466e0ff2a98 100644 --- a/drivers/net/pcs/pcs-xpcs.c +++ b/drivers/net/pcs/pcs-xpcs.c @@ -761,7 +761,9 @@ static int xpcs_config_aneg_c37_sgmii(struct dw_xpcs *xpcs, * DW xPCS used with DW EQoS MAC is always MAC side SGMII. * 4) VR_MII_DIG_CTRL1 Bit(9) [MAC_AUTO_SW] = 1b (Automatic * speed/duplex mode change by HW after SGMII AN complete) - * 5) VR_MII_MMD_CTRL Bit(12) [AN_ENABLE] = 1b (Enable SGMII AN) + * 5) VR_MII_AN_INTR_STS = 0x0 (Clear CL37 AN complete status) + * 6) VR_MII_MMD_CTRL Bit(12) [AN_ENABLE] = 1b (Enable SGMII AN) + * VR_MII_MMD_CTRL Bit(9) [AN_RESTART] = 1b (Restart SGMII AN) * * Note that VR_MII_MMD_CTRL is MII_BMCR. * @@ -769,7 +771,14 @@ static int xpcs_config_aneg_c37_sgmii(struct dw_xpcs *xpcs, * SR_MII_AN_ADV. MAC side SGMII receives AN Tx Config from * PHY about the link state change after C28 AN is completed * between PHY and Link Partner. There is also no need to - * trigger AN restart for MAC-side SGMII. + * trigger AN restart for MAC-side SGMII on most devices. + * + * Note: While the DesignWare databook states that AN restart is + * not needed for MAC side SGMII, some implementations (e.g. + * Rockchip RK3568) exhibit a timing quirk when integrated with + * phylink and do not restart AN automatically when the link + * comes back up. An explicit AN restart is required on those + * parts to recover the link after a disconnect. */ mdio_ctrl = xpcs_read(xpcs, MDIO_MMD_VEND2, MII_BMCR); if (mdio_ctrl < 0) @@ -816,9 +825,14 @@ static int xpcs_config_aneg_c37_sgmii(struct dw_xpcs *xpcs, if (ret < 0) return ret; + /* Clear CL37 AN complete status */ + ret = xpcs_write(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_INTR_STS, 0); + if (ret < 0) + return ret; + if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) ret = xpcs_write(xpcs, MDIO_MMD_VEND2, MII_BMCR, - mdio_ctrl | BMCR_ANENABLE); + mdio_ctrl | BMCR_ANENABLE | BMCR_ANRESTART); return ret; } @@ -1093,9 +1107,18 @@ static int xpcs_get_state_c37_sgmii(struct dw_xpcs *xpcs, return 0; } - /* Clear AN complete status or interrupt */ - if (state->an_complete) - xpcs_write(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_INTR_STS, 0); + if (state->an_complete) { + /* Clear AN complete status or interrupt */ + ret = xpcs_write(xpcs, MDIO_MMD_VEND2, DW_VR_MII_AN_INTR_STS, 0); + if (ret < 0) + return ret; + + /* Initiate the next round of AN */ + ret = xpcs_modify(xpcs, MDIO_MMD_VEND2, MII_BMCR, BMCR_ANRESTART, + BMCR_ANRESTART); + if (ret < 0) + return ret; + } return 0; } diff --git a/include/linux/pcs/pcs-xpcs-rk.h b/include/linux/pcs/pcs-xpcs-rk.h new file mode 100644 index 000000000000..e3ca3d51f7c7 --- /dev/null +++ b/include/linux/pcs/pcs-xpcs-rk.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __LINUX_PCS_XPCS_ROCKCHIP_H +#define __LINUX_PCS_XPCS_ROCKCHIP_H + +#include <linux/device.h> +#include <linux/of.h> +#include <linux/pcs/pcs-xpcs.h> + +struct dw_xpcs *xpcs_rk_create(struct device *dev, struct device_node *pcs, int port); + +#endif /* __LINUX_PCS_XPCS_ROCKCHIP_H */ |
