From nobody Sun Feb 8 16:31:12 2026 Received: from smtpout-02.galae.net (smtpout-02.galae.net [185.246.84.56]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id AD9B42248BE; Sun, 1 Feb 2026 15:13:26 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.246.84.56 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1769958814; cv=none; b=N9CQDtOtPoyCbpe3+NuLlojcbFzYCL0HgasAXujs+czi3HPh8HKlCgCQZ3TJM17x0PN6GoIaAcH7H3vhVvkf3cYNN/qExDym3Q5aUXfqDvzn2piwQOwWilS4ORZn+/23tH4M3dF/jITWYKt0Bn1CztWhQGJs8cCTVjp2FaTmmqU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1769958814; c=relaxed/simple; bh=fZD4qkC2vaumUgXUttUMGgs/7wOcVKKX8OfrImD/Kp4=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=UGXfh1XZqJ2y1U2mvc/93MVpjKLQWnO6f699RLtM/JJ8hZ10GM+/z4uE3Tg7migWkOX3etBXb7MNlR+O7XR6D7XUE+hULccTwDR9n8K3yKOx5mQmAfWLwbDuLUCVfAcvJWdJaaCcTwmOG8KIw5xe/BU8y+6HCOA8EJDMfvjn600= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com; spf=pass smtp.mailfrom=bootlin.com; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b=VcUO3oXf; arc=none smtp.client-ip=185.246.84.56 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=bootlin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b="VcUO3oXf" Received: from smtpout-01.galae.net (smtpout-01.galae.net [212.83.139.233]) by smtpout-02.galae.net (Postfix) with ESMTPS id 92CCE1A2B84; Sun, 1 Feb 2026 15:13:25 +0000 (UTC) Received: from mail.galae.net (mail.galae.net [212.83.136.155]) by smtpout-01.galae.net (Postfix) with ESMTPS id 67A3A60720; Sun, 1 Feb 2026 15:13:25 +0000 (UTC) Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with ESMTPSA id 1F0F2119A88F9; Sun, 1 Feb 2026 16:13:21 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=dkim; t=1769958804; h=from:subject:date:message-id:to:cc:mime-version: content-transfer-encoding:in-reply-to:references; bh=kDx1kDq+34hoYGNVSV5JG+uMIF+KB9v72rLjPQGQ424=; b=VcUO3oXfuqSCxk2hnY8Tk6/QtaCoqgBoLr9d2CXWAk6+RFGRR15F1Y+KL27L70HACvGjrw y7e+0toGC5mbvQLYHj8+aE+WmHo+QFbLAkYiTlL/k6lZTEbV//2YMNWaBokmWDF2sIeG1s io5vNgulx/+LBdxmWnnbKkhELi+FP0grIBzl7m7KxTNkbsDPi+Vk2sb9tH/XbbqnXx1jBX PvfFvUpq+GAc10ZFuvhU4AIPCNtiPsszliH/dFnVdVwLdk7c0FLoI7CfnjQSBv5R/7uvia Bue8OYSCUtOV1GbrZpRN7dWkJNtcjKnZ81Asc5x4jzfdJKEYkWWgURZYhk8EnQ== From: Maxime Chevallier To: davem@davemloft.net, Andrew Lunn , Jakub Kicinski , Eric Dumazet , Paolo Abeni , Russell King , Heiner Kallweit Cc: Maxime Chevallier , netdev@vger.kernel.org, linux-kernel@vger.kernel.org, thomas.petazzoni@bootlin.com, Christophe Leroy , Herve Codina , Florian Fainelli , Vladimir Oltean , =?UTF-8?q?K=C3=B6ry=20Maincent?= , =?UTF-8?q?Marek=20Beh=C3=BAn?= , Oleksij Rempel , =?UTF-8?q?Nicol=C3=B2=20Veronese?= , Simon Horman , mwojtas@chromium.org, Romain Gantois , Daniel Golle , Dimitri Fedrau Subject: [PATCH net-next v3 06/11] net: phy: Represent PHY-less SFP modules with phy_port Date: Sun, 1 Feb 2026 16:12:43 +0100 Message-ID: <20260201151249.642015-7-maxime.chevallier@bootlin.com> X-Mailer: git-send-email 2.49.0 In-Reply-To: <20260201151249.642015-1-maxime.chevallier@bootlin.com> References: <20260201151249.642015-1-maxime.chevallier@bootlin.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Last-TLS-Session-Version: TLSv1.3 Content-Type: text/plain; charset="utf-8" Now that the 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 to get the achievable modes on this module based on what the phy_port representing the bus supports. Signed-off-by: Maxime Chevallier --- drivers/net/phy/phy-caps.h | 2 + drivers/net/phy/phy_caps.c | 26 +++++++++++ drivers/net/phy/phy_device.c | 87 +++++++++++++++++++++++++++++++++++- include/linux/phy.h | 6 +++ 4 files changed, 120 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 *support= ed, 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 lon= g *from, + const unsigned long *interfaces); =20 #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 *lin= kmodes) 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 ac= hieve + * 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) =3D {}; + unsigned int ifaces_caps =3D 0; + phy_interface_t interface; + + for_each_set_bit(interface, interfaces, PHY_INTERFACE_MODE_MAX) + ifaces_caps |=3D 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 49696b1a2f13..37efde56e54a 100644 --- a/drivers/net/phy/phy_device.c +++ b/drivers/net/phy/phy_device.c @@ -1483,6 +1483,8 @@ static int phy_sfp_connect_phy(void *upstream, struct= phy_device *phy) struct phy_device *phydev =3D upstream; struct net_device *dev =3D phydev->attached_dev; =20 + phydev->has_sfp_mod_phy =3D true; + if (dev) return phy_link_topo_add_phy(dev, phy, PHY_UPSTREAM_PHY, phydev); =20 @@ -1504,6 +1506,8 @@ static void phy_sfp_disconnect_phy(void *upstream, st= ruct phy_device *phy) struct phy_device *phydev =3D upstream; struct net_device *dev =3D phydev->attached_dev; =20 + phydev->has_sfp_mod_phy =3D false; + if (dev) phy_link_topo_del_phy(dev, phy); } @@ -1609,6 +1613,74 @@ static void phy_sfp_link_down(void *upstream) port->ops->link_down(port); } =20 +static int phy_add_sfp_mod_port(struct phy_device *phydev) +{ + const struct sfp_module_caps *caps; + struct phy_port *port; + int ret =3D 0; + + /* Create mod port */ + port =3D phy_port_alloc(); + if (!port) + return -ENOMEM; + + port->active =3D true; + + caps =3D 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 =3D 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 =3D port; + + return 0; +} + +static void phy_del_sfp_mod_port(struct phy_device *phydev) +{ + if (!phydev->mod_port) + return; + + if (phydev->attached_dev) + phy_link_topo_del_port(phydev->attached_dev, phydev->mod_port); + + phy_port_destroy(phydev->mod_port); + phydev->mod_port =3D NULL; +} + +static int phy_sfp_module_start(void *upstream) +{ + struct phy_device *phydev =3D upstream; + + /* If there's a downstream SFP module, and it doesn't contain a PHY + * device, let's create a phy_port to represent that module. + */ + if (!phydev->has_sfp_mod_phy) + return phy_add_sfp_mod_port(phydev); + + return 0; +} + +static void phy_sfp_module_stop(void *upstream) +{ + struct phy_device *phydev =3D upstream; + + if (!phydev->has_sfp_mod_phy) + phy_del_sfp_mod_port(phydev); +} + static const struct sfp_upstream_ops sfp_phydev_ops =3D { .attach =3D phy_sfp_attach, .detach =3D phy_sfp_detach, @@ -1618,6 +1690,8 @@ static const struct sfp_upstream_ops sfp_phydev_ops = =3D { .link_down =3D phy_sfp_link_down, .connect_phy =3D phy_sfp_connect_phy, .disconnect_phy =3D phy_sfp_disconnect_phy, + .module_start =3D phy_sfp_module_start, + .module_stop =3D phy_sfp_module_stop, }; =20 static int phy_add_port(struct phy_device *phydev, struct phy_port *port) @@ -1700,7 +1774,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 =3D NULL; struct sfp_bus *bus; int ret; =20 @@ -1729,6 +1803,8 @@ static int phy_sfp_probe(struct phy_device *phydev) phy_port_destroy(port); } =20 + phydev->sfp_bus_port =3D port; + return ret; } =20 @@ -1818,6 +1894,12 @@ int phy_attach_direct(struct net_device *dev, struct= phy_device *phydev, err =3D phy_link_topo_add_phy(dev, phydev, PHY_UPSTREAM_MAC, dev); if (err) goto error; + + if (phydev->mod_port) { + err =3D phy_link_topo_add_port(dev, phydev->mod_port); + if (err) + goto error; + } } =20 /* Some Ethernet drivers try to connect to a PHY device before @@ -1991,6 +2073,8 @@ void phy_detach(struct phy_device *phydev) phydev->attached_dev->phydev =3D NULL; phydev->attached_dev =3D NULL; phy_link_topo_del_phy(dev, phydev); + if (phydev->mod_port) + phy_link_topo_del_port(dev, phydev->mod_port); } =20 phydev->phy_link_change =3D NULL; @@ -3818,6 +3902,7 @@ static int phy_remove(struct device *dev) =20 sfp_bus_del_upstream(phydev->sfp_bus); phydev->sfp_bus =3D NULL; + phydev->sfp_bus_port =3D NULL; =20 if (phydev->drv && phydev->drv->remove) phydev->drv->remove(phydev); diff --git a/include/linux/phy.h b/include/linux/phy.h index 6f9979a26892..9114fdff4c4f 100644 --- a/include/linux/phy.h +++ b/include/linux/phy.h @@ -582,6 +582,7 @@ struct phy_oatc14_sqi_capability { * @wol_enabled: Set to true if the PHY or the attached MAC have Wake-on-L= AN * enabled. * @is_genphy_driven: PHY is driven by one of the generic PHY drivers + * @has_sfp_mod_phy: Set true if downstream SFP bus's module contains a PHY * @state: State of the PHY for management purposes * @dev_flags: Device-specific flags used by the PHY driver. * @@ -594,6 +595,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: i= n the * link state. @@ -704,6 +707,7 @@ struct phy_device { unsigned irq_rerun:1; =20 unsigned default_timestamp:1; + unsigned has_sfp_mod_phy:1; =20 int rate_matching; =20 @@ -782,6 +786,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; --=20 2.49.0