mirror of
https://github.com/torvalds/linux.git
synced 2026-11-07 06:17:02 +01:00
Merge branch 'net-phy_port-sfp-modules-representation-and-phy_port-listing'
Maxime Chevallier says: ==================== net: phy_port: SFP modules representation and phy_port listing Thanks to Christophe for reviewing and testing, this is much appreciated. This work extends on the recent addition of phy_port representation to enable listing the front-facing ports of an interface. For now, we don't control these ports, we merely list their presence and their capabilities. As the most common use-case of multi-port interfaces is combo-ports that provide both RJ45 and SFP connectors on a single MAC, there's a lot of SFP stuff in this series. This series is in 2 main parts. The first one aims at representing the SFP cages and modules using phy_port, as combo-ports with RJ45 + SFP are by far the most common cases for multi-connector setups. The second part is the netlink interface to list those ports, now that most use-cases are covered. Let's see what we can do with some examples of the new ethtool API : - Get MII interfaces supported by an empty SFP cage : Port for eth3: Port id: 1 Supported MII interfaces : sgmii, 1000base-x, 2500base-x Port type: sfp - Get Combo-ports supported modes, on each port : Port for eth1: Port id: 1 Supported link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full 10000baseT/Full 2500baseT/Full 5000baseT/Full Port type: mdi Port for eth1: Port id: 2 Supported MII interfaces : 10gbase-r Port type: sfp - Get Achievable linkmodes on a SFP module (combo port with a DAC in the SFP cage) Port for eth1: Port id: 1 Supported link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full 10000baseT/Full 2500baseT/Full 5000baseT/Full Port type: mdi Port for eth1: Port id: 2 Supported MII interfaces : 10gbase-r Port type: sfp Port for eth1: Port id: 3 Upstream id: 2 Supported link modes: 10000baseCR/Full Port type: mdi Note that here, we have 3 ports : - The Copper port - The SFP Cage itself, - The SFP module This series builds on top of phy_port and phy_link_topology to allow tracking the ports of an interface. We maintain a list of supported linkmodes/interfaces on each port, which allows for fine-grained reporting of each port's capability. What this series doesn't do : - We don't support selecting which port is active. This is the next step. - We only support PHY-driven combo ports. The end-goal of this whole journey that started with phy_link_topology is to get support for MII muxes, such as the one we have on the Turris Omnia. This will eventually be upstreamed as well. ==================== Link: https://patch.msgid.link/20261016133107.54054-1-maxime.chevallier@bootlin.com Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
commit
de4aa72d6b
|
|
@ -212,6 +212,11 @@ definitions:
|
|||
-
|
||||
name: discard
|
||||
value: 31
|
||||
-
|
||||
name: port-type
|
||||
doc: type of physical connector
|
||||
type: enum
|
||||
entries: [mdi, sfp]
|
||||
|
||||
attribute-sets:
|
||||
-
|
||||
|
|
@ -1907,6 +1912,32 @@ attribute-sets:
|
|||
name: link
|
||||
type: nest
|
||||
nested-attributes: mse-snapshot
|
||||
-
|
||||
name: port
|
||||
attr-cnt-name: --ethtool-a-port-cnt
|
||||
attributes:
|
||||
-
|
||||
name: header
|
||||
type: nest
|
||||
nested-attributes: header
|
||||
-
|
||||
name: id
|
||||
type: u32
|
||||
-
|
||||
name: supported-modes
|
||||
type: nest
|
||||
nested-attributes: bitset
|
||||
-
|
||||
name: supported-interfaces
|
||||
type: nest
|
||||
nested-attributes: bitset
|
||||
-
|
||||
name: type
|
||||
type: u32
|
||||
enum: port-type
|
||||
-
|
||||
name: upstream-port
|
||||
type: u32
|
||||
|
||||
operations:
|
||||
enum-model: directional
|
||||
|
|
@ -2861,6 +2892,30 @@ operations:
|
|||
- worst-channel
|
||||
- link
|
||||
dump: *mse-get-op
|
||||
-
|
||||
name: port-get
|
||||
doc: Get ports attached to an interface
|
||||
|
||||
attribute-set: port
|
||||
|
||||
do:
|
||||
request:
|
||||
attributes:
|
||||
- header
|
||||
- id
|
||||
reply: &port-get-op-reply
|
||||
attributes:
|
||||
- header
|
||||
- id
|
||||
- supported-modes
|
||||
- supported-interfaces
|
||||
- type
|
||||
- upstream-port
|
||||
dump:
|
||||
request:
|
||||
attributes:
|
||||
- header
|
||||
reply: *port-get-op-reply
|
||||
|
||||
mcast-groups:
|
||||
list:
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ Userspace to kernel:
|
|||
``ETHTOOL_MSG_RSS_CREATE_ACT`` create an additional RSS context
|
||||
``ETHTOOL_MSG_RSS_DELETE_ACT`` delete an additional RSS context
|
||||
``ETHTOOL_MSG_MSE_GET`` get MSE diagnostic data
|
||||
``ETHTOOL_MSG_PORT_GET`` get port information
|
||||
===================================== =================================
|
||||
|
||||
Kernel to userspace:
|
||||
|
|
@ -305,6 +306,7 @@ Kernel to userspace:
|
|||
``ETHTOOL_MSG_RSS_CREATE_NTF`` additional RSS context created
|
||||
``ETHTOOL_MSG_RSS_DELETE_NTF`` additional RSS context deleted
|
||||
``ETHTOOL_MSG_MSE_GET_REPLY`` MSE diagnostic data
|
||||
``ETHTOOL_MSG_PORT_GET_REPLY`` port information
|
||||
======================================== =================================
|
||||
|
||||
``GET`` requests are sent by userspace applications to retrieve device
|
||||
|
|
@ -2537,6 +2539,39 @@ Within each channel nest, only the metrics supported by the PHY will be present.
|
|||
See ``struct phy_mse_snapshot`` kernel documentation in
|
||||
``include/linux/phy.h``.
|
||||
|
||||
PORT_GET
|
||||
========
|
||||
|
||||
Retrieve information about the physical connection points of a network device,
|
||||
referred to as "ports". User needs to specify a PORT_ID for the DO operation,
|
||||
in which case the DO request returns information about that specific port.
|
||||
|
||||
As there can be more than one port, the DUMP operation can be used to list the
|
||||
ports present on a given interface, by passing an interface index or name in
|
||||
the dump request.
|
||||
|
||||
Request contents:
|
||||
|
||||
===================================== ====== ===============================
|
||||
``ETHTOOL_A_PORT_HEADER`` nested request header
|
||||
``ETHTOOL_A_PORT_ID`` u32 port id
|
||||
===================================== ====== ===============================
|
||||
|
||||
Kernel response contents:
|
||||
|
||||
======================================= ====== =============================
|
||||
``ETHTOOL_A_PORT_HEADER`` nested request header
|
||||
``ETHTOOL_A_PORT_ID`` u32 the port's unique identifier,
|
||||
per netdevice.
|
||||
``ETHTOOL_A_PORT_SUPPORTED_MODES`` bitset bitset of supported linkmodes
|
||||
``ETHTOOL_A_PORT_SUPPORTED_INTERFACES`` bitset bitset of supported MII
|
||||
interfaces
|
||||
``ETHTOOL_A_PORT_TYPE`` u32 the port type
|
||||
``ETHTOOL_A_PORT_UPSTREAM_PORT`` u32 If any, the index of the MII
|
||||
port that feeds into this
|
||||
port.
|
||||
======================================= ====== =============================
|
||||
|
||||
Request translation
|
||||
===================
|
||||
|
||||
|
|
@ -2647,4 +2682,5 @@ are netlink only.
|
|||
n/a ``ETHTOOL_MSG_PHY_GET``
|
||||
``SIOCGHWTSTAMP`` ``ETHTOOL_MSG_TSCONFIG_GET``
|
||||
``SIOCSHWTSTAMP`` ``ETHTOOL_MSG_TSCONFIG_SET``
|
||||
n/a ``ETHTOOL_MSG_PORT_GET``
|
||||
=================================== =====================================
|
||||
|
|
|
|||
|
|
@ -99,13 +99,29 @@ will eventually be able to report its own ksettings::
|
|||
(_____)-----| Port |
|
||||
+------+
|
||||
|
||||
SFP ports
|
||||
=========
|
||||
|
||||
SFP interfaces involve 2 distinct components, each represented by
|
||||
a :c:type:`struct phy_port <phy_port>` instance :
|
||||
|
||||
- The SFP cage itself is a :c:type:`struct phy_port <phy_port>`. It's special
|
||||
in that it's not an MDI interface, but rather a hot-pluggable MII.
|
||||
The :c:type:`struct phy_port <phy_port>` associated to it lists the different
|
||||
MII interfaces we can use on the cage.
|
||||
|
||||
- The SFP module, when inserted, will also be associated to a
|
||||
:c:type:`struct phy_port <phy_port>`, that represents the various linkmodes
|
||||
that it gives access to. The module's :c:type:`struct phy_port <phy_port>`
|
||||
doesn't supersede the cage's port, it references it through
|
||||
the :c:type:`struct phy_port <phy_port>` :c:member:`upstream_port` field.
|
||||
|
||||
Next steps
|
||||
==========
|
||||
|
||||
As of writing this documentation, only ports controlled by PHY devices are
|
||||
supported. The next steps will be to add the Netlink API to expose these
|
||||
to userspace and add support for raw ports (controlled by some firmware, and directly
|
||||
managed by the NIC driver).
|
||||
As of writing this documentation, the port's presence and information can only
|
||||
be queried, and it's not possible to change any of the port's settings or select
|
||||
which one should be used.
|
||||
|
||||
Another parallel task is the introduction of a MII muxing framework to allow the
|
||||
control of non-PHY driver multi-port setups.
|
||||
control of non-PHY driven multi-port setups.
|
||||
|
|
|
|||
|
|
@ -19024,6 +19024,7 @@ F: Documentation/devicetree/bindings/net/ethernet-connector.yaml
|
|||
F: Documentation/networking/phy-port.rst
|
||||
F: drivers/net/phy/phy_port.c
|
||||
F: include/linux/phy_port.h
|
||||
F: net/ethtool/port.c
|
||||
K: struct\s+phy_port|phy_port_
|
||||
|
||||
NETWORKING [GENERAL]
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1400,11 +1400,22 @@ static int phy_sfp_connect_phy(void *upstream, struct phy_device *phy)
|
|||
{
|
||||
struct phy_device *phydev = upstream;
|
||||
struct net_device *dev = phydev->attached_dev;
|
||||
int ret;
|
||||
|
||||
if (dev)
|
||||
return phy_link_topo_add_phy(dev, phy, PHY_UPSTREAM_PHY, phydev);
|
||||
phydev->has_sfp_mod_phy = true;
|
||||
phy_set_upstream_port(phy, phydev->sfp_cage_port);
|
||||
|
||||
return 0;
|
||||
/* If we aren't attached to a netdev, we can't add the SFP PHY to its
|
||||
* topology.
|
||||
*/
|
||||
if (!dev)
|
||||
return 0;
|
||||
|
||||
ret = phy_link_topo_add_phy(dev, phy, PHY_UPSTREAM_PHY, phydev);
|
||||
if (ret)
|
||||
phydev->has_sfp_mod_phy = false;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1422,8 +1433,12 @@ static void phy_sfp_disconnect_phy(void *upstream, struct phy_device *phy)
|
|||
struct phy_device *phydev = upstream;
|
||||
struct net_device *dev = phydev->attached_dev;
|
||||
|
||||
phydev->has_sfp_mod_phy = false;
|
||||
|
||||
if (dev)
|
||||
phy_link_topo_del_phy(dev, phy);
|
||||
|
||||
phy_set_upstream_port(phy, NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1527,6 +1542,80 @@ static void phy_sfp_link_down(void *upstream)
|
|||
port->ops->link_down(port);
|
||||
}
|
||||
|
||||
static int phy_add_sfp_mod_port(struct phy_device *phydev)
|
||||
{
|
||||
DECLARE_PHY_INTERFACE_MASK(interfaces);
|
||||
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_interface_and(interfaces, phydev->sfp_cage_port->interfaces,
|
||||
caps->interfaces);
|
||||
phy_caps_linkmode_filter_ifaces(port->supported, caps->link_modes,
|
||||
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;
|
||||
|
||||
port->upstream_port = phydev->sfp_cage_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 = NULL;
|
||||
}
|
||||
|
||||
static int phy_sfp_module_start(void *upstream)
|
||||
{
|
||||
struct phy_device *phydev = 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 = upstream;
|
||||
|
||||
/* Called upon module removal or upstream removal */
|
||||
if (!phydev->has_sfp_mod_phy)
|
||||
phy_del_sfp_mod_port(phydev);
|
||||
}
|
||||
|
||||
static const struct sfp_upstream_ops sfp_phydev_ops = {
|
||||
.attach = phy_sfp_attach,
|
||||
.detach = phy_sfp_detach,
|
||||
|
|
@ -1536,6 +1625,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,
|
||||
.module_start = phy_sfp_module_start,
|
||||
.module_stop = phy_sfp_module_stop,
|
||||
};
|
||||
|
||||
static int phy_add_port(struct phy_device *phydev, struct phy_port *port)
|
||||
|
|
@ -1580,16 +1671,19 @@ static void phy_del_port(struct phy_device *phydev, struct phy_port *port)
|
|||
|
||||
list_del(&port->head);
|
||||
|
||||
if (phydev->attached_dev)
|
||||
phy_link_topo_del_port(phydev->attached_dev, port);
|
||||
|
||||
phydev->n_ports--;
|
||||
}
|
||||
|
||||
static int phy_setup_sfp_port(struct phy_device *phydev)
|
||||
static struct phy_port *phy_setup_sfp_port(struct phy_device *phydev)
|
||||
{
|
||||
struct phy_port *port = phy_port_alloc();
|
||||
int ret;
|
||||
|
||||
if (!port)
|
||||
return -ENOMEM;
|
||||
return ERR_PTR(-ENOMEM);
|
||||
|
||||
port->parent_type = PHY_PORT_PHY;
|
||||
port->phy = phydev;
|
||||
|
|
@ -1604,10 +1698,12 @@ static int phy_setup_sfp_port(struct phy_device *phydev)
|
|||
* when attaching the port to the phydev.
|
||||
*/
|
||||
ret = phy_add_port(phydev, port);
|
||||
if (ret)
|
||||
if (ret) {
|
||||
phy_port_destroy(port);
|
||||
return ERR_PTR(ret);
|
||||
}
|
||||
|
||||
return ret;
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1616,32 +1712,51 @@ static int phy_setup_sfp_port(struct phy_device *phydev)
|
|||
*/
|
||||
static int phy_sfp_probe(struct phy_device *phydev)
|
||||
{
|
||||
struct phy_port *port;
|
||||
struct sfp_bus *bus;
|
||||
int ret = 0;
|
||||
|
||||
if (phydev->mdio.dev.fwnode) {
|
||||
bus = sfp_bus_find_fwnode(phydev->mdio.dev.fwnode);
|
||||
if (IS_ERR(bus))
|
||||
return PTR_ERR(bus);
|
||||
if (!phydev->mdio.dev.fwnode)
|
||||
return 0;
|
||||
|
||||
phydev->sfp_bus = bus;
|
||||
bus = sfp_bus_find_fwnode(phydev->mdio.dev.fwnode);
|
||||
if (IS_ERR(bus))
|
||||
return PTR_ERR(bus);
|
||||
|
||||
ret = sfp_bus_add_upstream(bus, phydev, &sfp_phydev_ops);
|
||||
sfp_bus_put(bus);
|
||||
phydev->sfp_bus = bus;
|
||||
|
||||
if (ret)
|
||||
phydev->sfp_bus = NULL;
|
||||
}
|
||||
|
||||
if (!ret && phydev->sfp_bus) {
|
||||
ret = phy_setup_sfp_port(phydev);
|
||||
if (ret) {
|
||||
sfp_bus_del_upstream(phydev->sfp_bus);
|
||||
if (bus) {
|
||||
port = phy_setup_sfp_port(phydev);
|
||||
if (IS_ERR(port)) {
|
||||
sfp_bus_put(bus);
|
||||
phydev->sfp_bus = NULL;
|
||||
return PTR_ERR(port);
|
||||
}
|
||||
|
||||
phydev->sfp_cage_port = port;
|
||||
}
|
||||
|
||||
return ret;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int phy_sfp_add_upstream(struct phy_device *phydev)
|
||||
{
|
||||
struct sfp_bus *bus = phydev->sfp_bus;
|
||||
int ret;
|
||||
|
||||
ret = sfp_bus_add_upstream(bus, phydev, &sfp_phydev_ops);
|
||||
if (ret) {
|
||||
sfp_bus_put(bus);
|
||||
phydev->sfp_bus = NULL;
|
||||
phydev->sfp_cage_port = NULL;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* sfp_bus_add_upstream() grabs a ref to the sfp bus on success, it's
|
||||
* safe to release it now.
|
||||
*/
|
||||
sfp_bus_put(bus);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1723,6 +1838,8 @@ static void phy_detach_internal(struct phy_device *phydev, bool notify_bus)
|
|||
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;
|
||||
|
|
@ -1859,6 +1976,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
|
||||
|
|
@ -3553,6 +3676,10 @@ static int phy_setup_ports(struct phy_device *phydev)
|
|||
break;
|
||||
}
|
||||
|
||||
ret = phy_sfp_add_upstream(phydev);
|
||||
if (ret)
|
||||
goto err_sfp;
|
||||
|
||||
return 0;
|
||||
|
||||
err_sfp:
|
||||
|
|
@ -3581,6 +3708,25 @@ struct phy_port *phy_get_sfp_port(struct phy_device *phydev)
|
|||
}
|
||||
EXPORT_SYMBOL_GPL(phy_get_sfp_port);
|
||||
|
||||
/**
|
||||
* phy_set_upstream_port() - Sets the phy_port controlling the MII this PHY is
|
||||
* attached to.
|
||||
* @phydev: pointer to the PHY device we set the upstream of.
|
||||
* @port: The phy_port upstream of this PHY, can be NULL.
|
||||
*/
|
||||
void phy_set_upstream_port(struct phy_device *phydev, struct phy_port *port)
|
||||
{
|
||||
struct phy_port *local_port;
|
||||
|
||||
ASSERT_RTNL();
|
||||
|
||||
phydev->upstream_port = port;
|
||||
|
||||
phy_for_each_port(phydev, local_port)
|
||||
local_port->upstream_port = port;
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(phy_set_upstream_port);
|
||||
|
||||
/**
|
||||
* fwnode_mdio_find_device - Given a fwnode, find the mdio_device
|
||||
* @fwnode: pointer to the mdio_device's fwnode
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
#include <linux/phy_link_topology.h>
|
||||
#include <linux/phy_port.h>
|
||||
#include <linux/phy.h>
|
||||
#include <linux/rtnetlink.h>
|
||||
#include <linux/xarray.h>
|
||||
|
|
@ -23,17 +24,89 @@ static int netdev_alloc_phy_link_topology(struct net_device *dev)
|
|||
xa_init_flags(&topo->phys, XA_FLAGS_ALLOC1);
|
||||
topo->next_phy_index = 1;
|
||||
|
||||
xa_init_flags(&topo->ports, XA_FLAGS_ALLOC1);
|
||||
topo->next_port_index = 1;
|
||||
|
||||
dev->link_topo = topo;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct phy_link_topology *phy_link_topo_get_or_alloc(struct net_device *dev)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (dev->link_topo)
|
||||
return dev->link_topo;
|
||||
|
||||
/* The topology is allocated the first time we add an object to it.
|
||||
* It is freed alongside the netdev. It can be called on multiple
|
||||
* contexts:
|
||||
* - It can be called from .probe() : No rtnl, no netdev_lock
|
||||
* - .ndo_open() : rtnl and possibly netdev_lock
|
||||
* - SFP state machine : rtnl held or not
|
||||
*
|
||||
* However, we can't really have races :
|
||||
* - If we have a PHY, phy_link_topo_add_phy() will always run first
|
||||
* and trigger the alloc. Only then the ports can be added through
|
||||
* phylib or sfp.
|
||||
* - If we don't, the SFP port for the cage is registered first, and
|
||||
* only then other ports/PHYs can be registered.
|
||||
*/
|
||||
ret = netdev_alloc_phy_link_topology(dev);
|
||||
if (ret)
|
||||
return ERR_PTR(ret);
|
||||
|
||||
return dev->link_topo;
|
||||
}
|
||||
|
||||
int phy_link_topo_add_port(struct net_device *dev, struct phy_port *port)
|
||||
{
|
||||
struct phy_link_topology *topo;
|
||||
int ret;
|
||||
|
||||
/* Ports can now be queried without rtnl for ops-locked devices, which
|
||||
* we don't support now as port retrieval is done under rtnl.
|
||||
* We don't have phy_port enabled devices yet, let's make sure
|
||||
* we are loudly warned about that when it happens.
|
||||
*/
|
||||
if (WARN_ON_ONCE(dev->netdev_ops && netdev_need_ops_lock(dev)))
|
||||
return -EOPNOTSUPP;
|
||||
|
||||
topo = phy_link_topo_get_or_alloc(dev);
|
||||
if (IS_ERR(topo))
|
||||
return PTR_ERR(topo);
|
||||
|
||||
/* Attempt to re-use a previously allocated port_id */
|
||||
if (port->id)
|
||||
ret = xa_insert(&topo->ports, port->id, port, GFP_KERNEL);
|
||||
else
|
||||
ret = xa_alloc_cyclic(&topo->ports, &port->id, port,
|
||||
xa_limit_32b, &topo->next_port_index,
|
||||
GFP_KERNEL);
|
||||
|
||||
return ret < 0 ? ret : 0;
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(phy_link_topo_add_port);
|
||||
|
||||
void phy_link_topo_del_port(struct net_device *dev, struct phy_port *port)
|
||||
{
|
||||
struct phy_link_topology *topo = dev->link_topo;
|
||||
|
||||
if (!topo)
|
||||
return;
|
||||
|
||||
xa_erase(&topo->ports, port->id);
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(phy_link_topo_del_port);
|
||||
|
||||
int phy_link_topo_add_phy(struct net_device *dev,
|
||||
struct phy_device *phy,
|
||||
enum phy_upstream upt, void *upstream)
|
||||
{
|
||||
struct phy_link_topology *topo = dev->link_topo;
|
||||
struct phy_link_topology *topo;
|
||||
struct phy_device_node *pdn;
|
||||
struct phy_port *port;
|
||||
int ret;
|
||||
|
||||
/* ethtool ops may run without rtnl_lock, and rtnl_lock is what
|
||||
|
|
@ -45,13 +118,9 @@ int phy_link_topo_add_phy(struct net_device *dev,
|
|||
if (WARN_ON_ONCE(netdev_need_ops_lock(dev)))
|
||||
return -EOPNOTSUPP;
|
||||
|
||||
if (!topo) {
|
||||
ret = netdev_alloc_phy_link_topology(dev);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
topo = dev->link_topo;
|
||||
}
|
||||
topo = phy_link_topo_get_or_alloc(dev);
|
||||
if (IS_ERR(topo))
|
||||
return PTR_ERR(topo);
|
||||
|
||||
pdn = kzalloc_obj(*pdn);
|
||||
if (!pdn)
|
||||
|
|
@ -86,8 +155,20 @@ int phy_link_topo_add_phy(struct net_device *dev,
|
|||
if (ret < 0)
|
||||
goto err;
|
||||
|
||||
/* Add all the PHY's ports to the topology */
|
||||
list_for_each_entry(port, &phy->ports, head) {
|
||||
ret = phy_link_topo_add_port(dev, port);
|
||||
if (ret)
|
||||
goto del_ports;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
del_ports:
|
||||
list_for_each_entry_continue_reverse(port, &phy->ports, head)
|
||||
phy_link_topo_del_port(dev, port);
|
||||
|
||||
xa_erase(&topo->phys, phy->phyindex);
|
||||
err:
|
||||
kfree(pdn);
|
||||
return ret;
|
||||
|
|
@ -99,10 +180,14 @@ void phy_link_topo_del_phy(struct net_device *dev,
|
|||
{
|
||||
struct phy_link_topology *topo = dev->link_topo;
|
||||
struct phy_device_node *pdn;
|
||||
struct phy_port *port;
|
||||
|
||||
if (!topo)
|
||||
return;
|
||||
|
||||
list_for_each_entry(port, &phy->ports, head)
|
||||
phy_link_topo_del_port(dev, port);
|
||||
|
||||
pdn = xa_erase(&topo->phys, phy->phyindex);
|
||||
|
||||
/* We delete the PHY from the topology, however we don't re-set the
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
#include <linux/of_mdio.h>
|
||||
#include <linux/phy.h>
|
||||
#include <linux/phy_fixed.h>
|
||||
#include <linux/phy_link_topology.h>
|
||||
#include <linux/phy_port.h>
|
||||
#include <linux/phylink.h>
|
||||
#include <linux/rtnetlink.h>
|
||||
#include <linux/spinlock.h>
|
||||
|
|
@ -93,6 +95,8 @@ struct phylink {
|
|||
DECLARE_PHY_INTERFACE_MASK(sfp_interfaces);
|
||||
__ETHTOOL_DECLARE_LINK_MODE_MASK(sfp_support);
|
||||
u8 sfp_port;
|
||||
struct phy_port *sfp_cage_port;
|
||||
struct phy_port *mod_port;
|
||||
|
||||
struct eee_config eee_cfg;
|
||||
|
||||
|
|
@ -1767,6 +1771,57 @@ static void phylink_fixed_poll(struct timer_list *t)
|
|||
|
||||
static const struct sfp_upstream_ops sfp_phylink_ops;
|
||||
|
||||
static int phylink_create_sfp_cage_port(struct phylink *pl)
|
||||
{
|
||||
struct phy_port *port;
|
||||
int ret = 0;
|
||||
|
||||
if (!pl->netdev || !pl->sfp_bus)
|
||||
return 0;
|
||||
|
||||
port = phy_port_alloc();
|
||||
if (!port)
|
||||
return -ENOMEM;
|
||||
|
||||
port->is_sfp = true;
|
||||
port->is_mii = true;
|
||||
port->active = true;
|
||||
|
||||
phy_interface_and(port->interfaces, pl->config->supported_interfaces,
|
||||
phylink_sfp_interfaces);
|
||||
phy_port_update_supported(port);
|
||||
|
||||
rtnl_lock();
|
||||
ret = phy_link_topo_add_port(pl->netdev, port);
|
||||
rtnl_unlock();
|
||||
if (ret)
|
||||
goto out_destroy_port;
|
||||
|
||||
pl->sfp_cage_port = port;
|
||||
|
||||
return 0;
|
||||
|
||||
out_destroy_port:
|
||||
phy_port_destroy(port);
|
||||
pl->sfp_cage_port = NULL;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void phylink_destroy_sfp_cage_port(struct phylink *pl)
|
||||
{
|
||||
if (pl->netdev && pl->sfp_cage_port) {
|
||||
/* The port may still be accessed under RTNL */
|
||||
rtnl_lock();
|
||||
phy_link_topo_del_port(pl->netdev, pl->sfp_cage_port);
|
||||
rtnl_unlock();
|
||||
}
|
||||
|
||||
if (pl->sfp_cage_port)
|
||||
phy_port_destroy(pl->sfp_cage_port);
|
||||
|
||||
pl->sfp_cage_port = NULL;
|
||||
}
|
||||
|
||||
static int phylink_register_sfp(struct phylink *pl,
|
||||
const struct fwnode_handle *fwnode)
|
||||
{
|
||||
|
|
@ -1784,9 +1839,18 @@ static int phylink_register_sfp(struct phylink *pl,
|
|||
|
||||
pl->sfp_bus = bus;
|
||||
|
||||
ret = phylink_create_sfp_cage_port(pl);
|
||||
if (ret) {
|
||||
sfp_bus_put(bus);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = sfp_bus_add_upstream(bus, pl, &sfp_phylink_ops);
|
||||
sfp_bus_put(bus);
|
||||
|
||||
if (ret)
|
||||
phylink_destroy_sfp_cage_port(pl);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
@ -2069,6 +2133,7 @@ EXPORT_SYMBOL_GPL(phylink_create);
|
|||
void phylink_destroy(struct phylink *pl)
|
||||
{
|
||||
sfp_bus_del_upstream(pl->sfp_bus);
|
||||
phylink_destroy_sfp_cage_port(pl);
|
||||
if (pl->link_gpio)
|
||||
gpiod_put(pl->link_gpio);
|
||||
|
||||
|
|
@ -3955,14 +4020,70 @@ static void phylink_sfp_module_remove(void *upstream)
|
|||
phy_interface_zero(pl->sfp_interfaces);
|
||||
}
|
||||
|
||||
static int phylink_add_sfp_mod_port(struct phylink *pl)
|
||||
{
|
||||
DECLARE_PHY_INTERFACE_MASK(interfaces);
|
||||
const struct sfp_module_caps *caps;
|
||||
struct phy_port *port;
|
||||
int ret = 0;
|
||||
|
||||
if (!pl->sfp_cage_port)
|
||||
return 0;
|
||||
|
||||
/* Create mod port */
|
||||
port = phy_port_alloc();
|
||||
if (!port)
|
||||
return -ENOMEM;
|
||||
|
||||
port->active = true;
|
||||
|
||||
caps = sfp_get_module_caps(pl->sfp_bus);
|
||||
|
||||
phy_interface_and(interfaces, pl->sfp_cage_port->interfaces,
|
||||
caps->interfaces);
|
||||
phy_caps_linkmode_filter_ifaces(port->supported, caps->link_modes,
|
||||
interfaces);
|
||||
|
||||
if (pl->netdev) {
|
||||
ret = phy_link_topo_add_port(pl->netdev, port);
|
||||
if (ret) {
|
||||
phy_port_destroy(port);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
port->upstream_port = pl->sfp_cage_port;
|
||||
|
||||
pl->mod_port = port;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void phylink_del_sfp_mod_port(struct phylink *pl)
|
||||
{
|
||||
if (!pl->mod_port)
|
||||
return;
|
||||
|
||||
if (pl->netdev)
|
||||
phy_link_topo_del_port(pl->netdev, pl->mod_port);
|
||||
|
||||
phy_port_destroy(pl->mod_port);
|
||||
pl->mod_port = NULL;
|
||||
}
|
||||
|
||||
static int phylink_sfp_module_start(void *upstream)
|
||||
{
|
||||
struct phylink *pl = upstream;
|
||||
int ret;
|
||||
|
||||
/* If this SFP module has a PHY, start the PHY now. */
|
||||
if (pl->phydev) {
|
||||
phy_start(pl->phydev);
|
||||
return 0;
|
||||
} else {
|
||||
ret = phylink_add_sfp_mod_port(pl);
|
||||
if (ret)
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* If the module may have a PHY but we didn't detect one we
|
||||
|
|
@ -3971,7 +4092,16 @@ static int phylink_sfp_module_start(void *upstream)
|
|||
if (!pl->sfp_may_have_phy)
|
||||
return 0;
|
||||
|
||||
return phylink_sfp_config_optical(pl);
|
||||
ret = phylink_sfp_config_optical(pl);
|
||||
if (ret)
|
||||
goto del_mod_port;
|
||||
|
||||
return 0;
|
||||
|
||||
del_mod_port:
|
||||
phylink_del_sfp_mod_port(pl);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void phylink_sfp_module_stop(void *upstream)
|
||||
|
|
@ -3981,6 +4111,8 @@ static void phylink_sfp_module_stop(void *upstream)
|
|||
/* If this SFP module has a PHY, stop it. */
|
||||
if (pl->phydev)
|
||||
phy_stop(pl->phydev);
|
||||
else
|
||||
phylink_del_sfp_mod_port(pl);
|
||||
}
|
||||
|
||||
static void phylink_sfp_link_down(void *upstream)
|
||||
|
|
@ -4025,6 +4157,8 @@ static int phylink_sfp_connect_phy(void *upstream, struct phy_device *phy)
|
|||
phy_interface_and(phy->host_interfaces, phylink_sfp_interfaces,
|
||||
pl->config->supported_interfaces);
|
||||
|
||||
phy_set_upstream_port(phy, pl->sfp_cage_port);
|
||||
|
||||
/* Do the initial configuration */
|
||||
return phylink_sfp_config_phy(pl, phy);
|
||||
}
|
||||
|
|
@ -4033,6 +4167,7 @@ static void phylink_sfp_disconnect_phy(void *upstream,
|
|||
struct phy_device *phydev)
|
||||
{
|
||||
phylink_disconnect_phy(upstream);
|
||||
phy_set_upstream_port(phydev, NULL);
|
||||
}
|
||||
|
||||
static const struct sfp_upstream_ops sfp_phylink_ops = {
|
||||
|
|
|
|||
|
|
@ -612,7 +612,11 @@ struct phy_oatc14_sqi_capability {
|
|||
* @irq: IRQ number of the PHY's interrupt (-1 if none)
|
||||
* @phylink: Pointer to phylink instance for this PHY
|
||||
* @sfp_bus_attached: Flag indicating whether the SFP bus has been attached
|
||||
* @has_sfp_mod_phy: Set true if downstream SFP bus's module contains a PHY
|
||||
* @sfp_bus: SFP bus attached to this PHY's fiber port
|
||||
* @sfp_cage_port: The phy_port connected to the downstream SFP cage
|
||||
* @mod_port: phy_port representing the SFP module, if it is phy-less
|
||||
* @upstream_port: phy_port this PHY's MII attaches to, if any
|
||||
* @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.
|
||||
|
|
@ -804,7 +808,11 @@ struct phy_device {
|
|||
|
||||
/* This may be modified under the rtnl lock */
|
||||
bool sfp_bus_attached;
|
||||
bool has_sfp_mod_phy;
|
||||
struct sfp_bus *sfp_bus;
|
||||
struct phy_port *sfp_cage_port;
|
||||
struct phy_port *mod_port;
|
||||
struct phy_port *upstream_port;
|
||||
struct phylink *phylink;
|
||||
struct net_device *attached_dev;
|
||||
struct mii_timestamper *mii_ts;
|
||||
|
|
@ -2489,6 +2497,8 @@ int __phy_hwtstamp_set(struct phy_device *phydev,
|
|||
|
||||
struct phy_port *phy_get_sfp_port(struct phy_device *phydev);
|
||||
|
||||
void phy_set_upstream_port(struct phy_device *phydev, struct phy_port *port);
|
||||
|
||||
/**
|
||||
* phy_module_driver() - Helper macro for registering PHY drivers
|
||||
* @__phy_drivers: array of PHY drivers to register
|
||||
|
|
|
|||
|
|
@ -13,14 +13,19 @@
|
|||
|
||||
#include <linux/ethtool.h>
|
||||
#include <linux/netdevice.h>
|
||||
#include <linux/rtnetlink.h>
|
||||
|
||||
struct xarray;
|
||||
struct phy_device;
|
||||
struct phy_port;
|
||||
struct sfp_bus;
|
||||
|
||||
struct phy_link_topology {
|
||||
struct xarray phys;
|
||||
u32 next_phy_index;
|
||||
|
||||
struct xarray ports;
|
||||
u32 next_port_index;
|
||||
};
|
||||
|
||||
struct phy_device_node {
|
||||
|
|
@ -48,6 +53,9 @@ int phy_link_topo_add_phy(struct net_device *dev,
|
|||
|
||||
void phy_link_topo_del_phy(struct net_device *dev, struct phy_device *phy);
|
||||
|
||||
int phy_link_topo_add_port(struct net_device *dev, struct phy_port *port);
|
||||
void phy_link_topo_del_port(struct net_device *dev, struct phy_port *port);
|
||||
|
||||
static inline struct phy_device *
|
||||
phy_link_topo_get_phy(struct net_device *dev, u32 phyindex)
|
||||
{
|
||||
|
|
@ -64,6 +72,24 @@ phy_link_topo_get_phy(struct net_device *dev, u32 phyindex)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
static inline struct phy_port *
|
||||
phy_link_topo_get_port(struct net_device *dev, u32 port_id)
|
||||
{
|
||||
struct phy_link_topology *topo = dev->link_topo;
|
||||
|
||||
if (!topo)
|
||||
return NULL;
|
||||
|
||||
/* ports in the topo are RTNL protected, but may be accessed under
|
||||
* netdev_lock for ops-locked devices. For now keep them under rtnl
|
||||
* protection, as no ops-locked devices have phy_port capabilities
|
||||
* yet.
|
||||
*/
|
||||
ASSERT_RTNL();
|
||||
|
||||
return xa_load(&topo->ports, port_id);
|
||||
}
|
||||
|
||||
#else
|
||||
static inline int phy_link_topo_add_phy(struct net_device *dev,
|
||||
struct phy_device *phy,
|
||||
|
|
@ -77,11 +103,28 @@ static inline void phy_link_topo_del_phy(struct net_device *dev,
|
|||
{
|
||||
}
|
||||
|
||||
static inline int phy_link_topo_add_port(struct net_device *dev,
|
||||
struct phy_port *port)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void phy_link_topo_del_port(struct net_device *dev,
|
||||
struct phy_port *port)
|
||||
{
|
||||
}
|
||||
|
||||
static inline struct phy_device *
|
||||
phy_link_topo_get_phy(struct net_device *dev, u32 phyindex)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static inline struct phy_port *
|
||||
phy_link_topo_get_port(struct net_device *dev, u32 port_id)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __PHY_LINK_TOPOLOGY_H */
|
||||
|
|
|
|||
|
|
@ -36,9 +36,12 @@ struct phy_port_ops {
|
|||
/**
|
||||
* struct phy_port - A representation of a network device physical interface
|
||||
*
|
||||
* @id: Unique identifier for the port within the topology
|
||||
* @head: Used by the port's parent to list ports
|
||||
* @parent_type: The type of device this port is directly connected to
|
||||
* @phy: If the parent is PHY_PORT_PHYDEV, the PHY controlling that port
|
||||
* @upstream_port: Indicates the MII port that feeds this port, if any,
|
||||
* e.g. the SFP cage port for a SFP module port.
|
||||
* @ops: Callback ops implemented by the port controller
|
||||
* @pairs: The number of pairs this port has, 0 if not applicable
|
||||
* @mediums: Bitmask of the physical mediums this port provides access to
|
||||
|
|
@ -52,11 +55,13 @@ struct phy_port_ops {
|
|||
* @is_sfp: Indicates if this port drives an SFP cage.
|
||||
*/
|
||||
struct phy_port {
|
||||
u32 id;
|
||||
struct list_head head;
|
||||
enum phy_port_parent parent_type;
|
||||
union {
|
||||
struct phy_device *phy;
|
||||
};
|
||||
struct phy_port *upstream_port;
|
||||
|
||||
const struct phy_port_ops *ops;
|
||||
|
||||
|
|
|
|||
|
|
@ -685,6 +685,7 @@ enum ethtool_link_ext_substate_module {
|
|||
* @ETH_SS_STATS_RMON: names of RMON statistics
|
||||
* @ETH_SS_STATS_PHY: names of PHY(dev) statistics
|
||||
* @ETH_SS_TS_FLAGS: hardware timestamping flags
|
||||
* @ETH_SS_PHY_INTERFACES: names of MAC to PHY interfaces
|
||||
*
|
||||
* @ETH_SS_COUNT: number of defined string sets
|
||||
*/
|
||||
|
|
@ -712,6 +713,7 @@ enum ethtool_stringset {
|
|||
ETH_SS_STATS_RMON,
|
||||
ETH_SS_STATS_PHY,
|
||||
ETH_SS_TS_FLAGS,
|
||||
ETH_SS_PHY_INTERFACES,
|
||||
|
||||
/* add new constants above here */
|
||||
ETH_SS_COUNT
|
||||
|
|
|
|||
|
|
@ -78,6 +78,14 @@ enum ethtool_pse_event {
|
|||
ETHTOOL_PSE_EVENT_SW_PW_CONTROL_ERROR = 64,
|
||||
};
|
||||
|
||||
/*
|
||||
* type of physical connector
|
||||
*/
|
||||
enum ethtool_port_type {
|
||||
ETHTOOL_PORT_TYPE_MDI,
|
||||
ETHTOOL_PORT_TYPE_SFP,
|
||||
};
|
||||
|
||||
enum {
|
||||
ETHTOOL_A_HEADER_UNSPEC,
|
||||
ETHTOOL_A_HEADER_DEV_INDEX,
|
||||
|
|
@ -840,6 +848,18 @@ enum {
|
|||
ETHTOOL_A_MSE_MAX = (__ETHTOOL_A_MSE_CNT - 1)
|
||||
};
|
||||
|
||||
enum {
|
||||
ETHTOOL_A_PORT_HEADER = 1,
|
||||
ETHTOOL_A_PORT_ID,
|
||||
ETHTOOL_A_PORT_SUPPORTED_MODES,
|
||||
ETHTOOL_A_PORT_SUPPORTED_INTERFACES,
|
||||
ETHTOOL_A_PORT_TYPE,
|
||||
ETHTOOL_A_PORT_UPSTREAM_PORT,
|
||||
|
||||
__ETHTOOL_A_PORT_CNT,
|
||||
ETHTOOL_A_PORT_MAX = (__ETHTOOL_A_PORT_CNT - 1)
|
||||
};
|
||||
|
||||
enum {
|
||||
ETHTOOL_MSG_USER_NONE = 0,
|
||||
ETHTOOL_MSG_STRSET_GET = 1,
|
||||
|
|
@ -893,6 +913,7 @@ enum {
|
|||
ETHTOOL_MSG_RSS_CREATE_ACT,
|
||||
ETHTOOL_MSG_RSS_DELETE_ACT,
|
||||
ETHTOOL_MSG_MSE_GET,
|
||||
ETHTOOL_MSG_PORT_GET,
|
||||
|
||||
__ETHTOOL_MSG_USER_CNT,
|
||||
ETHTOOL_MSG_USER_MAX = (__ETHTOOL_MSG_USER_CNT - 1)
|
||||
|
|
@ -954,6 +975,7 @@ enum {
|
|||
ETHTOOL_MSG_RSS_CREATE_NTF,
|
||||
ETHTOOL_MSG_RSS_DELETE_NTF,
|
||||
ETHTOOL_MSG_MSE_GET_REPLY,
|
||||
ETHTOOL_MSG_PORT_GET_REPLY,
|
||||
|
||||
__ETHTOOL_MSG_KERNEL_CNT,
|
||||
ETHTOOL_MSG_KERNEL_MAX = (__ETHTOOL_MSG_KERNEL_CNT - 1)
|
||||
|
|
|
|||
|
|
@ -11521,6 +11521,7 @@ static void netdev_free_phy_link_topology(struct net_device *dev)
|
|||
|
||||
if (IS_ENABLED(CONFIG_PHYLIB) && topo) {
|
||||
xa_destroy(&topo->phys);
|
||||
xa_destroy(&topo->ports);
|
||||
kfree(topo);
|
||||
dev->link_topo = NULL;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ ethtool_nl-y := netlink.o bitset.o strset.o linkinfo.o linkmodes.o rss.o \
|
|||
channels.o coalesce.o pause.o eee.o tsinfo.o cabletest.o \
|
||||
tunnels.o fec.o eeprom.o stats.o phc_vclocks.o mm.o \
|
||||
module.o cmis_fw_update.o cmis_cdb.o pse-pd.o plca.o \
|
||||
phy.o tsconfig.o mse.o
|
||||
phy.o tsconfig.o mse.o port.o
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ static u32 ethnl_bcast_seq;
|
|||
ETHTOOL_FLAG_OMIT_REPLY)
|
||||
#define ETHTOOL_FLAGS_STATS (ETHTOOL_FLAGS_BASIC | ETHTOOL_FLAG_STATS)
|
||||
|
||||
char phy_interface_names[PHY_INTERFACE_MODE_MAX][ETH_GSTRING_LEN] __ro_after_init;
|
||||
|
||||
const struct nla_policy ethnl_header_policy[] = {
|
||||
[ETHTOOL_A_HEADER_DEV_INDEX] = { .type = NLA_U32 },
|
||||
[ETHTOOL_A_HEADER_DEV_NAME] = { .type = NLA_NUL_STRING,
|
||||
|
|
@ -433,6 +435,7 @@ ethnl_default_requests[__ETHTOOL_MSG_USER_CNT] = {
|
|||
[ETHTOOL_MSG_TSCONFIG_SET] = ðnl_tsconfig_request_ops,
|
||||
[ETHTOOL_MSG_PHY_GET] = ðnl_phy_request_ops,
|
||||
[ETHTOOL_MSG_MSE_GET] = ðnl_mse_request_ops,
|
||||
[ETHTOOL_MSG_PORT_GET] = ðnl_port_request_ops,
|
||||
};
|
||||
|
||||
static struct ethnl_dump_ctx *ethnl_dump_context(struct netlink_callback *cb)
|
||||
|
|
@ -1571,6 +1574,15 @@ static const struct genl_ops ethtool_genl_ops[] = {
|
|||
.policy = ethnl_mse_get_policy,
|
||||
.maxattr = ARRAY_SIZE(ethnl_mse_get_policy) - 1,
|
||||
},
|
||||
{
|
||||
.cmd = ETHTOOL_MSG_PORT_GET,
|
||||
.doit = ethnl_default_doit,
|
||||
.start = ethnl_port_dump_start,
|
||||
.dumpit = ethnl_port_dumpit,
|
||||
.done = ethnl_port_dump_done,
|
||||
.policy = ethnl_port_get_policy,
|
||||
.maxattr = ARRAY_SIZE(ethnl_port_get_policy) - 1,
|
||||
},
|
||||
};
|
||||
|
||||
static const struct genl_multicast_group ethtool_nl_mcgrps[] = {
|
||||
|
|
@ -1593,10 +1605,23 @@ static struct genl_family ethtool_genl_family __ro_after_init = {
|
|||
|
||||
/* module setup */
|
||||
|
||||
static void __init ethnl_phy_names_populate(void)
|
||||
{
|
||||
const char *name;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < PHY_INTERFACE_MODE_MAX; i++) {
|
||||
name = phy_modes(i);
|
||||
strscpy(phy_interface_names[i], name, ETH_GSTRING_LEN);
|
||||
}
|
||||
}
|
||||
|
||||
static int __init ethnl_init(void)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ethnl_phy_names_populate();
|
||||
|
||||
ret = genl_register_family(ðtool_genl_family);
|
||||
if (WARN(ret < 0, "ethtool: genetlink family registration failed"))
|
||||
return ret;
|
||||
|
|
|
|||
|
|
@ -5,11 +5,14 @@
|
|||
|
||||
#include <linux/ethtool_netlink.h>
|
||||
#include <linux/netdevice.h>
|
||||
#include <linux/phy.h>
|
||||
#include <net/genetlink.h>
|
||||
#include <net/sock.h>
|
||||
|
||||
struct ethnl_req_info;
|
||||
|
||||
extern char phy_interface_names[PHY_INTERFACE_MODE_MAX][ETH_GSTRING_LEN];
|
||||
|
||||
int ethnl_parse_header_dev_get(struct ethnl_req_info *req_info,
|
||||
const struct nlattr *nest, struct net *net,
|
||||
struct netlink_ext_ack *extack,
|
||||
|
|
@ -474,6 +477,7 @@ extern const struct ethnl_request_ops ethnl_mm_request_ops;
|
|||
extern const struct ethnl_request_ops ethnl_phy_request_ops;
|
||||
extern const struct ethnl_request_ops ethnl_tsconfig_request_ops;
|
||||
extern const struct ethnl_request_ops ethnl_mse_request_ops;
|
||||
extern const struct ethnl_request_ops ethnl_port_request_ops;
|
||||
|
||||
extern const struct nla_policy ethnl_header_policy[ETHTOOL_A_HEADER_FLAGS + 1];
|
||||
extern const struct nla_policy ethnl_header_policy_stats[ETHTOOL_A_HEADER_FLAGS + 1];
|
||||
|
|
@ -530,6 +534,7 @@ extern const struct nla_policy ethnl_phy_get_policy[ETHTOOL_A_PHY_HEADER + 1];
|
|||
extern const struct nla_policy ethnl_tsconfig_get_policy[ETHTOOL_A_TSCONFIG_HEADER + 1];
|
||||
extern const struct nla_policy ethnl_tsconfig_set_policy[ETHTOOL_A_TSCONFIG_MAX + 1];
|
||||
extern const struct nla_policy ethnl_mse_get_policy[ETHTOOL_A_MSE_HEADER + 1];
|
||||
extern const struct nla_policy ethnl_port_get_policy[ETHTOOL_A_PORT_ID + 1];
|
||||
|
||||
int ethnl_set_features(struct sk_buff *skb, struct genl_info *info);
|
||||
int ethnl_act_cable_test(struct sk_buff *skb, struct genl_info *info);
|
||||
|
|
@ -545,6 +550,9 @@ int ethnl_tsinfo_dumpit(struct sk_buff *skb, struct netlink_callback *cb);
|
|||
int ethnl_tsinfo_done(struct netlink_callback *cb);
|
||||
int ethnl_rss_create_doit(struct sk_buff *skb, struct genl_info *info);
|
||||
int ethnl_rss_delete_doit(struct sk_buff *skb, struct genl_info *info);
|
||||
int ethnl_port_dump_start(struct netlink_callback *cb);
|
||||
int ethnl_port_dumpit(struct sk_buff *skb, struct netlink_callback *cb);
|
||||
int ethnl_port_dump_done(struct netlink_callback *cb);
|
||||
|
||||
extern const char stats_std_names[__ETHTOOL_STATS_CNT][ETH_GSTRING_LEN];
|
||||
extern const char stats_eth_phy_names[__ETHTOOL_A_STATS_ETH_PHY_CNT][ETH_GSTRING_LEN];
|
||||
|
|
|
|||
385
net/ethtool/port.c
Normal file
385
net/ethtool/port.c
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Copyright 2026 Bootlin
|
||||
*/
|
||||
#include <linux/phy.h>
|
||||
#include <linux/phy_link_topology.h>
|
||||
#include <linux/phy_port.h>
|
||||
#include <net/netdev_lock.h>
|
||||
|
||||
#include "bitset.h"
|
||||
#include "common.h"
|
||||
#include "netlink.h"
|
||||
|
||||
struct port_req_info {
|
||||
struct ethnl_req_info base;
|
||||
u32 port_id;
|
||||
};
|
||||
|
||||
struct port_reply_data {
|
||||
struct ethnl_reply_data base;
|
||||
__ETHTOOL_DECLARE_LINK_MODE_MASK(supported);
|
||||
DECLARE_PHY_INTERFACE_MASK(interfaces);
|
||||
u32 port_id;
|
||||
bool mii;
|
||||
bool sfp;
|
||||
u32 upstream_port;
|
||||
};
|
||||
|
||||
#define PORT_REQINFO(__req_base) \
|
||||
container_of(__req_base, struct port_req_info, base)
|
||||
|
||||
#define PORT_REPDATA(__reply_base) \
|
||||
container_of(__reply_base, struct port_reply_data, base)
|
||||
|
||||
const struct nla_policy ethnl_port_get_policy[ETHTOOL_A_PORT_ID + 1] = {
|
||||
[ETHTOOL_A_PORT_HEADER] = NLA_POLICY_NESTED(ethnl_header_policy),
|
||||
[ETHTOOL_A_PORT_ID] = NLA_POLICY_MIN(NLA_U32, 1),
|
||||
};
|
||||
|
||||
static int port_parse_request(struct ethnl_req_info *req_info,
|
||||
const struct genl_info *info,
|
||||
struct nlattr **tb,
|
||||
struct netlink_ext_ack *extack)
|
||||
{
|
||||
struct port_req_info *request = PORT_REQINFO(req_info);
|
||||
|
||||
if (GENL_REQ_ATTR_CHECK(info, ETHTOOL_A_PORT_ID))
|
||||
return -EINVAL;
|
||||
|
||||
request->port_id = nla_get_u32(tb[ETHTOOL_A_PORT_ID]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int port_prepare_data(const struct ethnl_req_info *req_info,
|
||||
struct ethnl_reply_data *reply_data,
|
||||
const struct genl_info *info)
|
||||
{
|
||||
struct port_reply_data *reply = PORT_REPDATA(reply_data);
|
||||
struct port_req_info *request = PORT_REQINFO(req_info);
|
||||
struct phy_port *port;
|
||||
|
||||
/* RTNL must be held while holding a ref to the phy_port. Here, caller
|
||||
* holds RTNL.
|
||||
*/
|
||||
port = phy_link_topo_get_port(req_info->dev, request->port_id);
|
||||
if (!port)
|
||||
return -ENODEV;
|
||||
|
||||
linkmode_copy(reply->supported, port->supported);
|
||||
phy_interface_copy(reply->interfaces, port->interfaces);
|
||||
reply->port_id = port->id;
|
||||
reply->mii = port->is_mii;
|
||||
reply->sfp = port->is_sfp;
|
||||
reply->upstream_port = port->upstream_port ? port->upstream_port->id : 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int port_reply_size(const struct ethnl_req_info *req_info,
|
||||
const struct ethnl_reply_data *reply_data)
|
||||
{
|
||||
bool compact = req_info->flags & ETHTOOL_FLAG_COMPACT_BITSETS;
|
||||
struct port_reply_data *reply = PORT_REPDATA(reply_data);
|
||||
size_t size = 0;
|
||||
int ret;
|
||||
|
||||
/* ETHTOOL_A_PORT_ID */
|
||||
size += nla_total_size(sizeof(u32));
|
||||
|
||||
if (!reply->mii) {
|
||||
/* ETHTOOL_A_PORT_SUPPORTED_MODES */
|
||||
ret = ethnl_bitset_size(reply->supported, NULL,
|
||||
__ETHTOOL_LINK_MODE_MASK_NBITS,
|
||||
link_mode_names, compact);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
size += ret;
|
||||
} else {
|
||||
/* ETHTOOL_A_PORT_SUPPORTED_INTERFACES */
|
||||
ret = ethnl_bitset_size(reply->interfaces, NULL,
|
||||
PHY_INTERFACE_MODE_MAX,
|
||||
phy_interface_names, compact);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
size += ret;
|
||||
}
|
||||
|
||||
/* ETHTOOL_A_PORT_TYPE */
|
||||
size += nla_total_size(sizeof(u32));
|
||||
|
||||
/* ETHTOOL_A_PORT_UPSTREAM_PORT */
|
||||
if (reply->upstream_port)
|
||||
size += nla_total_size(sizeof(u32));
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
static int port_fill_reply(struct sk_buff *skb,
|
||||
const struct ethnl_req_info *req_info,
|
||||
const struct ethnl_reply_data *reply_data)
|
||||
{
|
||||
bool compact = req_info->flags & ETHTOOL_FLAG_COMPACT_BITSETS;
|
||||
struct port_reply_data *reply = PORT_REPDATA(reply_data);
|
||||
int ret, port_type;
|
||||
|
||||
if (nla_put_u32(skb, ETHTOOL_A_PORT_ID, reply->port_id))
|
||||
return -EMSGSIZE;
|
||||
|
||||
if (!reply->mii) {
|
||||
ret = ethnl_put_bitset(skb, ETHTOOL_A_PORT_SUPPORTED_MODES,
|
||||
reply->supported, NULL,
|
||||
__ETHTOOL_LINK_MODE_MASK_NBITS,
|
||||
link_mode_names, compact);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
} else {
|
||||
ret = ethnl_put_bitset(skb, ETHTOOL_A_PORT_SUPPORTED_INTERFACES,
|
||||
reply->interfaces, NULL,
|
||||
PHY_INTERFACE_MODE_MAX,
|
||||
phy_interface_names, compact);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
}
|
||||
|
||||
port_type = ETHTOOL_PORT_TYPE_MDI;
|
||||
if (reply->mii || reply->sfp)
|
||||
port_type = ETHTOOL_PORT_TYPE_SFP;
|
||||
|
||||
if (nla_put_u32(skb, ETHTOOL_A_PORT_TYPE, port_type))
|
||||
return -EMSGSIZE;
|
||||
|
||||
if (reply->upstream_port &&
|
||||
nla_put_u32(skb, ETHTOOL_A_PORT_UPSTREAM_PORT,
|
||||
reply->upstream_port))
|
||||
return -EMSGSIZE;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct port_dump_ctx {
|
||||
struct port_req_info *req_info;
|
||||
struct port_reply_data *reply_data;
|
||||
unsigned long ifindex;
|
||||
unsigned long pos_ifindex;
|
||||
unsigned long pos_portid;
|
||||
};
|
||||
|
||||
static struct port_dump_ctx *
|
||||
port_dump_ctx_get(struct netlink_callback *cb)
|
||||
{
|
||||
return (struct port_dump_ctx *)cb->ctx;
|
||||
}
|
||||
|
||||
int ethnl_port_dump_start(struct netlink_callback *cb)
|
||||
{
|
||||
const struct genl_dumpit_info *info = genl_dumpit_info(cb);
|
||||
struct port_dump_ctx *ctx = port_dump_ctx_get(cb);
|
||||
struct nlattr **tb = info->info.attrs;
|
||||
struct port_reply_data *reply_data;
|
||||
struct port_req_info *req_info;
|
||||
int ret;
|
||||
|
||||
BUILD_BUG_ON(sizeof(*ctx) > sizeof(cb->ctx));
|
||||
|
||||
if (tb[ETHTOOL_A_PORT_ID]) {
|
||||
NL_SET_BAD_ATTR(info->info.extack, tb[ETHTOOL_A_PORT_ID]);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
req_info = kzalloc_obj(*req_info);
|
||||
if (!req_info)
|
||||
return -ENOMEM;
|
||||
|
||||
reply_data = kmalloc_obj(*reply_data);
|
||||
if (!reply_data) {
|
||||
ret = -ENOMEM;
|
||||
goto free_req_info;
|
||||
}
|
||||
|
||||
ret = ethnl_parse_header_dev_get(&req_info->base, tb[ETHTOOL_A_PORT_HEADER],
|
||||
genl_info_net(&info->info),
|
||||
info->info.extack, false);
|
||||
if (ret < 0)
|
||||
goto free_rep_data;
|
||||
|
||||
ctx->ifindex = 0;
|
||||
ctx->pos_ifindex = 0;
|
||||
|
||||
/* For filtered DUMP requests, let's just store the ifindex. We'll check
|
||||
* again if the netdev is still there when looping over the netdev list
|
||||
* in the DUMP loop.
|
||||
*/
|
||||
if (req_info->base.dev) {
|
||||
ctx->ifindex = req_info->base.dev->ifindex;
|
||||
netdev_put(req_info->base.dev, &req_info->base.dev_tracker);
|
||||
req_info->base.dev = NULL;
|
||||
}
|
||||
|
||||
ctx->req_info = req_info;
|
||||
ctx->reply_data = reply_data;
|
||||
|
||||
return 0;
|
||||
|
||||
free_rep_data:
|
||||
kfree(reply_data);
|
||||
free_req_info:
|
||||
kfree(req_info);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int port_dump_one(struct sk_buff *skb, struct net_device *dev,
|
||||
struct netlink_callback *cb)
|
||||
{
|
||||
struct port_dump_ctx *ctx = port_dump_ctx_get(cb);
|
||||
void *ehdr;
|
||||
int ret;
|
||||
|
||||
ehdr = ethnl_dump_put(skb, cb, ETHTOOL_MSG_PORT_GET_REPLY);
|
||||
if (!ehdr)
|
||||
return -EMSGSIZE;
|
||||
|
||||
memset(ctx->reply_data, 0, sizeof(struct port_reply_data));
|
||||
ctx->reply_data->base.dev = dev;
|
||||
|
||||
ret = port_prepare_data(&ctx->req_info->base, &ctx->reply_data->base,
|
||||
genl_info_dump(cb));
|
||||
if (ret < 0)
|
||||
goto out;
|
||||
|
||||
ret = ethnl_fill_reply_header(skb, dev, ETHTOOL_A_PORT_HEADER);
|
||||
if (ret < 0)
|
||||
goto out;
|
||||
|
||||
ret = port_fill_reply(skb, &ctx->req_info->base, &ctx->reply_data->base);
|
||||
|
||||
out:
|
||||
ctx->reply_data->base.dev = NULL;
|
||||
if (ret < 0)
|
||||
genlmsg_cancel(skb, ehdr);
|
||||
else
|
||||
genlmsg_end(skb, ehdr);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int port_dump_one_dev(struct sk_buff *skb, struct netlink_callback *cb)
|
||||
{
|
||||
struct port_dump_ctx *ctx = port_dump_ctx_get(cb);
|
||||
struct net_device *dev;
|
||||
struct phy_port *port;
|
||||
int ret = 0;
|
||||
|
||||
dev = ctx->req_info->base.dev;
|
||||
|
||||
rtnl_lock();
|
||||
netdev_lock_ops(dev);
|
||||
|
||||
if (!dev->link_topo)
|
||||
goto out;
|
||||
|
||||
xa_for_each_start(&dev->link_topo->ports, ctx->pos_portid, port,
|
||||
ctx->pos_portid) {
|
||||
ctx->req_info->port_id = ctx->pos_portid;
|
||||
|
||||
ret = port_dump_one(skb, dev, cb);
|
||||
if (ret == -ENODEV)
|
||||
continue;
|
||||
if (ret)
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = 0;
|
||||
|
||||
out:
|
||||
netdev_unlock_ops(dev);
|
||||
rtnl_unlock();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int port_dump_all_dev(struct sk_buff *skb, struct netlink_callback *cb)
|
||||
{
|
||||
struct port_dump_ctx *ctx = port_dump_ctx_get(cb);
|
||||
unsigned long start_ifindex = ctx->pos_ifindex;
|
||||
struct net *net = sock_net(skb->sk);
|
||||
netdevice_tracker dev_tracker;
|
||||
struct net_device *dev;
|
||||
int ret = 0;
|
||||
|
||||
rcu_read_lock();
|
||||
for_each_netdev_dump(net, dev, ctx->pos_ifindex) {
|
||||
if (ctx->pos_ifindex != start_ifindex)
|
||||
ctx->pos_portid = 0;
|
||||
|
||||
netdev_hold(dev, &dev_tracker, GFP_ATOMIC);
|
||||
rcu_read_unlock();
|
||||
|
||||
ctx->req_info->base.dev = dev;
|
||||
ret = port_dump_one_dev(skb, cb);
|
||||
|
||||
rcu_read_lock();
|
||||
netdev_put(dev, &dev_tracker);
|
||||
ctx->req_info->base.dev = NULL;
|
||||
|
||||
if (ret)
|
||||
break;
|
||||
}
|
||||
rcu_read_unlock();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ethnl_port_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
|
||||
{
|
||||
const struct genl_dumpit_info *info = genl_dumpit_info(cb);
|
||||
struct port_dump_ctx *ctx = port_dump_ctx_get(cb);
|
||||
int ret = 0;
|
||||
|
||||
if (ctx->ifindex) {
|
||||
netdevice_tracker dev_tracker;
|
||||
struct net_device *dev;
|
||||
|
||||
dev = netdev_get_by_index(genl_info_net(&info->info),
|
||||
ctx->ifindex, &dev_tracker,
|
||||
GFP_KERNEL);
|
||||
if (!dev)
|
||||
return -ENODEV;
|
||||
|
||||
ctx->req_info->base.dev = dev;
|
||||
ret = port_dump_one_dev(skb, cb);
|
||||
|
||||
netdev_put(dev, &dev_tracker);
|
||||
} else {
|
||||
ret = port_dump_all_dev(skb, cb);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ethnl_port_dump_done(struct netlink_callback *cb)
|
||||
{
|
||||
struct port_dump_ctx *ctx = port_dump_ctx_get(cb);
|
||||
|
||||
kfree(ctx->req_info);
|
||||
kfree(ctx->reply_data);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const struct ethnl_request_ops ethnl_port_request_ops = {
|
||||
.request_cmd = ETHTOOL_MSG_PORT_GET,
|
||||
.reply_cmd = ETHTOOL_MSG_PORT_GET_REPLY,
|
||||
.hdr_attr = ETHTOOL_A_PORT_HEADER,
|
||||
.req_info_size = sizeof(struct port_req_info),
|
||||
.reply_data_size = sizeof(struct port_reply_data),
|
||||
|
||||
.parse_request = port_parse_request,
|
||||
.prepare_data = port_prepare_data,
|
||||
.reply_size = port_reply_size,
|
||||
.fill_reply = port_fill_reply,
|
||||
};
|
||||
|
|
@ -116,6 +116,11 @@ static const struct strset_info info_template[] = {
|
|||
.count = __ETHTOOL_A_STATS_PHY_CNT,
|
||||
.strings = stats_phy_names,
|
||||
},
|
||||
[ETH_SS_PHY_INTERFACES] = {
|
||||
.per_dev = false,
|
||||
.count = PHY_INTERFACE_MODE_MAX,
|
||||
.strings = phy_interface_names,
|
||||
},
|
||||
};
|
||||
|
||||
struct strset_req_info {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user