diff options
Diffstat (limited to 'drivers/net/phy/phy.c')
| -rw-r--r-- | drivers/net/phy/phy.c | 1111 |
1 files changed, 828 insertions, 283 deletions
diff --git a/drivers/net/phy/phy.c b/drivers/net/phy/phy.c index e5b6cb1a77f9..13dd1691886d 100644 --- a/drivers/net/phy/phy.c +++ b/drivers/net/phy/phy.c @@ -36,6 +36,9 @@ #include <net/genetlink.h> #include <net/sock.h> +#include "phylib-internal.h" +#include "phy-caps.h" + #define PHY_STATE_TIME HZ #define PHY_STATE_STR(_state) \ @@ -52,11 +55,24 @@ static const char *phy_state_to_str(enum phy_state st) PHY_STATE_STR(NOLINK) PHY_STATE_STR(CABLETEST) PHY_STATE_STR(HALTED) + PHY_STATE_STR(ERROR) } return NULL; } +static void phy_process_state_change(struct phy_device *phydev, + enum phy_state old_state) +{ + if (old_state != phydev->state) { + phydev_dbg(phydev, "PHY state change %s -> %s\n", + phy_state_to_str(old_state), + phy_state_to_str(phydev->state)); + if (phydev->drv && phydev->drv->link_change_notify) + phydev->drv->link_change_notify(phydev); + } +} + static void phy_link_up(struct phy_device *phydev) { phydev->phy_link_change(phydev, true); @@ -198,25 +214,6 @@ int phy_aneg_done(struct phy_device *phydev) EXPORT_SYMBOL(phy_aneg_done); /** - * phy_find_valid - find a PHY setting that matches the requested parameters - * @speed: desired speed - * @duplex: desired duplex - * @supported: mask of supported link modes - * - * Locate a supported phy setting that is, in priority order: - * - an exact match for the specified speed and duplex mode - * - a match for the specified speed, or slower speed - * - the slowest supported speed - * Returns the matched phy_setting entry, or %NULL if no supported phy - * settings were found. - */ -static const struct phy_setting * -phy_find_valid(int speed, int duplex, unsigned long *supported) -{ - return phy_lookup_setting(speed, duplex, supported, false); -} - -/** * phy_supported_speeds - return all speeds currently supported by a phy device * @phy: The phy device to return supported speeds of. * @speeds: buffer to store supported speeds in. @@ -230,7 +227,7 @@ unsigned int phy_supported_speeds(struct phy_device *phy, unsigned int *speeds, unsigned int size) { - return phy_speeds(speeds, size, phy->supported); + return phy_caps_speeds(speeds, size, phy->supported); } /** @@ -242,11 +239,11 @@ unsigned int phy_supported_speeds(struct phy_device *phy, * * Description: Returns true if there is a valid setting, false otherwise. */ -static inline bool phy_check_valid(int speed, int duplex, - unsigned long *features) +bool phy_check_valid(int speed, int duplex, unsigned long *features) { - return !!phy_lookup_setting(speed, duplex, features, true); + return phy_caps_valid(speed, duplex, features); } +EXPORT_SYMBOL(phy_check_valid); /** * phy_sanitize_settings - make sure the PHY is set to supported speed and duplex @@ -258,13 +255,14 @@ static inline bool phy_check_valid(int speed, int duplex, */ static void phy_sanitize_settings(struct phy_device *phydev) { - const struct phy_setting *setting; + const struct link_capabilities *c; - setting = phy_find_valid(phydev->speed, phydev->duplex, - phydev->supported); - if (setting) { - phydev->speed = setting->speed; - phydev->duplex = setting->duplex; + c = phy_caps_lookup(phydev->speed, phydev->duplex, phydev->supported, + false); + + if (c) { + phydev->speed = c->speed; + phydev->duplex = c->duplex; } else { /* We failed to find anything (no supported speeds?) */ phydev->speed = SPEED_UNKNOWN; @@ -289,7 +287,7 @@ void phy_ethtool_ksettings_get(struct phy_device *phydev, cmd->base.port = PORT_BNC; else cmd->base.port = phydev->port; - cmd->base.transceiver = phy_is_internal(phydev) ? + cmd->base.transceiver = phydev->is_internal ? XCVR_INTERNAL : XCVR_EXTERNAL; cmd->base.phy_address = phydev->mdio.addr; cmd->base.autoneg = phydev->autoneg; @@ -312,9 +310,13 @@ EXPORT_SYMBOL(phy_ethtool_ksettings_get); int phy_mii_ioctl(struct phy_device *phydev, struct ifreq *ifr, int cmd) { struct mii_ioctl_data *mii_data = if_mii(ifr); + struct kernel_hwtstamp_config kernel_cfg; + struct netlink_ext_ack extack = {}; u16 val = mii_data->val_in; bool change_autoneg = false; + struct hwtstamp_config cfg; int prtad, devad; + int ret; switch (cmd) { case SIOCGMIIPHY: @@ -325,14 +327,19 @@ int phy_mii_ioctl(struct phy_device *phydev, struct ifreq *ifr, int cmd) if (mdio_phy_id_is_c45(mii_data->phy_id)) { prtad = mdio_phy_id_prtad(mii_data->phy_id); devad = mdio_phy_id_devad(mii_data->phy_id); - mii_data->val_out = mdiobus_c45_read( - phydev->mdio.bus, prtad, devad, - mii_data->reg_num); + ret = mdiobus_c45_read(phydev->mdio.bus, prtad, devad, + mii_data->reg_num); + } else { - mii_data->val_out = mdiobus_read( - phydev->mdio.bus, mii_data->phy_id, - mii_data->reg_num); + ret = mdiobus_read(phydev->mdio.bus, mii_data->phy_id, + mii_data->reg_num); } + + if (ret < 0) + return ret; + + mii_data->val_out = ret; + return 0; case SIOCSMIIREG: @@ -398,8 +405,23 @@ int phy_mii_ioctl(struct phy_device *phydev, struct ifreq *ifr, int cmd) return 0; case SIOCSHWTSTAMP: - if (phydev->mii_ts && phydev->mii_ts->hwtstamp) - return phydev->mii_ts->hwtstamp(phydev->mii_ts, ifr); + if (phydev->mii_ts && phydev->mii_ts->hwtstamp_set) { + if (copy_from_user(&cfg, ifr->ifr_data, sizeof(cfg))) + return -EFAULT; + + hwtstamp_config_to_kernel(&kernel_cfg, &cfg); + ret = phydev->mii_ts->hwtstamp_set(phydev->mii_ts, + &kernel_cfg, + &extack); + if (ret) + return ret; + + hwtstamp_config_from_kernel(&cfg, &kernel_cfg); + if (copy_to_user(ifr->ifr_data, &cfg, sizeof(cfg))) + return -EFAULT; + + return 0; + } fallthrough; default: @@ -443,17 +465,58 @@ int phy_do_ioctl_running(struct net_device *dev, struct ifreq *ifr, int cmd) EXPORT_SYMBOL(phy_do_ioctl_running); /** + * __phy_hwtstamp_get - Get hardware timestamping configuration from PHY + * + * @phydev: the PHY device structure + * @config: structure holding the timestamping configuration + * + * Query the PHY device for its current hardware timestamping configuration. + */ +int __phy_hwtstamp_get(struct phy_device *phydev, + struct kernel_hwtstamp_config *config) +{ + if (!phydev) + return -ENODEV; + + if (phydev->mii_ts && phydev->mii_ts->hwtstamp_get) + return phydev->mii_ts->hwtstamp_get(phydev->mii_ts, config); + + return -EOPNOTSUPP; +} + +/** + * __phy_hwtstamp_set - Modify PHY hardware timestamping configuration + * + * @phydev: the PHY device structure + * @config: structure holding the timestamping configuration + * @extack: netlink extended ack structure, for error reporting + */ +int __phy_hwtstamp_set(struct phy_device *phydev, + struct kernel_hwtstamp_config *config, + struct netlink_ext_ack *extack) +{ + if (!phydev) + return -ENODEV; + + if (phydev->mii_ts && phydev->mii_ts->hwtstamp_set) + return phydev->mii_ts->hwtstamp_set(phydev->mii_ts, config, + extack); + + return -EOPNOTSUPP; +} + +/** * phy_queue_state_machine - Trigger the state machine to run soon * * @phydev: the phy_device struct * @jiffies: Run the state machine after these jiffies */ -void phy_queue_state_machine(struct phy_device *phydev, unsigned long jiffies) +static void phy_queue_state_machine(struct phy_device *phydev, + unsigned long jiffies) { mod_delayed_work(system_power_efficient_wq, &phydev->state_queue, jiffies); } -EXPORT_SYMBOL(phy_queue_state_machine); /** * phy_trigger_machine - Trigger the state machine to run now @@ -544,6 +607,241 @@ int phy_ethtool_get_stats(struct phy_device *phydev, EXPORT_SYMBOL(phy_ethtool_get_stats); /** + * __phy_ethtool_get_phy_stats - Retrieve standardized PHY statistics + * @phydev: Pointer to the PHY device + * @phy_stats: Pointer to ethtool_eth_phy_stats structure + * @phydev_stats: Pointer to ethtool_phy_stats structure + * + * Fetches PHY statistics using a kernel-defined interface for consistent + * diagnostics. Unlike phy_ethtool_get_stats(), which allows custom stats, + * this function enforces a standardized format for better interoperability. + */ +void __phy_ethtool_get_phy_stats(struct phy_device *phydev, + struct ethtool_eth_phy_stats *phy_stats, + struct ethtool_phy_stats *phydev_stats) +{ + if (!phydev->drv || !phydev->drv->get_phy_stats) + return; + + mutex_lock(&phydev->lock); + phydev->drv->get_phy_stats(phydev, phy_stats, phydev_stats); + mutex_unlock(&phydev->lock); +} + +/** + * __phy_ethtool_get_link_ext_stats - Retrieve extended link statistics for a PHY + * @phydev: Pointer to the PHY device + * @link_stats: Pointer to the structure to store extended link statistics + * + * Populates the ethtool_link_ext_stats structure with link down event counts + * and additional driver-specific link statistics, if available. + */ +void __phy_ethtool_get_link_ext_stats(struct phy_device *phydev, + struct ethtool_link_ext_stats *link_stats) +{ + link_stats->link_down_events = READ_ONCE(phydev->link_down_events); + + if (!phydev->drv || !phydev->drv->get_link_stats) + return; + + mutex_lock(&phydev->lock); + phydev->drv->get_link_stats(phydev, link_stats); + mutex_unlock(&phydev->lock); +} + +/** + * phy_ethtool_get_plca_cfg - Get PLCA RS configuration + * @phydev: the phy_device struct + * @plca_cfg: where to store the retrieved configuration + * + * Retrieve the PLCA configuration from the PHY. Return 0 on success or a + * negative value if an error occurred. + */ +int phy_ethtool_get_plca_cfg(struct phy_device *phydev, + struct phy_plca_cfg *plca_cfg) +{ + int ret; + + if (!phydev->drv) { + ret = -EIO; + goto out; + } + + if (!phydev->drv->get_plca_cfg) { + ret = -EOPNOTSUPP; + goto out; + } + + mutex_lock(&phydev->lock); + ret = phydev->drv->get_plca_cfg(phydev, plca_cfg); + + mutex_unlock(&phydev->lock); +out: + return ret; +} + +/** + * plca_check_valid - Check PLCA configuration before enabling + * @phydev: the phy_device struct + * @plca_cfg: current PLCA configuration + * @extack: extack for reporting useful error messages + * + * Checks whether the PLCA and PHY configuration are consistent and it is safe + * to enable PLCA. Returns 0 on success or a negative value if the PLCA or PHY + * configuration is not consistent. + */ +static int plca_check_valid(struct phy_device *phydev, + const struct phy_plca_cfg *plca_cfg, + struct netlink_ext_ack *extack) +{ + int ret = 0; + + if (!linkmode_test_bit(ETHTOOL_LINK_MODE_10baseT1S_P2MP_Half_BIT, + phydev->advertising)) { + ret = -EOPNOTSUPP; + NL_SET_ERR_MSG(extack, + "Point to Multi-Point mode is not enabled"); + } else if (plca_cfg->node_id >= 255) { + NL_SET_ERR_MSG(extack, "PLCA node ID is not set"); + ret = -EINVAL; + } + + return ret; +} + +/** + * phy_ethtool_set_plca_cfg - Set PLCA RS configuration + * @phydev: the phy_device struct + * @plca_cfg: new PLCA configuration to apply + * @extack: extack for reporting useful error messages + * + * Sets the PLCA configuration in the PHY. Return 0 on success or a + * negative value if an error occurred. + */ +int phy_ethtool_set_plca_cfg(struct phy_device *phydev, + const struct phy_plca_cfg *plca_cfg, + struct netlink_ext_ack *extack) +{ + struct phy_plca_cfg *curr_plca_cfg; + int ret; + + if (!phydev->drv) { + ret = -EIO; + goto out; + } + + if (!phydev->drv->set_plca_cfg || + !phydev->drv->get_plca_cfg) { + ret = -EOPNOTSUPP; + goto out; + } + + curr_plca_cfg = kmalloc(sizeof(*curr_plca_cfg), GFP_KERNEL); + if (!curr_plca_cfg) { + ret = -ENOMEM; + goto out; + } + + mutex_lock(&phydev->lock); + + ret = phydev->drv->get_plca_cfg(phydev, curr_plca_cfg); + if (ret) + goto out_drv; + + if (curr_plca_cfg->enabled < 0 && plca_cfg->enabled >= 0) { + NL_SET_ERR_MSG(extack, + "PHY does not support changing the PLCA 'enable' attribute"); + ret = -EINVAL; + goto out_drv; + } + + if (curr_plca_cfg->node_id < 0 && plca_cfg->node_id >= 0) { + NL_SET_ERR_MSG(extack, + "PHY does not support changing the PLCA 'local node ID' attribute"); + ret = -EINVAL; + goto out_drv; + } + + if (curr_plca_cfg->node_cnt < 0 && plca_cfg->node_cnt >= 0) { + NL_SET_ERR_MSG(extack, + "PHY does not support changing the PLCA 'node count' attribute"); + ret = -EINVAL; + goto out_drv; + } + + if (curr_plca_cfg->to_tmr < 0 && plca_cfg->to_tmr >= 0) { + NL_SET_ERR_MSG(extack, + "PHY does not support changing the PLCA 'TO timer' attribute"); + ret = -EINVAL; + goto out_drv; + } + + if (curr_plca_cfg->burst_cnt < 0 && plca_cfg->burst_cnt >= 0) { + NL_SET_ERR_MSG(extack, + "PHY does not support changing the PLCA 'burst count' attribute"); + ret = -EINVAL; + goto out_drv; + } + + if (curr_plca_cfg->burst_tmr < 0 && plca_cfg->burst_tmr >= 0) { + NL_SET_ERR_MSG(extack, + "PHY does not support changing the PLCA 'burst timer' attribute"); + ret = -EINVAL; + goto out_drv; + } + + // if enabling PLCA, perform a few sanity checks + if (plca_cfg->enabled > 0) { + // allow setting node_id concurrently with enabled + if (plca_cfg->node_id >= 0) + curr_plca_cfg->node_id = plca_cfg->node_id; + + ret = plca_check_valid(phydev, curr_plca_cfg, extack); + if (ret) + goto out_drv; + } + + ret = phydev->drv->set_plca_cfg(phydev, plca_cfg); + +out_drv: + kfree(curr_plca_cfg); + mutex_unlock(&phydev->lock); +out: + return ret; +} + +/** + * phy_ethtool_get_plca_status - Get PLCA RS status information + * @phydev: the phy_device struct + * @plca_st: where to store the retrieved status information + * + * Retrieve the PLCA status information from the PHY. Return 0 on success or a + * negative value if an error occurred. + */ +int phy_ethtool_get_plca_status(struct phy_device *phydev, + struct phy_plca_status *plca_st) +{ + int ret; + + if (!phydev->drv) { + ret = -EIO; + goto out; + } + + if (!phydev->drv->get_plca_status) { + ret = -EOPNOTSUPP; + goto out; + } + + mutex_lock(&phydev->lock); + ret = phydev->drv->get_plca_status(phydev, plca_st); + + mutex_unlock(&phydev->lock); +out: + return ret; +} + +/** * phy_start_cable_test - Start a cable test * * @phydev: the phy_device struct @@ -724,9 +1022,16 @@ static int phy_check_link_status(struct phy_device *phydev) if (phydev->link && phydev->state != PHY_RUNNING) { phy_check_downshift(phydev); phydev->state = PHY_RUNNING; + err = genphy_c45_eee_is_active(phydev, NULL); + phydev->eee_active = err > 0; + phydev->enable_tx_lpi = phydev->eee_cfg.tx_lpi_enabled && + phydev->eee_active; + phy_link_up(phydev); } else if (!phydev->link && phydev->state != PHY_NOLINK) { phydev->state = PHY_NOLINK; + phydev->eee_active = false; + phydev->enable_tx_lpi = false; phy_link_down(phydev); } @@ -734,6 +1039,55 @@ static int phy_check_link_status(struct phy_device *phydev) } /** + * phy_inband_caps - query which in-band signalling modes are supported + * @phydev: a pointer to a &struct phy_device + * @interface: the interface mode for the PHY + * + * Returns zero if it is unknown what in-band signalling is supported by the + * PHY (e.g. because the PHY driver doesn't implement the method.) Otherwise, + * returns a bit mask of the LINK_INBAND_* values from + * &enum link_inband_signalling to describe which inband modes are supported + * by the PHY for this interface mode. + */ +unsigned int phy_inband_caps(struct phy_device *phydev, + phy_interface_t interface) +{ + if (phydev->drv && phydev->drv->inband_caps) + return phydev->drv->inband_caps(phydev, interface); + + return 0; +} +EXPORT_SYMBOL_GPL(phy_inband_caps); + +/** + * phy_config_inband - configure the desired PHY in-band mode + * @phydev: the phy_device struct + * @modes: in-band modes to configure + * + * Description: disables, enables or enables-with-bypass in-band signalling + * between the PHY and host system. + * + * Returns: zero on success, or negative errno value. + */ +int phy_config_inband(struct phy_device *phydev, unsigned int modes) +{ + lockdep_assert_held(&phydev->lock); + + if (!!(modes & LINK_INBAND_DISABLE) + + !!(modes & LINK_INBAND_ENABLE) + + !!(modes & LINK_INBAND_BYPASS) != 1) + return -EINVAL; + + if (!phydev->drv) + return -EIO; + else if (!phydev->drv->config_inband) + return -EOPNOTSUPP; + + return phydev->drv->config_inband(phydev, modes); +} +EXPORT_SYMBOL(phy_config_inband); + +/** * _phy_start_aneg - start auto-negotiation for this PHY device * @phydev: the phy_device struct * @@ -742,7 +1096,7 @@ static int phy_check_link_status(struct phy_device *phydev) * If the PHYCONTROL Layer is operating, we change the state to * reflect the beginning of Auto-negotiation or forcing. */ -static int _phy_start_aneg(struct phy_device *phydev) +int _phy_start_aneg(struct phy_device *phydev) { int err; @@ -763,6 +1117,7 @@ static int _phy_start_aneg(struct phy_device *phydev) return err; } +EXPORT_SYMBOL(_phy_start_aneg); /** * phy_start_aneg - start auto-negotiation for this PHY device @@ -821,7 +1176,10 @@ int phy_ethtool_ksettings_set(struct phy_device *phydev, if (autoneg != AUTONEG_ENABLE && autoneg != AUTONEG_DISABLE) return -EINVAL; - if (autoneg == AUTONEG_ENABLE && linkmode_empty(advertising)) + if (autoneg == AUTONEG_ENABLE && + (linkmode_empty(advertising) || + !linkmode_test_bit(ETHTOOL_LINK_MODE_Autoneg_BIT, + phydev->supported))) return -EINVAL; if (autoneg == AUTONEG_DISABLE && @@ -877,27 +1235,35 @@ EXPORT_SYMBOL(phy_ethtool_ksettings_set); int phy_speed_down(struct phy_device *phydev, bool sync) { __ETHTOOL_DECLARE_LINK_MODE_MASK(adv_tmp); - int ret; + int ret = 0; + + mutex_lock(&phydev->lock); if (phydev->autoneg != AUTONEG_ENABLE) - return 0; + goto out; linkmode_copy(adv_tmp, phydev->advertising); ret = phy_speed_down_core(phydev); if (ret) - return ret; + goto out; linkmode_copy(phydev->adv_old, adv_tmp); - if (linkmode_equal(phydev->advertising, adv_tmp)) - return 0; + if (linkmode_equal(phydev->advertising, adv_tmp)) { + ret = 0; + goto out; + } ret = phy_config_aneg(phydev); if (ret) - return ret; + goto out; + + ret = sync ? phy_poll_aneg_done(phydev) : 0; +out: + mutex_unlock(&phydev->lock); - return sync ? phy_poll_aneg_done(phydev) : 0; + return ret; } EXPORT_SYMBOL_GPL(phy_speed_down); @@ -910,21 +1276,28 @@ EXPORT_SYMBOL_GPL(phy_speed_down); int phy_speed_up(struct phy_device *phydev) { __ETHTOOL_DECLARE_LINK_MODE_MASK(adv_tmp); + int ret = 0; + + mutex_lock(&phydev->lock); if (phydev->autoneg != AUTONEG_ENABLE) - return 0; + goto out; if (linkmode_empty(phydev->adv_old)) - return 0; + goto out; linkmode_copy(adv_tmp, phydev->advertising); linkmode_copy(phydev->advertising, phydev->adv_old); linkmode_zero(phydev->adv_old); if (linkmode_equal(phydev->advertising, adv_tmp)) - return 0; + goto out; + + ret = phy_config_aneg(phydev); +out: + mutex_unlock(&phydev->lock); - return phy_config_aneg(phydev); + return ret; } EXPORT_SYMBOL_GPL(phy_speed_up); @@ -962,24 +1335,36 @@ void phy_stop_machine(struct phy_device *phydev) mutex_unlock(&phydev->lock); } +static void phy_process_error(struct phy_device *phydev) +{ + /* phydev->lock must be held for the state change to be safe */ + if (!mutex_is_locked(&phydev->lock)) + phydev_err(phydev, "PHY-device data unsafe context\n"); + + phydev->state = PHY_ERROR; + + phy_trigger_machine(phydev); +} + +static void phy_error_precise(struct phy_device *phydev, + const void *func, int err) +{ + WARN(1, "%pS: returned: %d\n", func, err); + phy_process_error(phydev); +} + /** - * phy_error - enter HALTED state for this PHY device + * phy_error - enter ERROR state for this PHY device * @phydev: target phy_device struct * - * Moves the PHY to the HALTED state in response to a read + * Moves the PHY to the ERROR state in response to a read * or write error, and tells the controller the link is down. - * Must not be called from interrupt context, or while the - * phydev->lock is held. + * Must be called with phydev->lock held. */ void phy_error(struct phy_device *phydev) { WARN_ON(1); - - mutex_lock(&phydev->lock); - phydev->state = PHY_HALTED; - mutex_unlock(&phydev->lock); - - phy_trigger_machine(phydev); + phy_process_error(phydev); } EXPORT_SYMBOL(phy_error); @@ -1003,7 +1388,6 @@ int phy_disable_interrupts(struct phy_device *phydev) static irqreturn_t phy_interrupt(int irq, void *phy_dat) { struct phy_device *phydev = phy_dat; - struct phy_driver *drv = phydev->drv; irqreturn_t ret; /* Wakeup interrupts may occur during a system sleep transition. @@ -1015,7 +1399,7 @@ static irqreturn_t phy_interrupt(int irq, void *phy_dat) if (netdev) { struct device *parent = netdev->dev.parent; - if (netdev->wol_enabled) + if (netdev->ethtool->wol_enabled) pm_system_wakeup(); else if (device_may_wakeup(&netdev->dev)) pm_wakeup_dev_event(&netdev->dev, 0, true); @@ -1029,7 +1413,7 @@ static irqreturn_t phy_interrupt(int irq, void *phy_dat) } mutex_lock(&phydev->lock); - ret = drv->handle_interrupt(phydev); + ret = phydev->drv->handle_interrupt(phydev); mutex_unlock(&phydev->lock); return ret; @@ -1045,6 +1429,23 @@ static int phy_enable_interrupts(struct phy_device *phydev) } /** + * phy_update_stats - Update PHY device statistics if supported. + * @phydev: Pointer to the PHY device structure. + * + * If the PHY driver provides an update_stats callback, this function + * invokes it to update the PHY statistics. If not, it returns 0. + * + * Return: 0 on success, or a negative error code if the callback fails. + */ +static int phy_update_stats(struct phy_device *phydev) +{ + if (!phydev->drv->update_stats) + return 0; + + return phydev->drv->update_stats(phydev); +} + +/** * phy_request_interrupt - request and enable interrupt for a PHY device * @phydev: target phy_device struct * @@ -1088,20 +1489,168 @@ void phy_free_interrupt(struct phy_device *phydev) EXPORT_SYMBOL(phy_free_interrupt); /** + * phy_get_next_update_time - Determine the next PHY update time + * @phydev: Pointer to the phy_device structure + * + * This function queries the PHY driver to get the time for the next polling + * event. If the driver does not implement the callback, a default value is + * used. + * + * Return: The time for the next polling event in jiffies + */ +static unsigned int phy_get_next_update_time(struct phy_device *phydev) +{ + if (phydev->drv && phydev->drv->get_next_update_time) + return phydev->drv->get_next_update_time(phydev); + + return PHY_STATE_TIME; +} + +enum phy_state_work { + PHY_STATE_WORK_NONE, + PHY_STATE_WORK_ANEG, + PHY_STATE_WORK_SUSPEND, +}; + +static enum phy_state_work _phy_state_machine(struct phy_device *phydev) +{ + enum phy_state_work state_work = PHY_STATE_WORK_NONE; + struct net_device *dev = phydev->attached_dev; + enum phy_state old_state = phydev->state; + const void *func = NULL; + bool finished = false; + int err = 0; + + switch (phydev->state) { + case PHY_DOWN: + case PHY_READY: + break; + case PHY_UP: + state_work = PHY_STATE_WORK_ANEG; + break; + case PHY_NOLINK: + case PHY_RUNNING: + err = phy_check_link_status(phydev); + func = &phy_check_link_status; + + if (!err) + err = phy_update_stats(phydev); + break; + case PHY_CABLETEST: + err = phydev->drv->cable_test_get_status(phydev, &finished); + if (err) { + phy_abort_cable_test(phydev); + netif_testing_off(dev); + state_work = PHY_STATE_WORK_ANEG; + phydev->state = PHY_UP; + break; + } + + if (finished) { + ethnl_cable_test_finished(phydev); + netif_testing_off(dev); + state_work = PHY_STATE_WORK_ANEG; + phydev->state = PHY_UP; + } + break; + case PHY_HALTED: + if (phydev->link) { + if (phydev->autoneg == AUTONEG_ENABLE) { + phydev->speed = SPEED_UNKNOWN; + phydev->duplex = DUPLEX_UNKNOWN; + } + if (phydev->master_slave_state != + MASTER_SLAVE_STATE_UNSUPPORTED) + phydev->master_slave_state = + MASTER_SLAVE_STATE_UNKNOWN; + phydev->mdix = ETH_TP_MDI_INVALID; + linkmode_zero(phydev->lp_advertising); + } + fallthrough; + case PHY_ERROR: + if (phydev->link) { + phydev->link = 0; + phydev->eee_active = false; + phydev->enable_tx_lpi = false; + phy_link_down(phydev); + } + state_work = PHY_STATE_WORK_SUSPEND; + break; + } + + if (state_work == PHY_STATE_WORK_ANEG) { + err = _phy_start_aneg(phydev); + func = &_phy_start_aneg; + } + + if (err == -ENODEV) + return state_work; + + if (err < 0) + phy_error_precise(phydev, func, err); + + phy_process_state_change(phydev, old_state); + + /* Only re-schedule a PHY state machine change if we are polling the + * PHY, if PHY_MAC_INTERRUPT is set, then we will be moving + * between states from phy_mac_interrupt(). + * + * In state PHY_HALTED the PHY gets suspended, so rescheduling the + * state machine would be pointless and possibly error prone when + * called from phy_disconnect() synchronously. + */ + if (phy_polling_mode(phydev) && phy_is_started(phydev)) + phy_queue_state_machine(phydev, + phy_get_next_update_time(phydev)); + + return state_work; +} + +/* unlocked part of the PHY state machine */ +static void _phy_state_machine_post_work(struct phy_device *phydev, + enum phy_state_work state_work) +{ + if (state_work == PHY_STATE_WORK_SUSPEND) + phy_suspend(phydev); +} + +/** + * phy_state_machine - Handle the state machine + * @work: work_struct that describes the work to be done + */ +void phy_state_machine(struct work_struct *work) +{ + struct delayed_work *dwork = to_delayed_work(work); + struct phy_device *phydev = + container_of(dwork, struct phy_device, state_queue); + enum phy_state_work state_work; + + mutex_lock(&phydev->lock); + state_work = _phy_state_machine(phydev); + mutex_unlock(&phydev->lock); + + _phy_state_machine_post_work(phydev, state_work); +} + +/** * phy_stop - Bring down the PHY link, and stop checking the status * @phydev: target phy_device struct */ void phy_stop(struct phy_device *phydev) { struct net_device *dev = phydev->attached_dev; + enum phy_state_work state_work; + enum phy_state old_state; - if (!phy_is_started(phydev) && phydev->state != PHY_DOWN) { + if (!phy_is_started(phydev) && phydev->state != PHY_DOWN && + phydev->state != PHY_ERROR) { WARN(1, "called from state %s\n", phy_state_to_str(phydev->state)); return; } mutex_lock(&phydev->lock); + old_state = phydev->state; if (phydev->state == PHY_CABLETEST) { phy_abort_cable_test(phydev); @@ -1112,10 +1661,12 @@ void phy_stop(struct phy_device *phydev) sfp_upstream_stop(phydev->sfp_bus); phydev->state = PHY_HALTED; + phy_process_state_change(phydev, old_state); + state_work = _phy_state_machine(phydev); mutex_unlock(&phydev->lock); - phy_state_machine(&phydev->state_queue.work); + _phy_state_machine_post_work(phydev, state_work); phy_stop_machine(phydev); /* Cannot call flush_scheduled_work() here as desired because @@ -1160,134 +1711,146 @@ out: EXPORT_SYMBOL(phy_start); /** - * phy_state_machine - Handle the state machine - * @work: work_struct that describes the work to be done + * phy_mac_interrupt - MAC says the link has changed + * @phydev: phy_device struct with changed link + * + * The MAC layer is able to indicate there has been a change in the PHY link + * status. Trigger the state machine and work a work queue. */ -void phy_state_machine(struct work_struct *work) +void phy_mac_interrupt(struct phy_device *phydev) { - struct delayed_work *dwork = to_delayed_work(work); - struct phy_device *phydev = - container_of(dwork, struct phy_device, state_queue); - struct net_device *dev = phydev->attached_dev; - bool needs_aneg = false, do_suspend = false; - enum phy_state old_state; - bool finished = false; - int err = 0; - - mutex_lock(&phydev->lock); + /* Trigger a state machine change */ + phy_trigger_machine(phydev); +} +EXPORT_SYMBOL(phy_mac_interrupt); - old_state = phydev->state; +/** + * phy_loopback - Configure loopback mode of PHY + * @phydev: target phy_device struct + * @enable: enable or disable loopback mode + * @speed: enable loopback mode with speed + * + * Configure loopback mode of PHY and signal link down and link up if speed is + * changing. + * + * Return: 0 on success, negative error code on failure. + */ +int phy_loopback(struct phy_device *phydev, bool enable, int speed) +{ + bool link_up = false; + int ret = 0; - switch (phydev->state) { - case PHY_DOWN: - case PHY_READY: - break; - case PHY_UP: - needs_aneg = true; + if (!phydev->drv) + return -EIO; - break; - case PHY_NOLINK: - case PHY_RUNNING: - err = phy_check_link_status(phydev); - break; - case PHY_CABLETEST: - err = phydev->drv->cable_test_get_status(phydev, &finished); - if (err) { - phy_abort_cable_test(phydev); - netif_testing_off(dev); - needs_aneg = true; - phydev->state = PHY_UP; - break; - } + mutex_lock(&phydev->lock); - if (finished) { - ethnl_cable_test_finished(phydev); - netif_testing_off(dev); - needs_aneg = true; - phydev->state = PHY_UP; - } - break; - case PHY_HALTED: - if (phydev->link) { - phydev->link = 0; - phy_link_down(phydev); - } - do_suspend = true; - break; + if (enable && phydev->loopback_enabled) { + ret = -EBUSY; + goto out; } - mutex_unlock(&phydev->lock); + if (!enable && !phydev->loopback_enabled) { + ret = -EINVAL; + goto out; + } - if (needs_aneg) - err = phy_start_aneg(phydev); - else if (do_suspend) - phy_suspend(phydev); + if (enable) { + /* + * Link up is signaled with a defined speed. If speed changes, + * then first link down and after that link up needs to be + * signaled. + */ + if (phydev->link && phydev->state == PHY_RUNNING) { + /* link is up and signaled */ + if (speed && phydev->speed != speed) { + /* signal link down and up for new speed */ + phydev->link = false; + phydev->state = PHY_NOLINK; + phy_link_down(phydev); + + link_up = true; + } + } else { + /* link is not signaled */ + if (speed) { + /* signal link up for new speed */ + link_up = true; + } + } + } - if (err == -ENODEV) - return; + if (phydev->drv->set_loopback) + ret = phydev->drv->set_loopback(phydev, enable, speed); + else + ret = genphy_loopback(phydev, enable, speed); + + if (ret) { + if (enable) { + /* try to restore link if enabling loopback fails */ + if (phydev->drv->set_loopback) + phydev->drv->set_loopback(phydev, false, 0); + else + genphy_loopback(phydev, false, 0); + } - if (err < 0) - phy_error(phydev); + goto out; + } - if (old_state != phydev->state) { - phydev_dbg(phydev, "PHY state change %s -> %s\n", - phy_state_to_str(old_state), - phy_state_to_str(phydev->state)); - if (phydev->drv && phydev->drv->link_change_notify) - phydev->drv->link_change_notify(phydev); + if (link_up) { + phydev->link = true; + phydev->state = PHY_RUNNING; + phy_link_up(phydev); } - /* Only re-schedule a PHY state machine change if we are polling the - * PHY, if PHY_MAC_INTERRUPT is set, then we will be moving - * between states from phy_mac_interrupt(). - * - * In state PHY_HALTED the PHY gets suspended, so rescheduling the - * state machine would be pointless and possibly error prone when - * called from phy_disconnect() synchronously. - */ - mutex_lock(&phydev->lock); - if (phy_polling_mode(phydev) && phy_is_started(phydev)) - phy_queue_state_machine(phydev, PHY_STATE_TIME); + phydev->loopback_enabled = enable; + +out: mutex_unlock(&phydev->lock); + return ret; } +EXPORT_SYMBOL(phy_loopback); /** - * phy_mac_interrupt - MAC says the link has changed - * @phydev: phy_device struct with changed link + * phy_eee_tx_clock_stop_capable() - indicate whether the MAC can stop tx clock + * @phydev: target phy_device struct * - * The MAC layer is able to indicate there has been a change in the PHY link - * status. Trigger the state machine and work a work queue. + * Indicate whether the MAC can disable the transmit xMII clock while in LPI + * state. Returns 1 if the MAC may stop the transmit clock, 0 if the MAC must + * not stop the transmit clock, or negative error. */ -void phy_mac_interrupt(struct phy_device *phydev) +int phy_eee_tx_clock_stop_capable(struct phy_device *phydev) { - /* Trigger a state machine change */ - phy_trigger_machine(phydev); + int stat1; + + stat1 = phy_read_mmd(phydev, MDIO_MMD_PCS, MDIO_STAT1); + if (stat1 < 0) + return stat1; + + return !!(stat1 & MDIO_PCS_STAT1_CLKSTOP_CAP); } -EXPORT_SYMBOL(phy_mac_interrupt); +EXPORT_SYMBOL_GPL(phy_eee_tx_clock_stop_capable); -static void mmd_eee_adv_to_linkmode(unsigned long *advertising, u16 eee_adv) -{ - linkmode_zero(advertising); - - if (eee_adv & MDIO_EEE_100TX) - linkmode_set_bit(ETHTOOL_LINK_MODE_100baseT_Full_BIT, - advertising); - if (eee_adv & MDIO_EEE_1000T) - linkmode_set_bit(ETHTOOL_LINK_MODE_1000baseT_Full_BIT, - advertising); - if (eee_adv & MDIO_EEE_10GT) - linkmode_set_bit(ETHTOOL_LINK_MODE_10000baseT_Full_BIT, - advertising); - if (eee_adv & MDIO_EEE_1000KX) - linkmode_set_bit(ETHTOOL_LINK_MODE_1000baseKX_Full_BIT, - advertising); - if (eee_adv & MDIO_EEE_10GKX4) - linkmode_set_bit(ETHTOOL_LINK_MODE_10000baseKX4_Full_BIT, - advertising); - if (eee_adv & MDIO_EEE_10GKR) - linkmode_set_bit(ETHTOOL_LINK_MODE_10000baseKR_Full_BIT, - advertising); +/** + * phy_eee_rx_clock_stop() - configure PHY receive clock in LPI + * @phydev: target phy_device struct + * @clk_stop_enable: flag to indicate whether the clock can be stopped + * + * Configure whether the PHY can disable its receive clock during LPI mode, + * See IEEE 802.3 sections 22.2.2.2, 35.2.2.10, and 45.2.3.1.4. + * + * Returns: 0 or negative error. + */ +int phy_eee_rx_clock_stop(struct phy_device *phydev, bool clk_stop_enable) +{ + /* Configure the PHY to stop receiving xMII + * clock while it is signaling LPI. + */ + return phy_modify_mmd(phydev, MDIO_MMD_PCS, MDIO_CTRL1, + MDIO_PCS_CTRL1_CLKSTOP_EN, + clk_stop_enable ? MDIO_PCS_CTRL1_CLKSTOP_EN : 0); } +EXPORT_SYMBOL_GPL(phy_eee_rx_clock_stop); /** * phy_init_eee - init and check the EEE feature @@ -1301,62 +1864,21 @@ static void mmd_eee_adv_to_linkmode(unsigned long *advertising, u16 eee_adv) */ int phy_init_eee(struct phy_device *phydev, bool clk_stop_enable) { + int ret; + if (!phydev->drv) return -EIO; - /* According to 802.3az,the EEE is supported only in full duplex-mode. - */ - if (phydev->duplex == DUPLEX_FULL) { - __ETHTOOL_DECLARE_LINK_MODE_MASK(common); - __ETHTOOL_DECLARE_LINK_MODE_MASK(lp); - __ETHTOOL_DECLARE_LINK_MODE_MASK(adv); - int eee_lp, eee_cap, eee_adv; - int status; - u32 cap; - - /* Read phy status to properly get the right settings */ - status = phy_read_status(phydev); - if (status) - return status; - - /* First check if the EEE ability is supported */ - eee_cap = phy_read_mmd(phydev, MDIO_MMD_PCS, MDIO_PCS_EEE_ABLE); - if (eee_cap <= 0) - goto eee_exit_err; - - cap = mmd_eee_cap_to_ethtool_sup_t(eee_cap); - if (!cap) - goto eee_exit_err; - - /* Check which link settings negotiated and verify it in - * the EEE advertising registers. - */ - eee_lp = phy_read_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_LPABLE); - if (eee_lp <= 0) - goto eee_exit_err; - - eee_adv = phy_read_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_ADV); - if (eee_adv <= 0) - goto eee_exit_err; - - mmd_eee_adv_to_linkmode(adv, eee_adv); - mmd_eee_adv_to_linkmode(lp, eee_lp); - linkmode_and(common, adv, lp); - - if (!phy_check_valid(phydev->speed, phydev->duplex, common)) - goto eee_exit_err; + ret = genphy_c45_eee_is_active(phydev, NULL); + if (ret < 0) + return ret; + if (!ret) + return -EPROTONOSUPPORT; - if (clk_stop_enable) - /* Configure the PHY to stop receiving xMII - * clock while it is signaling LPI. - */ - phy_set_bits_mmd(phydev, MDIO_MMD_PCS, MDIO_CTRL1, - MDIO_PCS_CTRL1_CLKSTOP_EN); + if (clk_stop_enable) + ret = phy_eee_rx_clock_stop(phydev, true); - return 0; /* EEE supported */ - } -eee_exit_err: - return -EPROTONOSUPPORT; + return ret < 0 ? ret : 0; } EXPORT_SYMBOL(phy_init_eee); @@ -1369,99 +1891,107 @@ EXPORT_SYMBOL(phy_init_eee); */ int phy_get_eee_err(struct phy_device *phydev) { + int ret; + if (!phydev->drv) return -EIO; - return phy_read_mmd(phydev, MDIO_MMD_PCS, MDIO_PCS_EEE_WK_ERR); + mutex_lock(&phydev->lock); + ret = phy_read_mmd(phydev, MDIO_MMD_PCS, MDIO_PCS_EEE_WK_ERR); + mutex_unlock(&phydev->lock); + + return ret; } EXPORT_SYMBOL(phy_get_eee_err); /** * phy_ethtool_get_eee - get EEE supported and status * @phydev: target phy_device struct - * @data: ethtool_eee data + * @data: ethtool_keee data * - * Description: it reportes the Supported/Advertisement/LP Advertisement - * capabilities. + * Description: get the current EEE settings, filling in all members of + * @data. */ -int phy_ethtool_get_eee(struct phy_device *phydev, struct ethtool_eee *data) +int phy_ethtool_get_eee(struct phy_device *phydev, struct ethtool_keee *data) { - int val; + int ret; if (!phydev->drv) return -EIO; - /* Get Supported EEE */ - val = phy_read_mmd(phydev, MDIO_MMD_PCS, MDIO_PCS_EEE_ABLE); - if (val < 0) - return val; - data->supported = mmd_eee_cap_to_ethtool_sup_t(val); + mutex_lock(&phydev->lock); + ret = genphy_c45_ethtool_get_eee(phydev, data); + eeecfg_to_eee(data, &phydev->eee_cfg); + mutex_unlock(&phydev->lock); + + return ret; +} +EXPORT_SYMBOL(phy_ethtool_get_eee); - /* Get advertisement EEE */ - val = phy_read_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_ADV); - if (val < 0) - return val; - data->advertised = mmd_eee_adv_to_ethtool_adv_t(val); - data->eee_enabled = !!data->advertised; +/** + * phy_ethtool_set_eee_noneg - Adjusts MAC LPI configuration without PHY + * renegotiation + * @phydev: pointer to the target PHY device structure + * @old_cfg: pointer to the eee_config structure containing the old EEE settings + * + * This function updates the Energy Efficient Ethernet (EEE) configuration + * for cases where only the MAC's Low Power Idle (LPI) configuration changes, + * without triggering PHY renegotiation. It ensures that the MAC is properly + * informed of the new LPI settings by cycling the link down and up, which + * is necessary for the MAC to adopt the new configuration. This adjustment + * is done only if there is a change in the tx_lpi_enabled or tx_lpi_timer + * configuration. + */ +static void phy_ethtool_set_eee_noneg(struct phy_device *phydev, + const struct eee_config *old_cfg) +{ + bool enable_tx_lpi; - /* Get LP advertisement EEE */ - val = phy_read_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_LPABLE); - if (val < 0) - return val; - data->lp_advertised = mmd_eee_adv_to_ethtool_adv_t(val); + if (!phydev->link) + return; - data->eee_active = !!(data->advertised & data->lp_advertised); + enable_tx_lpi = phydev->eee_cfg.tx_lpi_enabled && phydev->eee_active; - return 0; + if (phydev->enable_tx_lpi != enable_tx_lpi || + phydev->eee_cfg.tx_lpi_timer != old_cfg->tx_lpi_timer) { + phydev->enable_tx_lpi = false; + phydev->link = false; + phy_link_down(phydev); + phydev->enable_tx_lpi = enable_tx_lpi; + phydev->link = true; + phy_link_up(phydev); + } } -EXPORT_SYMBOL(phy_ethtool_get_eee); /** * phy_ethtool_set_eee - set EEE supported and status * @phydev: target phy_device struct - * @data: ethtool_eee data + * @data: ethtool_keee data * * Description: it is to program the Advertisement EEE register. */ -int phy_ethtool_set_eee(struct phy_device *phydev, struct ethtool_eee *data) +int phy_ethtool_set_eee(struct phy_device *phydev, struct ethtool_keee *data) { - int cap, old_adv, adv = 0, ret; + struct eee_config old_cfg; + int ret; if (!phydev->drv) return -EIO; - /* Get Supported EEE */ - cap = phy_read_mmd(phydev, MDIO_MMD_PCS, MDIO_PCS_EEE_ABLE); - if (cap < 0) - return cap; - - old_adv = phy_read_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_ADV); - if (old_adv < 0) - return old_adv; + mutex_lock(&phydev->lock); - if (data->eee_enabled) { - adv = !data->advertised ? cap : - ethtool_adv_to_mmd_eee_adv_t(data->advertised) & cap; - /* Mask prohibited EEE modes */ - adv &= ~phydev->eee_broken_modes; - } + old_cfg = phydev->eee_cfg; + eee_to_eeecfg(&phydev->eee_cfg, data); - if (old_adv != adv) { - ret = phy_write_mmd(phydev, MDIO_MMD_AN, MDIO_AN_EEE_ADV, adv); - if (ret < 0) - return ret; + ret = genphy_c45_ethtool_set_eee(phydev, data); + if (ret == 0) + phy_ethtool_set_eee_noneg(phydev, &old_cfg); + else if (ret < 0) + phydev->eee_cfg = old_cfg; - /* Restart autonegotiation so the new modes get sent to the - * link partner. - */ - if (phydev->autoneg == AUTONEG_ENABLE) { - ret = phy_restart_aneg(phydev); - if (ret < 0) - return ret; - } - } + mutex_unlock(&phydev->lock); - return 0; + return ret < 0 ? ret : 0; } EXPORT_SYMBOL(phy_ethtool_set_eee); @@ -1473,8 +2003,15 @@ EXPORT_SYMBOL(phy_ethtool_set_eee); */ int phy_ethtool_set_wol(struct phy_device *phydev, struct ethtool_wolinfo *wol) { - if (phydev->drv && phydev->drv->set_wol) - return phydev->drv->set_wol(phydev, wol); + int ret; + + if (phydev->drv && phydev->drv->set_wol) { + mutex_lock(&phydev->lock); + ret = phydev->drv->set_wol(phydev, wol); + mutex_unlock(&phydev->lock); + + return ret; + } return -EOPNOTSUPP; } @@ -1488,8 +2025,11 @@ EXPORT_SYMBOL(phy_ethtool_set_wol); */ void phy_ethtool_get_wol(struct phy_device *phydev, struct ethtool_wolinfo *wol) { - if (phydev->drv && phydev->drv->get_wol) + if (phydev->drv && phydev->drv->get_wol) { + mutex_lock(&phydev->lock); phydev->drv->get_wol(phydev, wol); + mutex_unlock(&phydev->lock); + } } EXPORT_SYMBOL(phy_ethtool_get_wol); @@ -1526,6 +2066,7 @@ EXPORT_SYMBOL(phy_ethtool_set_link_ksettings); int phy_ethtool_nway_reset(struct net_device *ndev) { struct phy_device *phydev = ndev->phydev; + int ret; if (!phydev) return -ENODEV; @@ -1533,6 +2074,10 @@ int phy_ethtool_nway_reset(struct net_device *ndev) if (!phydev->drv) return -EIO; - return phy_restart_aneg(phydev); + mutex_lock(&phydev->lock); + ret = phy_restart_aneg(phydev); + mutex_unlock(&phydev->lock); + + return ret; } EXPORT_SYMBOL(phy_ethtool_nway_reset); |
