From c3a1b6763f4d76d69cb00e4e3a82ce7e18729507 Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:27 +0200 Subject: [PATCH 01/13] dt-bindings: net: realtek,rtl9301-mdio: Add RTL83xx series The lower end Realtek Otto switches provide 1G only and are divided into two series: - Maple : RTL838x up to 28 ports - Cypress: RTL839x up to 52 ports The Maple based devices have 3 different SoCs: RTL8380, RTL8381 and RTL8382. The Cypress series consists of the RTL8391, RTL8392 and RTL8393 SoCs. The MDIO controller of these switches works like the existing RTL93xx logic but has different characteristics and different registers. Add new compatibles in the device tree. With the extended compatibility list change the title to better reflect its scope. Especially add the "Ethernet" tag as these devices have multiple MDIO controllers. Signed-off-by: Markus Stockhausen Acked-by: Conor Dooley Link: https://patch.msgid.link/20260831143439.2404484-2-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- .../bindings/net/realtek,rtl9301-mdio.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Documentation/devicetree/bindings/net/realtek,rtl9301-mdio.yaml b/Documentation/devicetree/bindings/net/realtek,rtl9301-mdio.yaml index 271e05bae9c5..67e0b23a8470 100644 --- a/Documentation/devicetree/bindings/net/realtek,rtl9301-mdio.yaml +++ b/Documentation/devicetree/bindings/net/realtek,rtl9301-mdio.yaml @@ -4,7 +4,7 @@ $id: http://devicetree.org/schemas/net/realtek,rtl9301-mdio.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: Realtek RTL9300 MDIO Controller +title: Realtek Otto Switches Ethernet MDIO Controller maintainers: - Chris Packham @@ -12,6 +12,16 @@ maintainers: properties: compatible: oneOf: + - items: + - enum: + - realtek,rtl8381-mdio + - realtek,rtl8382-mdio + - const: realtek,rtl8380-mdio + - items: + - enum: + - realtek,rtl8392-mdio + - realtek,rtl8393-mdio + - const: realtek,rtl8391-mdio - items: - enum: - realtek,rtl9302b-mdio @@ -24,6 +34,8 @@ properties: - realtek,rtl9313-mdio - const: realtek,rtl9311-mdio - enum: + - realtek,rtl8380-mdio + - realtek,rtl8391-mdio - realtek,rtl9301-mdio - realtek,rtl9311-mdio From 1c86cd6937165b8ea702d8931d09f1df238c2a44 Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:28 +0200 Subject: [PATCH 02/13] net: mdio: realtek-rtl9300: Add polling documentation Add a detailed explanation how the hardware polling unit in the Realtek Otto switches works. This simplifies developing future patches and reviewing them. Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-3-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/mdio/mdio-realtek-rtl9300.c | 75 +++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/drivers/net/mdio/mdio-realtek-rtl9300.c b/drivers/net/mdio/mdio-realtek-rtl9300.c index afd52a1cd7f8..73ac5fdcd267 100644 --- a/drivers/net/mdio/mdio-realtek-rtl9300.c +++ b/drivers/net/mdio/mdio-realtek-rtl9300.c @@ -35,6 +35,81 @@ * * The driver works out the mapping based on the MDIO bus described in device tree and phandles on * the ethernet-ports property. + * + * The devices have a hardware polling unit that runs in the background without any CPU load. It + * constantly scans the MDIO bus and the attached PHYs and updates the MAC status registers. + * + * How does the polling work? + * + * Each device has a SMI_POLL_CTRL register. A per-port bitmask decides if the hardware polling of + * the associated bus/address is active or not. The hardware runs a tight loop over this and for + * each set polling bit it issues a status check for the PHY. Attaching a logic analyzer to the + * MDIO bus of an RTL8380 and RTL8393 gives the following commands (in kernel notation): + * + * RTL8380 RTL8393 + * --------------------------- --------------------------- + * phy_write(phy, 31, 0x0); phy_read(phy, 0); + * phy_write(phy, 13, 0x7); phy_read(phy, 1); + * phy_write(phy, 14, 0x3c); phy_read(phy, 4); + * phy_write(phy, 13, 0x8007); phy_read(phy, 5); + * phy_read(phy, 14); phy_read(phy, 6); + * phy_write(phy, 13, 0x7); phy_read(phy, 9); + * phy_write(phy, 14, 0x3d); phy_read(phy, 10); + * phy_write(phy, 13, 0x8007); phy_read(phy, 15); + * phy_read(phy, 14); phy_write(phy, 13, 0x7); + * phy_read(phy, 9); phy_write(phy, 14, 0x3c); + * phy_read(phy, 10); phy_write(phy, 13, 0x4007); + * phy_read(phy, 15); phy_read(phy, 14); + * phy_read(phy, 0); phy_write(phy, 13, 0x7); + * phy_read(phy, 1); phy_write(phy, 14, 0x3d); + * phy_read(phy, 4); phy_write(phy, 13, 0x4007); + * phy_read(phy, 5); phy_read(phy, 14); + * phy_read(phy, 6); + * + * After one PHY status is read, the polling engine goes over to the next PHY. If one bus is fully + * scanned it switches over to the next bus. Basically the polling system is always busy and the + * MAC state is updated in real-time. + * + * This is a glimpse look at the complexity of the polling and leaves out the C45 case and the MAC + * register update logic afterwards. Important to note: + * + * - 100 MBit downshifts (broken cables) are identified and propagated to the MAC correctly + * - Access to MDIO_AN_EEE_ADV and MDIO_AN_EEE_LPABLE works via C45 over C22. + * - It is unclear if these sequences change for different PHYs. + * - Access to Realtek reserved register 26 (link speed) has not yet been seen. + * + * How does MDIO access from kernel work? + * + * When issuing MDIO accesses via an MMIO based interface the final write to the command register + * sets a "run command now" bit. Between two polling sequences for different PHYs the hardware + * checks if a user command needs to run and sends it onto the bus. Afterwards it simply continues + * its polling work. Inspecting the command sequence for a paged write on the logic analyzer gives: + * + * RTL8380 RTL8393 + * --------------------------- --------------------------- + * phy_write(phy, 31, page); phy_write(phy, 31, page); + * phy_write(phy, reg, value); phy_write(phy, reg, value); + * phy_write(phy, 31, 0); + * + * What does this mean? + * + * There are slight differences in polling and PHY access between the models but the challenge + * stays the same. On the one hand that greatly simplifies the MAC layer, on the other hand it + * has some implications for the kernel PHY subsystem. + * + * - Without the polling and a proper MAC status, some of the link handling features do not work. + * Especially an unpopulated MAC_LINK_STS register cancels operations to other MAC registers. + * - The Realtek page register 31 is magically modified in the background so that polling will + * read the right data. On the RTL838x polling simply resets it to zero. Other devices seem + * to track the page access "magically" in the background. + * - A C45 over C22 kernel access sequence is most likely to fail because chances are high that + * the polling engine overwrites registers 13/14 in between. + * - PHY firmware loading can have issues. Especially if a PHY is designed to expect a clean + * sequence of registers and values without deviation. + * - An access to one PHY will need to wait for the next free slot of the polling engine. + * + * Conclusion: The Realtek MDIO bus driver PHY access must know and handle any interference that + * arises from the above described hardware polling. */ #include From 3383b50c1695f7a7f0895b235706a3f2acf4ab11 Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:29 +0200 Subject: [PATCH 03/13] net: mdio: realtek-rtl9300: deny C45 over C22 access Hardware polling breaks C45 over C22 PHY access. Currently this is accepted by the driver but it silently fails with undefined results. Make the situation clear to the caller by adding checks, returning an error and giving a one-time warning message. Suggested-by: Andrew Lunn Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-4-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/mdio/mdio-realtek-rtl9300.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/drivers/net/mdio/mdio-realtek-rtl9300.c b/drivers/net/mdio/mdio-realtek-rtl9300.c index 73ac5fdcd267..69565a87f99d 100644 --- a/drivers/net/mdio/mdio-realtek-rtl9300.c +++ b/drivers/net/mdio/mdio-realtek-rtl9300.c @@ -115,6 +115,7 @@ #include #include #include +#include #include #include #include @@ -453,6 +454,12 @@ static int otto_emdio_read_c22(struct mii_bus *bus, int phy_id, int regnum) int ret, port; u32 value; + if (regnum == MII_MMD_CTRL || regnum == MII_MMD_DATA) { + dev_warn_once(&bus->dev, + "C45 over C22 read access broken due to polling\n"); + return -EOPNOTSUPP; + } + port = otto_emdio_phy_to_port(bus, phy_id); if (port < 0) return port; @@ -463,11 +470,18 @@ static int otto_emdio_read_c22(struct mii_bus *bus, int phy_id, int regnum) return ret ? ret : value; } -static int otto_emdio_write_c22(struct mii_bus *bus, int phy_id, int regnum, u16 value) +static int otto_emdio_write_c22(struct mii_bus *bus, int phy_id, int regnum, + u16 value) { struct otto_emdio_priv *priv = otto_emdio_bus_to_priv(bus); int ret, port; + if (regnum == MII_MMD_CTRL || regnum == MII_MMD_DATA) { + dev_warn_once(&bus->dev, + "C45 over C22 write access broken due to polling\n"); + return -EOPNOTSUPP; + } + port = otto_emdio_phy_to_port(bus, phy_id); if (port < 0) return port; From 2df76677c1c08f622cc577a49f0645a5976d7c91 Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:30 +0200 Subject: [PATCH 04/13] net: phy: add phy_detach_internal() helper For the upcoming MDIO bus notification enhancements the PHY detach path needs to inform the bus about these actions. Until now phy_detach() is used for the regular detach path as well as from the phy_attach_direct() error paths So in the future there must be a clear indication if a notification is needed (normal path) or not (cleanup path). Carve out the phy_detach() code into a new helper phy_detach_internal() and make phy_detach() a short stub that calls the new helper. Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-5-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/phy/phy_device.c | 172 +++++++++++++++++++---------------- 1 file changed, 92 insertions(+), 80 deletions(-) diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c index 8cb0d60fcbba..69b04a92b776 100644 --- a/drivers/net/phy/phy_device.c +++ b/drivers/net/phy/phy_device.c @@ -1763,6 +1763,98 @@ static bool phy_drv_supports_irq(const struct phy_driver *phydrv) return phydrv->config_intr && phydrv->handle_interrupt; } +/** + * phy_detach_internal - detach a PHY device from its network device + * @phydev: target phy_device struct + * + * This detaches the phy device from its network device and the phy + * driver, and drops the reference count taken in phy_attach_direct(). + */ +static void phy_detach_internal(struct phy_device *phydev) +{ + struct net_device *dev = phydev->attached_dev; + struct module *ndev_owner = NULL; + struct mii_bus *bus; + + if (phydev->devlink) { + device_link_del(phydev->devlink); + phydev->devlink = NULL; + } + + if (phydev->sysfs_links) { + if (dev) + sysfs_remove_link(&dev->dev.kobj, "phydev"); + sysfs_remove_link(&phydev->mdio.dev.kobj, "attached_dev"); + } + + if (!phydev->attached_dev) + sysfs_remove_file(&phydev->mdio.dev.kobj, + &dev_attr_phy_standalone.attr); + + phy_suspend(phydev); + if (dev) { + struct hwtstamp_provider *hwprov; + + /* hwprov may technically be protected by ops lock but + * not for devices with a phydev, see phy_link_topo_add_phy() + */ + hwprov = rtnl_dereference(dev->hwprov); + /* Disable timestamp if it is the one selected */ + if (hwprov && hwprov->phydev == phydev) { + rcu_assign_pointer(dev->hwprov, NULL); + kfree_rcu(hwprov, rcu_head); + } + + phydev->attached_dev->phydev = NULL; + phydev->attached_dev = NULL; + phy_link_topo_del_phy(dev, phydev); + } + + phydev->phy_link_change = NULL; + phydev->phylink = NULL; + + if (phydev->mdio.dev.driver) + module_put(phydev->mdio.dev.driver->owner); + + /* If the device had no specific driver before (i.e. - it + * was using the generic driver), we unbind the device + * from the generic driver so that there's a chance a + * real driver could be loaded + */ + if (phydev->is_genphy_driven) { + device_release_driver(&phydev->mdio.dev); + phydev->is_genphy_driven = 0; + } + + /* Assert the reset signal */ + phy_device_reset(phydev, 1); + + /* + * The phydev might go away on the put_device() below, so avoid + * a use-after-free bug by reading the underlying bus first. + */ + bus = phydev->mdio.bus; + + put_device(&phydev->mdio.dev); + if (dev) + ndev_owner = dev->dev.parent->driver->owner; + if (ndev_owner != bus->owner) + module_put(bus->owner); +} + +/** + * phy_detach - detach a PHY device from its network device + * @phydev: target phy_device struct + * + * This detaches the phy device from its network device and the phy + * driver, and drops the reference count taken in phy_attach_direct(). + */ +void phy_detach(struct phy_device *phydev) +{ + phy_detach_internal(phydev); +} +EXPORT_SYMBOL(phy_detach); + /** * phy_attach_direct - attach a network device to a given PHY device pointer * @dev: network device to attach @@ -1935,86 +2027,6 @@ error_put_device: } EXPORT_SYMBOL(phy_attach_direct); -/** - * phy_detach - detach a PHY device from its network device - * @phydev: target phy_device struct - * - * This detaches the phy device from its network device and the phy - * driver, and drops the reference count taken in phy_attach_direct(). - */ -void phy_detach(struct phy_device *phydev) -{ - struct net_device *dev = phydev->attached_dev; - struct module *ndev_owner = NULL; - struct mii_bus *bus; - - if (phydev->devlink) { - device_link_del(phydev->devlink); - phydev->devlink = NULL; - } - - if (phydev->sysfs_links) { - if (dev) - sysfs_remove_link(&dev->dev.kobj, "phydev"); - sysfs_remove_link(&phydev->mdio.dev.kobj, "attached_dev"); - } - - if (!phydev->attached_dev) - sysfs_remove_file(&phydev->mdio.dev.kobj, - &dev_attr_phy_standalone.attr); - - phy_suspend(phydev); - if (dev) { - struct hwtstamp_provider *hwprov; - - /* hwprov may technically be protected by ops lock but - * not for devices with a phydev, see phy_link_topo_add_phy() - */ - hwprov = rtnl_dereference(dev->hwprov); - /* Disable timestamp if it is the one selected */ - if (hwprov && hwprov->phydev == phydev) { - rcu_assign_pointer(dev->hwprov, NULL); - kfree_rcu(hwprov, rcu_head); - } - - phydev->attached_dev->phydev = NULL; - phydev->attached_dev = NULL; - phy_link_topo_del_phy(dev, phydev); - } - - phydev->phy_link_change = NULL; - phydev->phylink = NULL; - - if (phydev->mdio.dev.driver) - module_put(phydev->mdio.dev.driver->owner); - - /* If the device had no specific driver before (i.e. - it - * was using the generic driver), we unbind the device - * from the generic driver so that there's a chance a - * real driver could be loaded - */ - if (phydev->is_genphy_driven) { - device_release_driver(&phydev->mdio.dev); - phydev->is_genphy_driven = 0; - } - - /* Assert the reset signal */ - phy_device_reset(phydev, 1); - - /* - * The phydev might go away on the put_device() below, so avoid - * a use-after-free bug by reading the underlying bus first. - */ - bus = phydev->mdio.bus; - - put_device(&phydev->mdio.dev); - if (dev) - ndev_owner = dev->dev.parent->driver->owner; - if (ndev_owner != bus->owner) - module_put(bus->owner); -} -EXPORT_SYMBOL(phy_detach); - int phy_suspend(struct phy_device *phydev) { struct net_device *netdev = phydev->attached_dev; From 4b1d5d51528a2178aa9abeea4881e15f2344285d Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Mon, 31 Aug 2026 16:34:31 +0200 Subject: [PATCH 05/13] net: phy: add (*notify_phy_attach/detach)() hooks to struct mii_bus Some MDIO buses require programming PHY polling registers depending on the PHY type. RealTek switch SoCs are the most prominent example of a DSA switch which doesn't allow to program MAC speed, duplex and flow-control settings without using PHY polling to do so [1]. Avoid a half-baked solution in the MDIO bus driver because - it must reinvent the bus scanning to determine the PHYs and - it must anticipate the right point in time (e.g. deferred PHYs). Hence there is a need to inform the MDIO bus driver that a PHY is being attached or detached. Provide two hooks in struct mii_bus - notify_phy_attach(): called in phy_attach_direct() after PHY hardware has been initialized and just before PHY is resumed. - notify_phy_detach(): called in phy_detach() right after PHY has been suspended. Worth to notice: As of now phy_detach() is not 100% LIFO symmetric to phy_attach_direct(). E.g. sysfs links are torn down before suspend while being created before resume. Without reordering of the detach function the above mentioned notifier placement is the best possible symmetric implementation. An unconditional call of notify_phy_detach() was favoured [3]. Remark! A slightly different version of this patch was part of a former series [2]. The discussion already showed that an initialization hook should be placed somewhere late during the whole setup. This commit implants it right after phy_init_hw() as suggested. On top of this it adds the detach hook. [1] https://github.com/openwrt/openwrt/pull/21515#discussion_r2714069716 [2] https://lore.kernel.org/netdev/cover.1769053496.git.daniel@makrotopia.org/ [3] https://lore.kernel.org/netdev/9e40f50b-357a-4a93-9f59-94847850835d@lunn.ch/#t Signed-off-by: Daniel Golle Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-6-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/phy/phy_device.c | 20 ++++++++++++++++---- include/linux/phy.h | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c index 69b04a92b776..41d1b20cde2c 100644 --- a/drivers/net/phy/phy_device.c +++ b/drivers/net/phy/phy_device.c @@ -1766,11 +1766,12 @@ static bool phy_drv_supports_irq(const struct phy_driver *phydrv) /** * phy_detach_internal - detach a PHY device from its network device * @phydev: target phy_device struct + * @notify_bus: whether to notify the MDIO bus about the PHY detach * * This detaches the phy device from its network device and the phy * driver, and drops the reference count taken in phy_attach_direct(). */ -static void phy_detach_internal(struct phy_device *phydev) +static void phy_detach_internal(struct phy_device *phydev, bool notify_bus) { struct net_device *dev = phydev->attached_dev; struct module *ndev_owner = NULL; @@ -1792,6 +1793,10 @@ static void phy_detach_internal(struct phy_device *phydev) &dev_attr_phy_standalone.attr); phy_suspend(phydev); + + if (notify_bus && phydev->mdio.bus->notify_phy_detach) + phydev->mdio.bus->notify_phy_detach(phydev); + if (dev) { struct hwtstamp_provider *hwprov; @@ -1851,7 +1856,8 @@ static void phy_detach_internal(struct phy_device *phydev) */ void phy_detach(struct phy_device *phydev) { - phy_detach_internal(phydev); + /* cleanup including bus notification */ + phy_detach_internal(phydev, true); } EXPORT_SYMBOL(phy_detach); @@ -1997,6 +2003,12 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev, if (err) goto error; + if (phydev->mdio.bus->notify_phy_attach) { + err = phydev->mdio.bus->notify_phy_attach(phydev); + if (err) + goto error; + } + phy_resume(phydev); /** @@ -2011,8 +2023,8 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev, return err; error: - /* phy_detach() does all of the cleanup below */ - phy_detach(phydev); + /* phy_detach_internal() does all of the cleanup below */ + phy_detach_internal(phydev, false); return err; error_module_put: diff --git a/include/linux/phy.h b/include/linux/phy.h index 5f8d65868e0f..3d8afe6b7f1c 100644 --- a/include/linux/phy.h +++ b/include/linux/phy.h @@ -376,6 +376,24 @@ struct mii_bus { int regnum, u16 val); /** @reset: Perform a reset of the bus */ int (*reset)(struct mii_bus *bus); + /** + * @notify_phy_attach: Perform post-attach handling for MDIO bus + * drivers. Optional and independent of @notify_phy_detach. Called + * in phy_attach_direct() right before phy_resume(). Runs in process + * context, may sleep and may be called with RTNL held. Must not + * acquire or rely on RTNL. Returns 0 on success or negative errno + * on failure. Must unwind its own state on error as attachment is + * aborted. + */ + int (*notify_phy_attach)(struct phy_device *phydev); + /** + * @notify_phy_detach: Perform pre-detach handling for MDIO bus + * drivers. Optional and independent of @notify_phy_attach. Called + * in phy_detach() right after phy_suspend(). Runs in process context, + * may sleep and may be called with RTNL held. Must not acquire or + * rely on RTNL. + */ + void (*notify_phy_detach)(struct phy_device *phydev); /** @stats: Statistic counters per device on the bus */ struct mdio_bus_stats stats[PHY_MAX_ADDR]; From fe7a07ff5194e3b0168ca65c5e0ebde689eb73a9 Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:32 +0200 Subject: [PATCH 06/13] net: mdio: realtek-rtl9300: suppress sysfs bind/unbind attributes The Realtek MDIO driver relies on devm managed resources for lifetime management and allocates its internal central controller structure otto_emdio_priv via devm_kzalloc(). This can lead to a use-after-free in the following sitauation: - If the driver is removed or unbound via sysfs the controller devm cleanup is triggered immediately. A corrupt devicetree might produce a load failure and a cleanup too. - However the MDIO buses are allocated with devm_mdiobus_alloc_size() and are reference counted. So if a MAC driver still holds a reference to a PHY device on the bus, the mii_bus will remain alive. - Any action that is run for the buses afterwards still relies on the availability of the central priv structure. This will access already freed memory. There are multiple mitigations to choose from - Clone all controller->priv data into the bus->priv structure. - disable sysfs bind/unbind attributes - Check for "bus->state != MDIOBUS_REGISTERED" in all callbacks This SoC-integrated MDIO controller cannot be physically hot-plugged and manual unbinding gives no benefit. The driver is usually integrated into the kernel and not loaded as a module. Avoid a complex driver rewrite (data duplication) for now and set suppress_bind_attrs to true to prevent unbinding via sysfs. This mitigates issues for the known use cases. Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-7-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/mdio/mdio-realtek-rtl9300.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/net/mdio/mdio-realtek-rtl9300.c b/drivers/net/mdio/mdio-realtek-rtl9300.c index 69565a87f99d..7de8adec243b 100644 --- a/drivers/net/mdio/mdio-realtek-rtl9300.c +++ b/drivers/net/mdio/mdio-realtek-rtl9300.c @@ -844,6 +844,7 @@ static struct platform_driver otto_emdio_driver = { .driver = { .name = "mdio-rtl9300", .of_match_table = otto_emdio_ids, + .suppress_bind_attrs = true, }, }; From ee18e22c63424ed9eddacaa3b4480413e29862bc Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:33 +0200 Subject: [PATCH 07/13] net: mdio: realtek-rtl9300: Configure hardware polling during probing During PHY probing and configuration complex configuration sequences might be issued and firmware might be loaded. Hardware polling can interfere badly with that. E.g. a hardware polling MMD c45 over c22 request might break an ongoing firmware loading sequence. To avoid such issues the polling of the Realtek Otto switches can be (de)activated with one or two 32 bit mask registers. Each bit enables (=1) or disables (=0) the polling of the corresponding port. Make use of this as follows: - Disable polling for all ports when the MDIO driver starts. - Reenable polling just after the PHY has been attached. - Disable polling just before the PHY is being detached. This synchronizes the kernel and hardware polling to some extent. It gracefully handles deferred probing of PHYs in case the driver is loaded asynchronously during boot. Additionally it brings the hardware polling into a consistent operation mode for devices where U-Boot does not take care. Important notes about the implementation: Realtek is very inconsistent about its register naming. RTL930x uses SMI_POLL_CTRL for polling control while it is SMI_PORT_POLLING_CTRL on RTL931x. Keep these vendor names. These devices do not support power management for the whole system. So mdio_bus_phy_resume() is not used and it is not required to disable/enable hardware polling for that usecase. These devices have no CPU MAC to PHY design. Thus abort phy_attach() in case PHYs are not mapped to valid switch ports. All MDIO bus commands will fail afterwards, leaving it in an unusable state. Enabling/disabling hardware polling is a simple regmap bit access. For this it is enough to rely on the regmap internal lock. This polling enabling/disabling does not solve the current inability of the driver to handle C45-over-C22 access during normal operation. For all existing designs this is not needed. [1] https://github.com/openwrt/openwrt/blob/main/target/linux/realtek/files-6.18/drivers/net/mdio/mdio-realtek-otto.c#L818 [2] https://lore.kernel.org/netdev/680696024a8648535ce6dee771fe4de67802e0e8.1769053496.git.daniel@makrotopia.org/ Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-8-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/mdio/mdio-realtek-rtl9300.c | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/drivers/net/mdio/mdio-realtek-rtl9300.c b/drivers/net/mdio/mdio-realtek-rtl9300.c index 7de8adec243b..a925640290cb 100644 --- a/drivers/net/mdio/mdio-realtek-rtl9300.c +++ b/drivers/net/mdio/mdio-realtek-rtl9300.c @@ -146,6 +146,7 @@ #define RTL9300_PHY_CTRL_INDATA GENMASK(31, 16) #define RTL9300_PHY_CTRL_DATA GENMASK(15, 0) #define RTL9300_SMI_ACCESS_PHY_CTRL_3 0xcb7c +#define RTL9300_SMI_POLL_CTRL 0xca90 #define RTL9300_SMI_PORT0_5_ADDR_CTRL 0xcb80 #define RTL9310_NUM_BUSES 4 @@ -171,6 +172,7 @@ #define RTL9310_PHY_CTRL_INDATA GENMASK(15, 0) #define RTL9310_SMI_INDRT_ACCESS_MMD_CTRL 0x0c18 #define RTL9310_SMI_PORT_ADDR_CTRL 0x0c74 +#define RTL9310_SMI_PORT_POLLING_CTRL 0x0ccc #define RTL9310_SMI_PORT_POLLING_SEL 0x0c9c #define PHY_CTRL_CMD BIT(0) @@ -218,6 +220,7 @@ struct otto_emdio_info { u8 num_buses; u8 num_ports; u16 num_pages; + u32 poll_ctrl; int (*setup_controller)(struct otto_emdio_priv *priv); int (*read_c22)(struct mii_bus *bus, int port, int regnum, u32 *value); int (*read_c45)(struct mii_bus *bus, int port, int dev_addr, int regnum, u32 *value); @@ -253,6 +256,12 @@ static struct otto_emdio_priv *otto_emdio_bus_to_priv(struct mii_bus *bus) return chan->priv; } +static int otto_emdio_set_port_polling(struct otto_emdio_priv *priv, int port, bool active) +{ + return regmap_assign_bits(priv->regmap, priv->info->poll_ctrl + (port / 32) * 4, + BIT(port % 32), active); +} + static int otto_emdio_run_cmd(struct mii_bus *bus, u32 cmd, struct otto_emdio_cmd_regs *cmd_data) { @@ -596,6 +605,36 @@ static int otto_emdio_9310_setup_controller(struct otto_emdio_priv *priv) return 0; } +static int otto_emdio_notify_phy_attach(struct phy_device *phydev) +{ + struct otto_emdio_priv *priv = otto_emdio_bus_to_priv(phydev->mdio.bus); + int port = otto_emdio_phy_to_port(phydev->mdio.bus, phydev->mdio.addr); + + if (port < 0) { + /* All subsequent bus operations will fail */ + phydev_err(phydev, "PHY is not mapped to a valid switch port\n"); + return port; + } + + return otto_emdio_set_port_polling(priv, port, true); +} + +static void otto_emdio_notify_phy_detach(struct phy_device *phydev) +{ + struct mii_bus *bus = phydev->mdio.bus; + struct otto_emdio_priv *priv; + int port; + + priv = otto_emdio_bus_to_priv(phydev->mdio.bus); + port = otto_emdio_phy_to_port(phydev->mdio.bus, phydev->mdio.addr); + + if (port < 0) + return; + + if (otto_emdio_set_port_polling(priv, port, false)) + dev_err(bus->parent, "failed to disable polling for port %d\n", port); +} + static int otto_emdio_probe_one(struct device *dev, struct otto_emdio_priv *priv, struct fwnode_handle *node) { @@ -625,6 +664,9 @@ static int otto_emdio_probe_one(struct device *dev, struct otto_emdio_priv *priv bus->write = otto_emdio_write_c22; } bus->parent = dev; + bus->notify_phy_attach = otto_emdio_notify_phy_attach; + bus->notify_phy_detach = otto_emdio_notify_phy_detach; + chan = bus->priv; chan->mdio_bus = mdio_bus; chan->priv = priv; @@ -741,6 +783,19 @@ put_nodes: return err; } +static int otto_emdio_init_polling(struct otto_emdio_priv *priv) +{ + int err; + + for (int port = 0; port < priv->info->num_ports; port++) { + err = otto_emdio_set_port_polling(priv, port, false); + if (err) + return err; + } + + return 0; +} + static int otto_emdio_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; @@ -760,6 +815,10 @@ static int otto_emdio_probe(struct platform_device *pdev) if (IS_ERR(priv->regmap)) return PTR_ERR(priv->regmap); + err = otto_emdio_init_polling(priv); + if (err) + return err; + platform_set_drvdata(pdev, priv); err = otto_emdio_map_ports(dev); @@ -800,6 +859,7 @@ static const struct otto_emdio_info otto_emdio_9300_info = { .num_buses = RTL9300_NUM_BUSES, .num_ports = RTL9300_NUM_PORTS, .num_pages = RTL9300_NUM_PAGES, + .poll_ctrl = RTL9300_SMI_POLL_CTRL, .setup_controller = otto_emdio_9300_setup_controller, .read_c22 = otto_emdio_9300_read_c22, .read_c45 = otto_emdio_9300_read_c45, @@ -825,6 +885,7 @@ static const struct otto_emdio_info otto_emdio_9310_info = { .num_buses = RTL9310_NUM_BUSES, .num_pages = RTL9310_NUM_PAGES, .num_ports = RTL9310_NUM_PORTS, + .poll_ctrl = RTL9310_SMI_PORT_POLLING_CTRL, .setup_controller = otto_emdio_9310_setup_controller, .read_c22 = otto_emdio_9310_read_c22, .read_c45 = otto_emdio_9310_read_c45, From cc3cb8db1eef9fbe6f2c520312cf6384cf2c2ce7 Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:34 +0200 Subject: [PATCH 08/13] net: mdio: realtek-rtl9300: Add page tracking The hardware polling unit of the Realtek switches has a very special handling for c22 PHY register 31 (aka Realtek page register) in place. - On the RTL838x it is permanently reset to zero. - On other devices there is some magic saving/restoring (aka parking) in the background in place. This makes access to PHYs a gamble. It is vital to keep the polling alive so the MAC layer can rely on consistent data. Intercept access to c22 register 31 and handle it internally. Store the desired value for each port in the driver. When issuing hardware access to other registers add the page to the command towards the controller. Given this, the hardware will run two c22 commands that are not interrupted by polling. ... hardware poll ... phy_write(phy, 31, page) phy_write(phy, reg, value) ... hardware poll ... Looking at this implementation one might argue that disabling/enabling polling might be a cleaner solution. But one must remember that - This driver differentiates clearly between C22 and C45 buses. During probing it enables only one of the protocols for a bus. - All known devices run RTL8218 (B/D/E) or RTL8214FC on 1G - RTL839x gives link flapping when deactivating polling for a port So a solution for a Realtek-only ecosystem is required. This commit copies the downstream-proven driver-only page handling patch without any new MDIO callbacks and is the lowest common denominator. If a non-Realtek PHY is identified on a c22 bus the attachment aborts. It should be noted that bus scan runs with the page handling already in place before the check in notify_phy_attach(). This is accepted for now. Remark: To keep this simple, writes to register 31 are only accepted if they are lower than the device specific raw page - 0..4094/8190. Otherwise -EINVAL is returned. Under the above assumption (Only 1G Realtek PHYs on a c22 bus) this is no limitation. Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-9-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/mdio/mdio-realtek-rtl9300.c | 46 +++++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/drivers/net/mdio/mdio-realtek-rtl9300.c b/drivers/net/mdio/mdio-realtek-rtl9300.c index a925640290cb..d6027062e452 100644 --- a/drivers/net/mdio/mdio-realtek-rtl9300.c +++ b/drivers/net/mdio/mdio-realtek-rtl9300.c @@ -179,6 +179,9 @@ #define PHY_CTRL_MMD_DEVAD GENMASK(20, 16) #define PHY_CTRL_MMD_REG GENMASK(15, 0) +#define RTL_VENDOR_ID 0x001cc800 +#define RTL_PAGE_SELECT 31 + #define MAP_ADDRS_PER_REG 6 #define MAP_BITS_PER_ADDR 5 #define MAP_BITS_PER_BUS 2 @@ -204,6 +207,7 @@ struct otto_emdio_priv { struct regmap *regmap; struct mutex lock; /* protect HW access */ DECLARE_BITMAP(valid_ports, MAX_PORTS); + u16 page[MAX_PORTS]; u8 smi_bus[MAX_PORTS]; u8 smi_addr[MAX_PORTS]; bool smi_bus_is_c45[MAX_SMI_BUSSES]; @@ -355,7 +359,7 @@ static int otto_emdio_9300_read_c22(struct mii_bus *bus, int port, int regnum, u struct otto_emdio_cmd_regs cmd_data = { .c22_data = FIELD_PREP(RTL9300_PHY_CTRL_REG_ADDR, regnum) | FIELD_PREP(RTL9300_PHY_CTRL_PARK_PAGE, 0x1f) | - FIELD_PREP(RTL9300_PHY_CTRL_MAIN_PAGE, RAW_PAGE(priv)), + FIELD_PREP(RTL9300_PHY_CTRL_MAIN_PAGE, priv->page[port]), .io_data = FIELD_PREP(RTL9300_PHY_CTRL_INDATA, port), }; @@ -369,7 +373,7 @@ static int otto_emdio_9300_write_c22(struct mii_bus *bus, int port, int regnum, struct otto_emdio_cmd_regs cmd_data = { .c22_data = FIELD_PREP(RTL9300_PHY_CTRL_REG_ADDR, regnum) | FIELD_PREP(RTL9300_PHY_CTRL_PARK_PAGE, 0x1f) | - FIELD_PREP(RTL9300_PHY_CTRL_MAIN_PAGE, RAW_PAGE(priv)), + FIELD_PREP(RTL9300_PHY_CTRL_MAIN_PAGE, priv->page[port]), .io_data = FIELD_PREP(RTL9300_PHY_CTRL_INDATA, value), .port_mask_low = BIT(port), }; @@ -409,7 +413,7 @@ static int otto_emdio_9310_read_c22(struct mii_bus *bus, int port, int regnum, u struct otto_emdio_cmd_regs cmd_data = { .broadcast = FIELD_PREP(RTL9310_BC_PORT_ID, port), .c22_data = FIELD_PREP(RTL9310_PHY_CTRL_REG_ADDR, regnum) | - FIELD_PREP(RTL9310_PHY_CTRL_MAIN_PAGE, RAW_PAGE(priv)), + FIELD_PREP(RTL9310_PHY_CTRL_MAIN_PAGE, priv->page[port]), }; return otto_emdio_read_cmd(bus, RTL9310_PHY_CTRL_TYPE_C22, &cmd_data, @@ -421,7 +425,7 @@ static int otto_emdio_9310_write_c22(struct mii_bus *bus, int port, int regnum, struct otto_emdio_priv *priv = otto_emdio_bus_to_priv(bus); struct otto_emdio_cmd_regs cmd_data = { .c22_data = FIELD_PREP(RTL9310_PHY_CTRL_REG_ADDR, regnum) | - FIELD_PREP(RTL9310_PHY_CTRL_MAIN_PAGE, RAW_PAGE(priv)), + FIELD_PREP(RTL9310_PHY_CTRL_MAIN_PAGE, priv->page[port]), .io_data = FIELD_PREP(RTL9310_PHY_CTRL_INDATA, value), .port_mask_high = (u32)(BIT_ULL(port) >> 32), .port_mask_low = (u32)(BIT_ULL(port)), @@ -473,8 +477,12 @@ static int otto_emdio_read_c22(struct mii_bus *bus, int phy_id, int regnum) if (port < 0) return port; - scoped_guard(mutex, &priv->lock) + scoped_guard(mutex, &priv->lock) { + if (regnum == RTL_PAGE_SELECT) + return priv->page[port]; + ret = priv->info->read_c22(bus, port, regnum, &value); + } return ret ? ret : value; } @@ -495,8 +503,17 @@ static int otto_emdio_write_c22(struct mii_bus *bus, int phy_id, int regnum, if (port < 0) return port; - scoped_guard(mutex, &priv->lock) + scoped_guard(mutex, &priv->lock) { + if (regnum == RTL_PAGE_SELECT) { + if (value >= RAW_PAGE(priv)) + return -EINVAL; + + priv->page[port] = value; + return 0; + } + ret = priv->info->write_c22(bus, port, regnum, value); + } return ret; } @@ -607,8 +624,9 @@ static int otto_emdio_9310_setup_controller(struct otto_emdio_priv *priv) static int otto_emdio_notify_phy_attach(struct phy_device *phydev) { - struct otto_emdio_priv *priv = otto_emdio_bus_to_priv(phydev->mdio.bus); int port = otto_emdio_phy_to_port(phydev->mdio.bus, phydev->mdio.addr); + struct otto_emdio_chan *chan = phydev->mdio.bus->priv; + struct otto_emdio_priv *priv = chan->priv; if (port < 0) { /* All subsequent bus operations will fail */ @@ -616,6 +634,16 @@ static int otto_emdio_notify_phy_attach(struct phy_device *phydev) return port; } + /* "sync" page in case of previously failed attachment */ + scoped_guard(mutex, &priv->lock) + priv->page[port] = 0; + + if (!priv->smi_bus_is_c45[chan->mdio_bus] && + !phy_id_compare_vendor(phydev->phy_id, RTL_VENDOR_ID)) { + phydev_err(phydev, "Only Realtek PHYs allowed on C22 bus\n"); + return -EOPNOTSUPP; + } + return otto_emdio_set_port_polling(priv, port, true); } @@ -631,6 +659,10 @@ static void otto_emdio_notify_phy_detach(struct phy_device *phydev) if (port < 0) return; + /* "sync" page for next attachment */ + scoped_guard(mutex, &priv->lock) + priv->page[port] = 0; + if (otto_emdio_set_port_polling(priv, port, false)) dev_err(bus->parent, "failed to disable polling for port %d\n", port); } From 6be4be074be82ea1196bec504bf1490b6fe6037f Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:35 +0200 Subject: [PATCH 09/13] net: mdio: realtek-rtl9300: Increase MDIO timeout RTL838x devices with 28 ports produce PHY access timeout errors during one of three boots while waiting for MDIO command completion. This is currently set to 1ms. Background: Access to the Realtek Otto ethernet MDIO bus must wait for a free slot between two hardware polls. The polling sequence consists of at least 17 commands on the RTL838x devices. This delay can be nicely seen when disabling polling completely. The following times are measured on a bus running on the default 2.5MHz. Time measured is from the last register write that sets the command-start-bit until the hardware responds with the command-finished-bit set. - average c22 read with polling enabled on all ports: ~380us - average c22 read with polling enabled on one port: ~380us - average c22 read with polling completely disabled: ~180us For this bus frequency the bare hardware runtime for a single command (32 bit preamble + 32 bit data) is ~25us. So the hardware adds quite some overhead. On top of this comes the fact that the RTL838x devices are low on resources (500Mhz 4Kec core with 16K cache). Increase the timeout to 10ms to be on the safe side. Remark! In a future patch the bus clock frequency will be made configurable with a minimum frequency of 1.25MHz. Setting this (e.g. for debugging purposes) doubles the command run times but will safely stay below 10ms. Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-10-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/mdio/mdio-realtek-rtl9300.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/net/mdio/mdio-realtek-rtl9300.c b/drivers/net/mdio/mdio-realtek-rtl9300.c index d6027062e452..ae7ba86e90b9 100644 --- a/drivers/net/mdio/mdio-realtek-rtl9300.c +++ b/drivers/net/mdio/mdio-realtek-rtl9300.c @@ -274,9 +274,9 @@ static int otto_emdio_run_cmd(struct mii_bus *bus, u32 cmd, u32 cmdstate; int ret; - /* Defensive pre check just in case something goes horrible wrong */ + /* Defensive pre check just in case something goes horribly wrong */ ret = regmap_read_poll_timeout(priv->regmap, info->cmd_regs.c22_data, - cmdstate, !(cmdstate & PHY_CTRL_CMD), 10, 1000); + cmdstate, !(cmdstate & PHY_CTRL_CMD), 10, 10000); if (ret) return ret; @@ -316,7 +316,7 @@ static int otto_emdio_run_cmd(struct mii_bus *bus, u32 cmd, return ret; ret = regmap_read_poll_timeout(priv->regmap, info->cmd_regs.c22_data, - cmdstate, !(cmdstate & PHY_CTRL_CMD), 10, 1000); + cmdstate, !(cmdstate & PHY_CTRL_CMD), 10, 10000); if (ret) return ret; From cc7a5bfaea142ebaa84bbcd8ddd14a3b21cc5bd5 Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:36 +0200 Subject: [PATCH 10/13] net: mdio: realtek-rtl9300: Open up C22 and C45 space in parallel Until now the MDIO driver supports either access to the C22 or to the C45 address space of the PHYs. This is due to the fact that the polling configuration favours one of the address spaces and limits access to the other space. E.g. when polling is set to C22 most of the C45 space can not be accessed. There are however some exceptions from that. EEE register access is allowed independently from the polling mode [1]. As the downstream driver already allows parallel C22/C45 access [2] do this in upstream too. [1] https://github.com/openwrt/openwrt/blob/main/target/linux/realtek/files-6.18/drivers/net/phy/realtek/realtek_multiport.c#L349 [2] https://github.com/openwrt/openwrt/blob/main/target/linux/realtek/files-6.18/drivers/net/mdio/mdio-realtek-otto.c#L1072 Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-11-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/mdio/mdio-realtek-rtl9300.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/drivers/net/mdio/mdio-realtek-rtl9300.c b/drivers/net/mdio/mdio-realtek-rtl9300.c index ae7ba86e90b9..b90972c1dc6a 100644 --- a/drivers/net/mdio/mdio-realtek-rtl9300.c +++ b/drivers/net/mdio/mdio-realtek-rtl9300.c @@ -688,13 +688,10 @@ static int otto_emdio_probe_one(struct device *dev, struct otto_emdio_priv *priv return -ENOMEM; bus->name = "Realtek Switch MDIO Bus"; - if (priv->smi_bus_is_c45[mdio_bus]) { - bus->read_c45 = otto_emdio_read_c45; - bus->write_c45 = otto_emdio_write_c45; - } else { - bus->read = otto_emdio_read_c22; - bus->write = otto_emdio_write_c22; - } + bus->read_c45 = otto_emdio_read_c45; + bus->write_c45 = otto_emdio_write_c45; + bus->read = otto_emdio_read_c22; + bus->write = otto_emdio_write_c22; bus->parent = dev; bus->notify_phy_attach = otto_emdio_notify_phy_attach; bus->notify_phy_detach = otto_emdio_notify_phy_detach; From 6ab6cb66b3b94239ae1007de4185c17756040279 Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:37 +0200 Subject: [PATCH 11/13] net: mdio: realtek-rtl9300: Add support for RTL838x The MDIO driver has been prepared for multiple device support. Add all required bits for the RTL838x (aka maple) series. This is straightforward but some things are worth mentioning. - The device has a lot in common with the RTL930x series. It has 28 ports, 4096 (Realtek) pages and 4 MMIO registers. With this a lot of the existing RTL9300 defines could be reused. But to avoid confusion and for better readability duplicate the defines with a proper prefix. - The MDIO engine has no fail bit. Thus the mask is set to zero. - There is only one SMI bus for 1G PHYs. No bus_map_base register exists. - The setup_controller() function needs no C45 configuration as polling only works in C22 mode - The PHY access must be activated by a special register flag. Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-12-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/mdio/mdio-realtek-rtl9300.c | 108 ++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/drivers/net/mdio/mdio-realtek-rtl9300.c b/drivers/net/mdio/mdio-realtek-rtl9300.c index b90972c1dc6a..57ab89afb9e5 100644 --- a/drivers/net/mdio/mdio-realtek-rtl9300.c +++ b/drivers/net/mdio/mdio-realtek-rtl9300.c @@ -126,6 +126,28 @@ #include #include +#define RTL8380_NUM_BUSES 1 +#define RTL8380_NUM_PAGES 4096 +#define RTL8380_NUM_PORTS 28 +#define RTL8380_SMI_GLB_CTRL 0xa100 +#define RTL8380_SMI_PHY_PATCH_DONE BIT(15) +#define RTL8380_SMI_ACCESS_PHY_CTRL_0 0xa1b8 +#define RTL8380_SMI_ACCESS_PHY_CTRL_1 0xa1bc +#define RTL8380_PHY_CTRL_REG_ADDR GENMASK(24, 20) +#define RTL8380_PHY_CTRL_PARK_PAGE GENMASK(19, 15) +#define RTL8380_PHY_CTRL_MAIN_PAGE GENMASK(14, 3) +#define RTL8380_PHY_CTRL_WRITE BIT(2) +#define RTL8380_PHY_CTRL_READ 0 +#define RTL8380_PHY_CTRL_TYPE_C45 BIT(1) +#define RTL8380_PHY_CTRL_TYPE_C22 0 +#define RTL8380_PHY_CTRL_FAIL 0 /* no fail indicator */ +#define RTL8380_SMI_ACCESS_PHY_CTRL_2 0xa1c0 +#define RTL8380_PHY_CTRL_INDATA GENMASK(31, 16) +#define RTL8380_PHY_CTRL_DATA GENMASK(15, 0) +#define RTL8380_SMI_ACCESS_PHY_CTRL_3 0xa1c4 +#define RTL8380_SMI_POLL_CTRL 0xa17c +#define RTL8380_SMI_PORT0_5_ADDR_CTRL 0xa1c8 + #define RTL9300_NUM_BUSES 4 #define RTL9300_NUM_PAGES 4096 #define RTL9300_NUM_PORTS 28 @@ -353,6 +375,60 @@ static int otto_emdio_write_cmd(struct mii_bus *bus, u32 cmd, return otto_emdio_run_cmd(bus, cmd | priv->info->cmd_write, cmd_data); } +static int otto_emdio_8380_read_c22(struct mii_bus *bus, int port, int regnum, u32 *value) +{ + struct otto_emdio_priv *priv = otto_emdio_bus_to_priv(bus); + struct otto_emdio_cmd_regs cmd_data = { + .c22_data = FIELD_PREP(RTL8380_PHY_CTRL_REG_ADDR, regnum) | + FIELD_PREP(RTL8380_PHY_CTRL_PARK_PAGE, 0x1f) | + FIELD_PREP(RTL8380_PHY_CTRL_MAIN_PAGE, priv->page[port]), + .io_data = FIELD_PREP(RTL8380_PHY_CTRL_INDATA, port), + }; + + return otto_emdio_read_cmd(bus, RTL8380_PHY_CTRL_TYPE_C22, &cmd_data, + RTL8380_PHY_CTRL_DATA, value); +} + +static int otto_emdio_8380_write_c22(struct mii_bus *bus, int port, int regnum, u16 value) +{ + struct otto_emdio_priv *priv = otto_emdio_bus_to_priv(bus); + struct otto_emdio_cmd_regs cmd_data = { + .c22_data = FIELD_PREP(RTL8380_PHY_CTRL_REG_ADDR, regnum) | + FIELD_PREP(RTL8380_PHY_CTRL_PARK_PAGE, 0x1f) | + FIELD_PREP(RTL8380_PHY_CTRL_MAIN_PAGE, priv->page[port]), + .io_data = FIELD_PREP(RTL8380_PHY_CTRL_INDATA, value), + .port_mask_low = BIT(port), + }; + + return otto_emdio_write_cmd(bus, RTL8380_PHY_CTRL_TYPE_C22, &cmd_data); +} + +static int otto_emdio_8380_read_c45(struct mii_bus *bus, int port, + int dev_addr, int regnum, u32 *value) +{ + struct otto_emdio_cmd_regs cmd_data = { + .c45_data = FIELD_PREP(PHY_CTRL_MMD_DEVAD, dev_addr) | + FIELD_PREP(PHY_CTRL_MMD_REG, regnum), + .io_data = FIELD_PREP(RTL8380_PHY_CTRL_INDATA, port), + }; + + return otto_emdio_read_cmd(bus, RTL8380_PHY_CTRL_TYPE_C45, &cmd_data, + RTL8380_PHY_CTRL_DATA, value); +} + +static int otto_emdio_8380_write_c45(struct mii_bus *bus, int port, + int dev_addr, int regnum, u16 value) +{ + struct otto_emdio_cmd_regs cmd_data = { + .c45_data = FIELD_PREP(PHY_CTRL_MMD_DEVAD, dev_addr) | + FIELD_PREP(PHY_CTRL_MMD_REG, regnum), + .io_data = FIELD_PREP(RTL8380_PHY_CTRL_INDATA, value), + .port_mask_low = BIT(port), + }; + + return otto_emdio_write_cmd(bus, RTL8380_PHY_CTRL_TYPE_C45, &cmd_data); +} + static int otto_emdio_9300_read_c22(struct mii_bus *bus, int port, int regnum, u32 *value) { struct otto_emdio_priv *priv = otto_emdio_bus_to_priv(bus); @@ -586,6 +662,15 @@ static int otto_emdio_setup_topology(struct otto_emdio_priv *priv) return 0; } +static int otto_emdio_8380_setup_controller(struct otto_emdio_priv *priv) +{ + /* + * PHY_PATCH_DONE enables PHY control via SoC. This is required for PHY access, including + * patching and must be set before the PHYs are probed. + */ + return regmap_set_bits(priv->regmap, RTL8380_SMI_GLB_CTRL, RTL8380_SMI_PHY_PATCH_DONE); +} + static int otto_emdio_9300_setup_controller(struct otto_emdio_priv *priv) { u32 glb_ctrl_mask = 0, glb_ctrl_val = 0; @@ -873,6 +958,28 @@ static int otto_emdio_probe(struct platform_device *pdev) return 0; } +static const struct otto_emdio_info otto_emdio_8380_info = { + .addr_map_base = RTL8380_SMI_PORT0_5_ADDR_CTRL, + .cmd_fail = RTL8380_PHY_CTRL_FAIL, + .cmd_read = RTL8380_PHY_CTRL_READ, + .cmd_write = RTL8380_PHY_CTRL_WRITE, + .cmd_regs = { + .c22_data = RTL8380_SMI_ACCESS_PHY_CTRL_1, + .c45_data = RTL8380_SMI_ACCESS_PHY_CTRL_3, + .io_data = RTL8380_SMI_ACCESS_PHY_CTRL_2, + .port_mask_low = RTL8380_SMI_ACCESS_PHY_CTRL_0, + }, + .num_buses = RTL8380_NUM_BUSES, + .num_pages = RTL8380_NUM_PAGES, + .num_ports = RTL8380_NUM_PORTS, + .poll_ctrl = RTL8380_SMI_POLL_CTRL, + .setup_controller = otto_emdio_8380_setup_controller, + .read_c22 = otto_emdio_8380_read_c22, + .read_c45 = otto_emdio_8380_read_c45, + .write_c22 = otto_emdio_8380_write_c22, + .write_c45 = otto_emdio_8380_write_c45, +}; + static const struct otto_emdio_info otto_emdio_9300_info = { .addr_map_base = RTL9300_SMI_PORT0_5_ADDR_CTRL, .bus_map_base = RTL9300_SMI_PORT0_15_POLLING_SEL, @@ -923,6 +1030,7 @@ static const struct otto_emdio_info otto_emdio_9310_info = { }; static const struct of_device_id otto_emdio_ids[] = { + { .compatible = "realtek,rtl8380-mdio", .data = &otto_emdio_8380_info }, { .compatible = "realtek,rtl9301-mdio", .data = &otto_emdio_9300_info }, { .compatible = "realtek,rtl9311-mdio", .data = &otto_emdio_9310_info }, {} From 11b8be84ccafc946c5b3124ad41988e9243309a0 Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:38 +0200 Subject: [PATCH 12/13] net: mdio: realtek-rtl9300: Add support for RTL839x The MDIO driver has been prepared for multiple device support. Add all required bits for the RTL839x (aka cypress) series. This is straightforward but some things are worth mentioning. - The device has a lot in common with the RTL931x series. It has 8192 (Realtek) pages and 7 MMIO registers. - There are two SMI buses for 1G PHYs. Neither the bus nor address map registers exist. - The hardware has not much to configure. So the setup_controller() function is not needed. - The bit specific registers (polling & port_mask) are 2x32 bit wide. The ports 0..31 got to and the ports 32..51 go to . - C22 read/write functions must be called with PARK_PAGE = 0. Keep code clean and avoid setting it to zero, matching the behavior of the RTL9310 logic. - As per SDK the broadcast register allows to write to multiple ports at the same time. Unlike RTL9310 where this is filled with a bit mask for the current port RTL8390 does not use it for normal reads/writes. It is simply set to 0 like the SDK does. - The SDK fills the EXT_PAGE register with 0x1ff for C22 access and with 0x0 for C45 access. The reason for this is currently unknown and a meaningful name can not be given. Align the driver coding with the RTL9300_PHY_CTRL_PARK_PAGE settings and simply fill the hardcoded value. Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-13-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/mdio/mdio-realtek-rtl9300.c | 102 ++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/drivers/net/mdio/mdio-realtek-rtl9300.c b/drivers/net/mdio/mdio-realtek-rtl9300.c index 57ab89afb9e5..86b2040fa333 100644 --- a/drivers/net/mdio/mdio-realtek-rtl9300.c +++ b/drivers/net/mdio/mdio-realtek-rtl9300.c @@ -148,6 +148,28 @@ #define RTL8380_SMI_POLL_CTRL 0xa17c #define RTL8380_SMI_PORT0_5_ADDR_CTRL 0xa1c8 +#define RTL8390_NUM_BUSES 2 +#define RTL8390_NUM_PAGES 8192 +#define RTL8390_NUM_PORTS 52 +#define RTL8390_BCAST_PHYID_CTRL 0x03ec +#define RTL8390_PHYREG_ACCESS_CTRL 0x03dc +#define RTL8390_PHY_CTRL_REG_ADDR GENMASK(9, 5) +#define RTL8390_PHY_CTRL_MAIN_PAGE GENMASK(22, 10) +#define RTL8390_PHY_CTRL_FAIL BIT(1) +#define RTL8390_PHY_CTRL_WRITE BIT(3) +#define RTL8390_PHY_CTRL_READ 0 +#define RTL8390_PHY_CTRL_TYPE_C45 BIT(2) +#define RTL8390_PHY_CTRL_TYPE_C22 0 +#define RTL8390_PHYREG_CTRL 0x03e0 +#define RTL8390_PHY_CTRL_EXT_PAGE GENMASK(8, 0) +#define RTL8390_PHYREG_DATA_CTRL 0x03f0 +#define RTL8390_PHY_CTRL_INDATA GENMASK(31, 16) +#define RTL8390_PHY_CTRL_DATA GENMASK(15, 0) +#define RTL8390_PHYREG_MMD_CTRL 0x03f4 +#define RTL8390_PHYREG_PORT_CTRL_LOW 0x03e4 +#define RTL8390_PHYREG_PORT_CTRL_HIGH 0x03e8 +#define RTL8390_SMI_PORT_POLLING_CTRL 0x03fc + #define RTL9300_NUM_BUSES 4 #define RTL9300_NUM_PAGES 4096 #define RTL9300_NUM_PORTS 28 @@ -429,6 +451,62 @@ static int otto_emdio_8380_write_c45(struct mii_bus *bus, int port, return otto_emdio_write_cmd(bus, RTL8380_PHY_CTRL_TYPE_C45, &cmd_data); } +static int otto_emdio_8390_read_c22(struct mii_bus *bus, int port, int regnum, u32 *value) +{ + struct otto_emdio_priv *priv = otto_emdio_bus_to_priv(bus); + struct otto_emdio_cmd_regs cmd_data = { + .c22_data = FIELD_PREP(RTL8390_PHY_CTRL_REG_ADDR, regnum) | + FIELD_PREP(RTL8390_PHY_CTRL_MAIN_PAGE, priv->page[port]), + .ext_page = FIELD_PREP(RTL8390_PHY_CTRL_EXT_PAGE, 0x1ff), + .io_data = FIELD_PREP(RTL8390_PHY_CTRL_INDATA, port), + }; + + return otto_emdio_read_cmd(bus, RTL8390_PHY_CTRL_TYPE_C22, &cmd_data, + RTL8390_PHY_CTRL_DATA, value); +} + +static int otto_emdio_8390_write_c22(struct mii_bus *bus, int port, int regnum, u16 value) +{ + struct otto_emdio_priv *priv = otto_emdio_bus_to_priv(bus); + struct otto_emdio_cmd_regs cmd_data = { + .c22_data = FIELD_PREP(RTL8390_PHY_CTRL_REG_ADDR, regnum) | + FIELD_PREP(RTL8390_PHY_CTRL_MAIN_PAGE, priv->page[port]), + .ext_page = FIELD_PREP(RTL8390_PHY_CTRL_EXT_PAGE, 0x1ff), + .io_data = FIELD_PREP(RTL8390_PHY_CTRL_INDATA, value), + .port_mask_high = (u32)(BIT_ULL(port) >> 32), + .port_mask_low = (u32)(BIT_ULL(port)), + }; + + return otto_emdio_write_cmd(bus, RTL8390_PHY_CTRL_TYPE_C22, &cmd_data); +} + +static int otto_emdio_8390_read_c45(struct mii_bus *bus, int port, + int dev_addr, int regnum, u32 *value) +{ + struct otto_emdio_cmd_regs cmd_data = { + .c45_data = FIELD_PREP(PHY_CTRL_MMD_DEVAD, dev_addr) | + FIELD_PREP(PHY_CTRL_MMD_REG, regnum), + .io_data = FIELD_PREP(RTL8390_PHY_CTRL_INDATA, port), + }; + + return otto_emdio_read_cmd(bus, RTL8390_PHY_CTRL_TYPE_C45, &cmd_data, + RTL8390_PHY_CTRL_DATA, value); +} + +static int otto_emdio_8390_write_c45(struct mii_bus *bus, int port, + int dev_addr, int regnum, u16 value) +{ + struct otto_emdio_cmd_regs cmd_data = { + .c45_data = FIELD_PREP(PHY_CTRL_MMD_DEVAD, dev_addr) | + FIELD_PREP(PHY_CTRL_MMD_REG, regnum), + .io_data = FIELD_PREP(RTL8390_PHY_CTRL_INDATA, value), + .port_mask_high = (u32)(BIT_ULL(port) >> 32), + .port_mask_low = (u32)(BIT_ULL(port)), + }; + + return otto_emdio_write_cmd(bus, RTL8390_PHY_CTRL_TYPE_C45, &cmd_data); +} + static int otto_emdio_9300_read_c22(struct mii_bus *bus, int port, int regnum, u32 *value) { struct otto_emdio_priv *priv = otto_emdio_bus_to_priv(bus); @@ -980,6 +1058,29 @@ static const struct otto_emdio_info otto_emdio_8380_info = { .write_c45 = otto_emdio_8380_write_c45, }; +static const struct otto_emdio_info otto_emdio_8390_info = { + .cmd_fail = RTL8390_PHY_CTRL_FAIL, + .cmd_read = RTL8390_PHY_CTRL_READ, + .cmd_write = RTL8390_PHY_CTRL_WRITE, + .cmd_regs = { + .broadcast = RTL8390_BCAST_PHYID_CTRL, + .c22_data = RTL8390_PHYREG_ACCESS_CTRL, + .c45_data = RTL8390_PHYREG_MMD_CTRL, + .ext_page = RTL8390_PHYREG_CTRL, + .io_data = RTL8390_PHYREG_DATA_CTRL, + .port_mask_low = RTL8390_PHYREG_PORT_CTRL_LOW, + .port_mask_high = RTL8390_PHYREG_PORT_CTRL_HIGH, + }, + .num_buses = RTL8390_NUM_BUSES, + .num_pages = RTL8390_NUM_PAGES, + .num_ports = RTL8390_NUM_PORTS, + .poll_ctrl = RTL8390_SMI_PORT_POLLING_CTRL, + .read_c22 = otto_emdio_8390_read_c22, + .read_c45 = otto_emdio_8390_read_c45, + .write_c22 = otto_emdio_8390_write_c22, + .write_c45 = otto_emdio_8390_write_c45, +}; + static const struct otto_emdio_info otto_emdio_9300_info = { .addr_map_base = RTL9300_SMI_PORT0_5_ADDR_CTRL, .bus_map_base = RTL9300_SMI_PORT0_15_POLLING_SEL, @@ -1031,6 +1132,7 @@ static const struct otto_emdio_info otto_emdio_9310_info = { static const struct of_device_id otto_emdio_ids[] = { { .compatible = "realtek,rtl8380-mdio", .data = &otto_emdio_8380_info }, + { .compatible = "realtek,rtl8391-mdio", .data = &otto_emdio_8390_info }, { .compatible = "realtek,rtl9301-mdio", .data = &otto_emdio_9300_info }, { .compatible = "realtek,rtl9311-mdio", .data = &otto_emdio_9310_info }, {} From d4ce2cef189ef4c267d5bcec922f5d390a85bc22 Mon Sep 17 00:00:00 2001 From: Markus Stockhausen Date: Mon, 31 Aug 2026 16:34:39 +0200 Subject: [PATCH 13/13] net: mdio: realtek-rtl9300: reword Kconfig and module description The Realtek MDIO driver has been enhanced to support multiple devices from the RTL83xx/RTL93xx series. Reword Kconfig and module description accordingly. Signed-off-by: Markus Stockhausen Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260831143439.2404484-14-markus.stockhausen@gmx.de Signed-off-by: Jakub Kicinski --- drivers/net/mdio/Kconfig | 4 ++-- drivers/net/mdio/mdio-realtek-rtl9300.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/net/mdio/Kconfig b/drivers/net/mdio/Kconfig index a05229838cb4..d44278f26fab 100644 --- a/drivers/net/mdio/Kconfig +++ b/drivers/net/mdio/Kconfig @@ -172,11 +172,11 @@ config MDIO_IPQ8064 interface units of the IPQ8064 SoC config MDIO_REALTEK_RTL9300 - tristate "Realtek RTL9300 MDIO interface support" + tristate "Realtek RTL83xx/RTL93xx MDIO interface support" depends on MACH_REALTEK_RTL || COMPILE_TEST help This driver supports the MDIO interface found in the Realtek - RTL9300 family of Ethernet switches with integrated SoC. + RTL83xx/RTL93xx family of Ethernet switches with integrated SoC. config MDIO_REGMAP tristate diff --git a/drivers/net/mdio/mdio-realtek-rtl9300.c b/drivers/net/mdio/mdio-realtek-rtl9300.c index 86b2040fa333..11d078ef941c 100644 --- a/drivers/net/mdio/mdio-realtek-rtl9300.c +++ b/drivers/net/mdio/mdio-realtek-rtl9300.c @@ -1150,5 +1150,5 @@ static struct platform_driver otto_emdio_driver = { module_platform_driver(otto_emdio_driver); -MODULE_DESCRIPTION("RTL9300 MDIO driver"); +MODULE_DESCRIPTION("RTL83xx/RTL93xx MDIO driver"); MODULE_LICENSE("GPL");