Another benefit of having that is to avoid vendor-specific DT properties (micrel,fiber-mode or ti,fiber-mode). This basic representation is meant to be expanded, by the introduction of port ops, userspace listing of ports, and support for multi-port devices. Signed-off-by: Maxime Chevallier <maxime.chevallier@xxxxxxxxxxx> --- RFC V2: Made lanes optional, and added a helper to get default lanes values drivers/net/phy/Makefile | 2 +- drivers/net/phy/phy_device.c | 167 +++++++++++++++++++++++++++++++++++ drivers/net/phy/phy_port.c | 166 ++++++++++++++++++++++++++++++++++ include/linux/ethtool.h | 15 ++++ include/linux/phy.h | 31 +++++++ include/linux/phy_port.h | 69 +++++++++++++++ 6 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 drivers/net/phy/phy_port.c create mode 100644 include/linux/phy_port.h diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile index c8dac6e92278..de1415a46629 100644 --- a/drivers/net/phy/Makefile +++ b/drivers/net/phy/Makefile @@ -2,7 +2,7 @@ # Makefile for Linux PHY drivers libphy-y := phy.o phy-c45.o phy-core.o phy_device.o \ - linkmode.o phy_link_topology.o + linkmode.o phy_link_topology.o phy_port.o mdio-bus-y += mdio_bus.o mdio_device.o ifdef CONFIG_MDIO_DEVICE diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c index 46713d27412b..f385b8fc70d1 100644 --- a/drivers/net/phy/phy_device.c +++ b/drivers/net/phy/phy_device.c @@ -656,6 +656,13 @@ struct phy_device *phy_device_create(struct mii_bus *bus, int addr, u32 phy_id, dev->state = PHY_DOWN; INIT_LIST_HEAD(&dev->leds); + INIT_LIST_HEAD(&dev->ports); + + /* The driver's probe function must change that to the real number + * of ports possible on the PHY. We assume by default we are dealing + * with a single-port PHY + */ + dev->max_n_ports = 1; mutex_init(&dev->lock); INIT_DELAYED_WORK(&dev->state_queue, phy_state_machine); @@ -3405,6 +3412,159 @@ static int of_phy_leds(struct phy_device *phydev) return 0; } +static int phy_add_port(struct phy_device *phydev, struct phy_port *port) +{ + int ret = 0; + + if (phydev->n_ports == phydev->max_n_ports) + return -EBUSY; + + /* We set all ports as active by default, PHY drivers may deactivate + * them (when unused) + */ + port->active = true; + + if (phydev->drv && phydev->drv->attach_port) + ret = phydev->drv->attach_port(phydev, port); + + if (ret) + return ret; + + /* The PHY driver might have added, removed or set medium/lanes info, + * so update the port supported accordingly. + */ + phy_port_update_supported(port); + + list_add(&port->head, &phydev->ports); + + phydev->n_ports++; + + return 0; +} + +static void phy_del_port(struct phy_device *phydev, struct phy_port *port) +{ + if (!phydev->n_ports) + return; + + list_del(&port->head); + + phydev->n_ports--; +} + +static void phy_cleanup_ports(struct phy_device *phydev) +{ + struct phy_port *tmp, *port; + + list_for_each_entry_safe(port, tmp, &phydev->ports, head) { + phy_del_port(phydev, port); + phy_port_destroy(port); + } +} + +static int phy_default_setup_single_port(struct phy_device *phydev) +{ + struct phy_port *port = phy_port_alloc(); + + if (!port) + return -ENOMEM; + + port->parent_type = PHY_PORT_PHY; + port->phy = phydev; + linkmode_copy(port->supported, phydev->supported); + + /* default medium is copper */ + if (!port->mediums) + port->mediums |= BIT(ETHTOOL_LINK_MEDIUM_BASET); + + phy_add_port(phydev, port); + + return 0; +} + +static int of_phy_ports(struct phy_device *phydev) +{ + struct device_node *node = phydev->mdio.dev.of_node; + struct device_node *mdi; + struct phy_port *port; + int err; + + if (!IS_ENABLED(CONFIG_OF_MDIO)) + return 0; + + if (!node) + return 0; + + mdi = of_get_child_by_name(node, "mdi"); + if (!mdi) + return 0; + + for_each_available_child_of_node_scoped(mdi, port_node) { + port = phy_of_parse_port(port_node); + if (IS_ERR(port)) { + err = PTR_ERR(port); + goto out_err; + } + + port->parent_type = PHY_PORT_PHY; + port->phy = phydev; + err = phy_add_port(phydev, port); + if (err) + goto out_err; + } + of_node_put(mdi); + + return 0; + +out_err: + phy_cleanup_ports(phydev); + of_node_put(mdi); + return err; +} + +static int phy_setup_ports(struct phy_device *phydev) +{ + __ETHTOOL_DECLARE_LINK_MODE_MASK(ports_supported); + struct phy_port *port; + int ret; + + ret = of_phy_ports(phydev); + if (ret) + return ret; + + if (phydev->n_ports < phydev->max_n_ports) { + ret = phy_default_setup_single_port(phydev); + if (ret) + goto out; + } + + linkmode_zero(ports_supported); + + /* Aggregate the supported modes, which are made-up of : + * - What the PHY itself supports + * - What the sum of all ports support + */ + list_for_each_entry(port, &phydev->ports, head) + if (port->active) + linkmode_or(ports_supported, ports_supported, + port->supported); + + if (!linkmode_empty(ports_supported)) + linkmode_and(phydev->supported, phydev->supported, + ports_supported); + + /* For now, the phy->port field is set as the first active port's type */ + list_for_each_entry(port, &phydev->ports, head) + if (port->active) + phydev->port = phy_port_get_type(port); + + return 0; + +out: + phy_cleanup_ports(phydev); + return ret; +} + /** * fwnode_mdio_find_device - Given a fwnode, find the mdio_device * @fwnode: pointer to the mdio_device's fwnode @@ -3554,6 +3714,11 @@ static int phy_probe(struct device *dev) phydev->is_gigabit_capable = 1; of_set_phy_supported(phydev); + + err = phy_setup_ports(phydev); + if (err) + goto out; + phy_advertise_supported(phydev); /* Get PHY default EEE advertising modes and handle them as potentially @@ -3630,6 +3795,8 @@ static int phy_remove(struct device *dev) phydev->state = PHY_DOWN; + phy_cleanup_ports(phydev); + sfp_bus_del_upstream(phydev->sfp_bus); phydev->sfp_bus = NULL; diff --git a/drivers/net/phy/phy_port.c b/drivers/net/phy/phy_port.c new file mode 100644 index 000000000000..3a7bdc44b556 --- /dev/null +++ b/drivers/net/phy/phy_port.c @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* Framework to drive Ethernet ports + * + * Copyright (c) 2024 Maxime Chevallier <maxime.chevallier@xxxxxxxxxxx> + */ + +#include <linux/linkmode.h> +#include <linux/of.h> +#include <linux/phy_port.h> + +/** + * phy_port_alloc: Allocate a new phy_port + * + * Returns a newly allocated struct phy_port, or NULL. + */ +struct phy_port *phy_port_alloc(void) +{ + struct phy_port *port; + + port = kzalloc(sizeof(*port), GFP_KERNEL); + if (!port) + return NULL; + + linkmode_zero(port->supported); + INIT_LIST_HEAD(&port->head); + + return port; +} +EXPORT_SYMBOL_GPL(phy_port_alloc); + +/** + * phy_port_destroy: Free a struct phy_port + */ +void phy_port_destroy(struct phy_port *port) +{ + kfree(port); +} +EXPORT_SYMBOL_GPL(phy_port_destroy); + +static void ethtool_medium_get_supported(unsigned long *supported, + enum ethtool_link_medium medium, + int lanes) +{ + int i; + + for (i = 0; i < __ETHTOOL_LINK_MODE_MASK_NBITS; i++) { + /* Special bits such as Autoneg, Pause, Asym_pause, etc. are + * set and will be masked away by the port parent. + */ + if (link_mode_params[i].medium == ETHTOOL_LINK_MEDIUM_NONE) { + linkmode_set_bit(i, supported); + continue; + } + + /* For most cases, min_lanes == lanes, except for 10/100BaseT that work + * on 2 lanes but are compatible with 4 lanes mediums + */ + if (link_mode_params[i].medium == medium && + link_mode_params[i].lanes >= lanes && + link_mode_params[i].min_lanes <= lanes) { + linkmode_set_bit(i, supported); + } + } +} + +static enum ethtool_link_medium ethtool_str_to_medium(const char *str) +{ + int i; + + for (i = 0; i < __ETHTOOL_LINK_MEDIUM_LAST; i++) + if (!strcmp(phy_mediums(i), str)) + return i; + + return ETHTOOL_LINK_MEDIUM_NONE; +} + +/** + * phy_of_parse_port: Create a phy_port from a firmware representation + * + * Returns a newly allocated and initialized phy_port pointer, or an ERR_PTR. + */ +struct phy_port *phy_of_parse_port(struct device_node *dn) +{ + struct fwnode_handle *fwnode = of_fwnode_handle(dn); + enum ethtool_link_medium medium; + struct phy_port *port; + struct property *prop; + const char *med_str; + u32 lanes, mediums = 0; + int ret; + + ret = fwnode_property_read_u32(fwnode, "lanes", &lanes); + if (ret) + lanes = 0; + + ret = fwnode_property_read_string(fwnode, "media", &med_str); + if (ret) + return ERR_PTR(ret); + + of_property_for_each_string(to_of_node(fwnode), "media", prop, med_str) { + medium = ethtool_str_to_medium(med_str); + if (medium == ETHTOOL_LINK_MEDIUM_NONE) + return ERR_PTR(-EINVAL); + + mediums |= BIT(medium); + } + + if (!mediums) + return ERR_PTR(-EINVAL); + + port = phy_port_alloc(); + if (!port) + return ERR_PTR(-ENOMEM); + + port->lanes = lanes; + port->mediums = mediums; + + return port; +} +EXPORT_SYMBOL_GPL(phy_of_parse_port); + +/** + * phy_port_update_supported: Setup the port->supported field + * port: the port to update + * + * Once the port's medium list and number of lanes has been configured based + * on firmware, straps and vendor-specific properties, this function may be + * called to update the port's supported linkmodes list. + * + * Any mode that was manually set in the port's supported list remains set. + */ +void phy_port_update_supported(struct phy_port *port) +{ + __ETHTOOL_DECLARE_LINK_MODE_MASK(supported); + int i, lanes = 1; + + /* If there's no lanes specified, we grab the default number of + * lanes as the max of the default lanes for each medium + */ + if (!port->lanes) + for_each_set_bit(i, &port->mediums, __ETHTOOL_LINK_MEDIUM_LAST) + lanes = max_t(int, lanes, phy_medium_default_lanes(i)); + + for_each_set_bit(i, &port->mediums, __ETHTOOL_LINK_MEDIUM_LAST) { + linkmode_zero(supported); + ethtool_medium_get_supported(supported, i, port->lanes); + linkmode_or(port->supported, port->supported, supported); + } +} +EXPORT_SYMBOL_GPL(phy_port_update_supported); + +/** + * phy_port_get_type: get the PORT_* attribut for that port. + */ +int phy_port_get_type(struct phy_port *port) +{ + if (port->mediums & ETHTOOL_LINK_MEDIUM_BASET) + return PORT_TP; + + if (phy_port_is_fiber(port) || + (port->mediums & BIT(ETHTOOL_LINK_MEDIUM_BASEX))) + return PORT_FIBRE; + + return PORT_OTHER; +} +EXPORT_SYMBOL_GPL(phy_port_get_type); diff --git a/include/linux/ethtool.h b/include/linux/ethtool.h index 519a90ce24d3..32fe062b715c 100644 --- a/include/linux/ethtool.h +++ b/include/linux/ethtool.h @@ -227,6 +227,10 @@ enum ethtool_link_medium { __ETHTOOL_LINK_MEDIUM_LAST, }; +#define ETHTOOL_MEDIUM_FIBER_BITS (BIT(ETHTOOL_LINK_MEDIUM_BASES) | \ + BIT(ETHTOOL_LINK_MEDIUM_BASEL) | \ + BIT(ETHTOOL_LINK_MEDIUM_BASEF)) + static inline const char *phy_mediums(enum ethtool_link_medium medium) { switch (medium) { @@ -258,6 +262,17 @@ static inline const char *phy_mediums(enum ethtool_link_medium medium) } } +static inline int phy_medium_default_lanes(enum ethtool_link_medium medium) +{ + /* Let's consider that the default BaseT ethernet is BaseT4, i.e. + * Gigabit Ethernet. + */ + if (medium == ETHTOOL_LINK_MEDIUM_BASET) + return 4; + + return 1; +} + struct link_mode_info { int speed; u8 min_lanes; diff --git a/include/linux/phy.h b/include/linux/phy.h index 19f076a71f94..5d536b601e3f 100644 --- a/include/linux/phy.h +++ b/include/linux/phy.h @@ -21,6 +21,7 @@ #include <linux/mii.h> #include <linux/mii_timestamper.h> #include <linux/module.h> +#include <linux/phy_port.h> #include <linux/timer.h> #include <linux/workqueue.h> #include <linux/mod_devicetable.h> @@ -642,6 +643,9 @@ struct macsec_ops; * @master_slave_state: Current master/slave configuration * @mii_ts: Pointer to time stamper callbacks * @psec: Pointer to Power Sourcing Equipment control struct + * @ports: List of PHY ports structures + * n_ports: Number of ports currently attached to the PHY + * @max_n_ports: Max number of ports this PHY can expose * @lock: Mutex for serialization access to PHY * @state_queue: Work queue for state machine * @link_down_events: Number of times link was lost @@ -734,6 +738,7 @@ struct phy_device { /* Host supported PHY interface types. Should be ignored if empty. */ DECLARE_PHY_INTERFACE_MASK(host_interfaces); + DECLARE_PHY_INTERFACE_MASK(sfp_bus_interfaces); #ifdef CONFIG_LED_TRIGGER_PHY struct phy_led_trigger *phy_led_triggers; @@ -776,6 +781,10 @@ struct phy_device { struct mii_timestamper *mii_ts; struct pse_control *psec; + struct list_head ports; + int n_ports; + int max_n_ports; + u8 mdix; u8 mdix_ctrl; @@ -1273,6 +1282,27 @@ struct phy_driver { */ int (*led_polarity_set)(struct phy_device *dev, int index, unsigned long modes); + + /** + * @attach_port: Indicates to the PHY driver that a port is detected + * @dev: PHY device to notify + * @port: The port being added + * + * Called when a port that needs to be driven by the PHY is found. The + * number of time this will be called depends on phydev->max_n_ports, + * which the driver can change in .probe(). + * + * The port that is being passed may or may not be initialized. If it is + * already initialized, it is by the generic port representation from + * devicetree, which superseeds any strapping or vendor-specific + * properties. + * + * If the port isn't initialized, the port->mediums and port->lanes + * fields must be set, possibly according to stapping information. + * + * Returns 0, or an error code. + */ + int (*attach_port)(struct phy_device *dev, struct phy_port *port); }; #define to_phy_driver(d) container_of_const(to_mdio_common_driver(d), \ struct phy_driver, mdiodrv) @@ -2083,6 +2113,7 @@ void phy_trigger_machine(struct phy_device *phydev); void phy_mac_interrupt(struct phy_device *phydev); void phy_start_machine(struct phy_device *phydev); void phy_stop_machine(struct phy_device *phydev); + void phy_ethtool_ksettings_get(struct phy_device *phydev, struct ethtool_link_ksettings *cmd); int phy_ethtool_ksettings_set(struct phy_device *phydev, diff --git a/include/linux/phy_port.h b/include/linux/phy_port.h new file mode 100644 index 000000000000..b34c15523adc --- /dev/null +++ b/include/linux/phy_port.h @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <linux/ethtool.h> +#include <linux/types.h> + +#ifndef __PHY_PORT_H +#define __PHY_PORT_H + +struct phy_port; + +/** + * enum phy_port_parent - The device this port is attached to + * + * @PHY_PORT_PHY: Indicates that the port is driven by a PHY device + */ +enum phy_port_parent { + PHY_PORT_PHY, +}; + +/** + * struct phy_port - A representation of a network device physical interface + * + * @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 + * @lanes: The number of lanes (diff pairs) this port has, 0 if not applicable + * @medium: The physical medium this port provides access to + * @supported: The link modes this port can expose + * @active: Indicates if the port is currently part of the active link. + */ +struct phy_port { + struct list_head head; + enum phy_port_parent parent_type; + union { + struct phy_device *phy; + }; + + int lanes; + unsigned long mediums; + __ETHTOOL_DECLARE_LINK_MODE_MASK(supported); + + bool active; +}; + +struct phy_port *phy_port_alloc(void); +void phy_port_destroy(struct phy_port *port); + +static inline struct phy_device *port_phydev(struct phy_port *port) +{ + return port->phy; +} + +struct phy_port *phy_of_parse_port(struct device_node *dn); + +static inline bool phy_port_is_copper(struct phy_port *port) +{ + return port->mediums == BIT(ETHTOOL_LINK_MEDIUM_BASET); +} + +static inline bool phy_port_is_fiber(struct phy_port *port) +{ + return !!(port->mediums & ETHTOOL_MEDIUM_FIBER_BITS); +} + +void phy_port_update_supported(struct phy_port *port); + +int phy_port_get_type(struct phy_port *port); + +#endif -- 2.48.1