| // SPDX-License-Identifier: GPL-2.0+ |
| /* Framework to drive Ethernet ports |
| * |
| * Copyright (c) 2024 Maxime Chevallier <maxime.chevallier@bootlin.com> |
| */ |
| |
| #include <linux/linkmode.h> |
| #include <linux/of.h> |
| #include <linux/phy_port.h> |
| |
| #include "phy-caps.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_obj(*port); |
| 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 |
| * @port: The port to destroy |
| */ |
| void phy_port_destroy(struct phy_port *port) |
| { |
| kfree(port); |
| } |
| EXPORT_SYMBOL_GPL(phy_port_destroy); |
| |
| /** |
| * phy_of_parse_port() - Create a phy_port from a firmware representation |
| * @dn: device_node representation of the port, following the |
| * ethernet-connector.yaml binding |
| * |
| * 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; |
| const char *med_str; |
| u32 pairs = 0; |
| int ret; |
| |
| ret = fwnode_property_read_string(fwnode, "media", &med_str); |
| if (ret) |
| return ERR_PTR(ret); |
| |
| medium = ethtool_str_to_medium(med_str); |
| if (medium == ETHTOOL_LINK_MEDIUM_NONE) |
| return ERR_PTR(-EINVAL); |
| |
| if (medium == ETHTOOL_LINK_MEDIUM_BASET) { |
| ret = fwnode_property_read_u32(fwnode, "pairs", &pairs); |
| if (ret) |
| return ERR_PTR(ret); |
| |
| switch (pairs) { |
| case 1: /* BaseT1 */ |
| case 2: /* 100BaseTX */ |
| case 4: |
| break; |
| default: |
| pr_err("%u is not a valid number of pairs\n", pairs); |
| return ERR_PTR(-EINVAL); |
| } |
| } |
| |
| if (pairs && medium != ETHTOOL_LINK_MEDIUM_BASET) { |
| pr_err("pairs property is only compatible with BaseT medium\n"); |
| return ERR_PTR(-EINVAL); |
| } |
| |
| port = phy_port_alloc(); |
| if (!port) |
| return ERR_PTR(-ENOMEM); |
| |
| port->pairs = pairs; |
| port->mediums = BIT(medium); |
| |
| 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 pairs 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) = {0}; |
| unsigned long mode; |
| int i; |
| |
| /* If there's no pairs specified, we grab the default number of |
| * pairs as the max of the default pairs for each linkmode |
| */ |
| if (!port->pairs) |
| for_each_set_bit(mode, port->supported, |
| __ETHTOOL_LINK_MODE_MASK_NBITS) |
| port->pairs = max_t(int, port->pairs, |
| ethtool_linkmode_n_pairs(mode)); |
| |
| for_each_set_bit(i, &port->mediums, __ETHTOOL_LINK_MEDIUM_LAST) { |
| __ETHTOOL_DECLARE_LINK_MODE_MASK(med_supported) = {0}; |
| |
| phy_caps_medium_get_supported(med_supported, i, port->pairs); |
| linkmode_or(supported, supported, med_supported); |
| } |
| |
| /* If port->supported is already populated, filter it out with the |
| * medium/pair support. Otherwise, let's just use this medium-based |
| * support as the port's supported list. |
| */ |
| if (linkmode_empty(port->supported)) |
| linkmode_copy(port->supported, supported); |
| else |
| linkmode_and(port->supported, supported, port->supported); |
| |
| /* Serdes ports supported through SFP may not have any medium set, |
| * as they will output PHY_INTERFACE_MODE_XXX modes. In that case, derive |
| * the supported list based on these interfaces |
| */ |
| if (port->is_mii && !port->mediums) { |
| unsigned long interface, link_caps = 0; |
| |
| /* Get each interface's caps */ |
| for_each_set_bit(interface, port->interfaces, |
| PHY_INTERFACE_MODE_MAX) |
| link_caps |= phy_caps_from_interface(interface); |
| |
| phy_caps_linkmodes(link_caps, port->supported); |
| } |
| } |
| EXPORT_SYMBOL_GPL(phy_port_update_supported); |
| |
| /** |
| * phy_port_filter_supported() - Make sure that port->supported match port->mediums |
| * @port: The port to filter |
| * |
| * After updating a port's mediums to a more restricted subset, this helper will |
| * make sure that port->supported only contains linkmodes that are compatible |
| * with port->mediums. |
| */ |
| static void phy_port_filter_supported(struct phy_port *port) |
| { |
| __ETHTOOL_DECLARE_LINK_MODE_MASK(supported) = { 0 }; |
| int i; |
| |
| for_each_set_bit(i, &port->mediums, __ETHTOOL_LINK_MEDIUM_LAST) |
| phy_caps_medium_get_supported(supported, i, port->pairs); |
| |
| linkmode_and(port->supported, port->supported, supported); |
| } |
| |
| /** |
| * phy_port_restrict_mediums - Mask away some of the port's supported mediums |
| * @port: The port to act upon |
| * @mediums: A mask of mediums to support on the port |
| * |
| * This helper allows removing some mediums from a port's list of supported |
| * mediums, which occurs once we have enough information about the port to |
| * know its nature. |
| * |
| * Returns: 0 if the change was donne correctly, a negative value otherwise. |
| */ |
| int phy_port_restrict_mediums(struct phy_port *port, unsigned long mediums) |
| { |
| /* We forbid ending-up with a port with empty mediums */ |
| if (!(port->mediums & mediums)) |
| return -EINVAL; |
| |
| port->mediums &= mediums; |
| |
| phy_port_filter_supported(port); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(phy_port_restrict_mediums); |
| |
| /** |
| * phy_port_get_type() - get the PORT_* attribute for that port. |
| * @port: The port we want the information from |
| * |
| * Returns: A PORT_XXX value. |
| */ |
| int phy_port_get_type(struct phy_port *port) |
| { |
| if (port->mediums & BIT(ETHTOOL_LINK_MEDIUM_BASET)) |
| return PORT_TP; |
| |
| if (phy_port_is_fiber(port)) |
| return PORT_FIBRE; |
| |
| return PORT_OTHER; |
| } |
| EXPORT_SYMBOL_GPL(phy_port_get_type); |