[PATCH net-next 07/13] net: phy: Represent PHY-less SFP modules with phy_port

Maxime Chevallier
2026-01-27T13:42:37Z

Now that th SFP bus infrastructure notifies when PHY-less modules are
connected, we can create a phy_port to represent it. Instead of letting
the SFP subsystem handle that, the Bus' upstream is in charge of
maintaining that phy_port and register it to the topology, as the
upstream (in this case a phy device) is directly interacting with the
underlying net_device.

Add a phy_caps helper alongside to get the achievable modes on this
module based on what the phy_port representing the bus supports.

Signed-off-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
---
drivers/net/phy/phy-caps.h | 2 ++
drivers/net/phy/phy_caps.c | 26 +++++++++++++++
drivers/net/phy/phy_device.c | 62 +++++++++++++++++++++++++++++++++++-
include/linux/phy.h | 4 +++
4 files changed, 93 insertions(+), 1 deletion(-)

diff --git a/drivers/net/phy/phy-caps.h b/drivers/net/phy/phy-caps.h
index 421088e6f6e8..ec3d39a0ae06 100644
--- a/drivers/net/phy/phy-caps.h
+++ b/drivers/net/phy/phy-caps.h
@@ -66,5 +66,7 @@ void phy_caps_medium_get_supported(unsigned long *supported,
enum ethtool_link_medium medium,
int lanes);
u32 phy_caps_mediums_from_linkmodes(unsigned long *linkmodes);
+void phy_caps_linkmode_filter_ifaces(unsigned long *to, const unsigned long *from,
+ const unsigned long *interfaces);

#endif /* __PHY_CAPS_H */
diff --git a/drivers/net/phy/phy_caps.c b/drivers/net/phy/phy_caps.c
index 942d43191561..558e4df4d63c 100644
--- a/drivers/net/phy/phy_caps.c
+++ b/drivers/net/phy/phy_caps.c
@@ -445,3 +445,29 @@ u32 phy_caps_mediums_from_linkmodes(unsigned long *linkmodes)
return mediums;
}
EXPORT_SYMBOL_GPL(phy_caps_mediums_from_linkmodes);
+
+/**
+ * phy_caps_linkmode_filter_ifaces() - Filter linkmodes with an interface list
+ * @to: Stores the filtered linkmodes
+ * @from: Linkmodes to filter
+ * @interfaces: Bitfield of phy_interface_t that we use for filtering
+ *
+ * Filter the provided linkmodes, only to keep the ones we can possibly achieve
+ * when using any of the provided MII interfaces.
+ */
+void phy_caps_linkmode_filter_ifaces(unsigned long *to,
+ const unsigned long *from,
+ const unsigned long *interfaces)
+{
+ __ETHTOOL_DECLARE_LINK_MODE_MASK(ifaces_supported) = {};
+ unsigned int ifaces_caps = 0;
+ phy_interface_t interface;
+
+ for_each_set_bit(interface, interfaces, PHY_INTERFACE_MODE_MAX)
+ ifaces_caps |= phy_caps_from_interface(interface);
+
+ phy_caps_linkmodes(ifaces_caps, ifaces_supported);
+
+ linkmode_and(to, from, ifaces_supported);
+}
+EXPORT_SYMBOL_GPL(phy_caps_linkmode_filter_ifaces);
diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c
index 819c9e81bdef..12c8c2ff2c06 100644
--- a/drivers/net/phy/phy_device.c
+++ b/drivers/net/phy/phy_device.c
@@ -1609,6 +1609,53 @@ static void phy_sfp_link_down(void *upstream)
port->ops->link_down(port);
}

+static int phy_sfp_connect_nophy(void *upstream)
+{
+ struct phy_device *phydev = upstream;
+ const struct sfp_module_caps *caps;
+ struct phy_port *port;
+ int ret = 0;
+
+ /* Create mod port */
+ port = phy_port_alloc();
+ if (!port)
+ return -ENOMEM;
+
+ port->active = true;
+
+ caps = sfp_get_module_caps(phydev->sfp_bus);
+
+ phy_caps_linkmode_filter_ifaces(port->supported, caps->link_modes,
+ phydev->sfp_bus_port->interfaces);
+
+ if (phydev->attached_dev) {
+ ret = phy_link_topo_add_port(phydev->attached_dev, port);
+ if (ret) {
+ phy_port_destroy(port);
+ return ret;
+ }
+ }
+
+ /* we don't use phy_add_port() here as the module port isn't a direct
+ * interface from the PHY, but rather an extension to the sfp-bus, that
+ * is already represented by its own phy_port
+ */
+ phydev->mod_port = port;
+
+ return 0;
+}
+
+static void phy_sfp_disconnect_nophy(void *upstream)
+{
+ struct phy_device *phydev = upstream;
+
+ if (phydev->attached_dev)
+ phy_link_topo_del_port(phydev->attached_dev, phydev->mod_port);
+
+ phy_port_destroy(phydev->mod_port);
+ phydev->mod_port = NULL;
+}
+
static const struct sfp_upstream_ops sfp_phydev_ops = {
.attach = phy_sfp_attach,
.detach = phy_sfp_detach,
@@ -1618,6 +1665,8 @@ static const struct sfp_upstream_ops sfp_phydev_ops = {
.link_down = phy_sfp_link_down,
.connect_phy = phy_sfp_connect_phy,
.disconnect_phy = phy_sfp_disconnect_phy,
+ .connect_nophy = phy_sfp_connect_nophy,
+ .disconnect_nophy = phy_sfp_disconnect_nophy,
};

static int phy_add_port(struct phy_device *phydev, struct phy_port *port)
@@ -1700,7 +1749,7 @@ static struct phy_port *phy_setup_sfp_port(struct phy_device *phydev)
*/
static int phy_sfp_probe(struct phy_device *phydev)
{
- struct phy_port *port;
+ struct phy_port *port = NULL;
struct sfp_bus *bus;
int ret;

@@ -1727,6 +1776,8 @@ static int phy_sfp_probe(struct phy_device *phydev)
if (ret && port)
phy_del_port(phydev, port);

+ phydev->sfp_bus_port = port;
+
return ret;
}

@@ -1816,6 +1867,12 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
err = phy_link_topo_add_phy(dev, phydev, PHY_UPSTREAM_MAC, dev);
if (err)
goto error;
+
+ if (phydev->mod_port) {
+ err = phy_link_topo_add_port(dev, phydev->mod_port);
+ if (err)
+ goto error;
+ }
}

/* Some Ethernet drivers try to connect to a PHY device before
@@ -1989,6 +2046,8 @@ void phy_detach(struct phy_device *phydev)
phydev->attached_dev->phydev = NULL;
phydev->attached_dev = NULL;
phy_link_topo_del_phy(dev, phydev);
+ if (phydev->mod_port)
+ phy_link_topo_del_port(dev, phydev->mod_port);
}

phydev->phy_link_change = NULL;
@@ -3816,6 +3875,7 @@ static int phy_remove(struct device *dev)

sfp_bus_del_upstream(phydev->sfp_bus);
phydev->sfp_bus = NULL;
+ phydev->sfp_bus_port = NULL;

if (phydev->drv && phydev->drv->remove)
phydev->drv->remove(phydev);
diff --git a/include/linux/phy.h b/include/linux/phy.h
index 6f9979a26892..dc788ae4da64 100644
--- a/include/linux/phy.h
+++ b/include/linux/phy.h
@@ -594,6 +594,8 @@ struct phy_oatc14_sqi_capability {
* @phylink: Pointer to phylink instance for this PHY
* @sfp_bus_attached: Flag indicating whether the SFP bus has been attached
* @sfp_bus: SFP bus attached to this PHY's fiber port
+ * @sfp_bus_port: The phy_port connected to the downstream SFP bus
+ * @mod_port: phy_port representing the SFP module, if it is phy-less
* @attached_dev: The attached enet driver's device instance ptr
* @adjust_link: Callback for the enet controller to respond to changes: in the
* link state.
@@ -782,6 +784,8 @@ struct phy_device {
/* This may be modified under the rtnl lock */
bool sfp_bus_attached;
struct sfp_bus *sfp_bus;
+ struct phy_port *sfp_bus_port;
+ struct phy_port *mod_port;
struct phylink *phylink;
struct net_device *attached_dev;
struct mii_timestamper *mii_ts;
--
2.49.0