// SPDX-License-Identifier: GPL-2.0 /* Copyright (C) 2026 Intel Corporation */ #include "ice.h" #include "ice_cpi.h" #include "ice_txclk.h" #define ICE_PHY0 0 #define ICE_PHY1 1 /** * ice_txclk_get_pin - map TX reference clock to its DPLL pin * @pf: pointer to the PF structure * @ref_clk: TX reference clock selection * * Return the DPLL pin corresponding to a given external TX reference * clock. Only external TX reference clocks (SYNCE and EREF0) are * represented as DPLL pins. The internal ENET (TXCO) clock has no * associated DPLL pin and therefore yields %NULL. * * This helper is used when emitting DPLL pin change notifications * after TX reference clock transitions have been verified. * * Return: Pointer to the corresponding struct dpll_pin, or %NULL if * the TX reference clock has no DPLL pin representation. */ struct dpll_pin * ice_txclk_get_pin(struct ice_pf *pf, enum ice_e825c_ref_clk ref_clk) { switch (ref_clk) { case ICE_REF_CLK_SYNCE: return pf->dplls.txclks[E825_EXT_SYNCE_PIN_IDX].pin; case ICE_REF_CLK_EREF0: return pf->dplls.txclks[E825_EXT_EREF_PIN_IDX].pin; case ICE_REF_CLK_ENET: default: return NULL; } } /** * ice_txclk_enable_peer - Enable required TX reference clock on peer PHY * @pf: pointer to the PF structure * @clk: TX reference clock that must be enabled * * Some TX reference clocks on E825-class devices (SyncE and EREF0) must * be enabled on both PHY complexes to allow proper routing: * * - SyncE must be enabled on both PHYs when used by PHY0 * - EREF0 must be enabled on both PHYs when used by PHY1 * * If the requested clock is not yet enabled on the peer PHY, enable it. * ENET does not require duplication and is ignored. * * Return: 0 on success or negative error code on failure. */ static int ice_txclk_enable_peer(struct ice_pf *pf, enum ice_e825c_ref_clk clk) { struct ice_pf *ctrl_pf = ice_get_ctrl_pf(pf); bool peer_clk_in_use; u8 port_num, phy; int err; if (clk == ICE_REF_CLK_ENET) return 0; if (IS_ERR_OR_NULL(ctrl_pf)) { dev_err(ice_pf_to_dev(pf), "Can't enable tx-clk on peer: no controlling PF\n"); return -EINVAL; } port_num = pf->ptp.port.port_num; phy = port_num / pf->hw.ptp.ports_per_phy; peer_clk_in_use = true; /* Hold ctrl_pf->dplls.lock across both the peer-usage check and * the enable AQ command so that two PFs racing to enable the same * peer-PHY clock cannot both observe peer_clk_in_use == false and * issue duplicate enables. */ mutex_lock(&ctrl_pf->dplls.lock); if (clk == ICE_REF_CLK_SYNCE && phy == ICE_PHY0) peer_clk_in_use = ice_txclk_any_port_uses(ctrl_pf, ICE_PHY1, clk); else if (clk == ICE_REF_CLK_EREF0 && phy == ICE_PHY1) peer_clk_in_use = ice_txclk_any_port_uses(ctrl_pf, ICE_PHY0, clk); if ((clk == ICE_REF_CLK_SYNCE && phy == ICE_PHY0 && !peer_clk_in_use) || (clk == ICE_REF_CLK_EREF0 && phy == ICE_PHY1 && !peer_clk_in_use)) { u8 peer_phy = phy ? ICE_PHY0 : ICE_PHY1; err = ice_cpi_ena_dis_clk_ref(&pf->hw, peer_phy, clk, true); if (err) { mutex_unlock(&ctrl_pf->dplls.lock); dev_err(ice_pf_to_dev(pf), "Failed to enable the %u TX clock for the %u PHY\n", clk, peer_phy); return err; } } mutex_unlock(&ctrl_pf->dplls.lock); return 0; } #define ICE_REFCLK_USER_TO_AQ_IDX(x) ((x) + 1) /** * ice_txclk_set_clk - Set Tx reference clock * @pf: pointer to pf structure * @clk: new Tx clock * * Return: 0 on success, negative value otherwise. */ int ice_txclk_set_clk(struct ice_pf *pf, enum ice_e825c_ref_clk clk) { struct ice_pf *ctrl_pf = ice_get_ctrl_pf(pf); struct ice_port_info *port_info; bool clk_in_use; u8 port_num, phy; int err; if (pf->ptp.port.tx_clk == clk) return 0; if (IS_ERR_OR_NULL(ctrl_pf)) { dev_err(ice_pf_to_dev(pf), "Can't set tx-clk: no controlling PF\n"); return -EINVAL; } if (!test_bit(ICE_FLAG_DPLL, ctrl_pf->flags)) { dev_err(ice_pf_to_dev(pf), "Can't set tx-clk: ctrl PF DPLL not available\n"); return -EOPNOTSUPP; } port_num = pf->ptp.port.port_num; phy = port_num / pf->hw.ptp.ports_per_phy; port_info = pf->hw.port_info; /* Hold ctrl_pf->dplls.lock across both the usage check and the * enable AQ command so that two PFs racing to switch to the same * (phy, clk) cannot both observe clk_in_use == false and issue * duplicate enables. The tx_refclks bitmap is updated only later * by ice_txclk_update_and_notify() after link-up, so without this * the check-then-act window is wide open. */ mutex_lock(&ctrl_pf->dplls.lock); clk_in_use = ice_txclk_any_port_uses(ctrl_pf, phy, clk); if (!clk_in_use) { err = ice_cpi_ena_dis_clk_ref(&pf->hw, phy, clk, true); if (err) { mutex_unlock(&ctrl_pf->dplls.lock); dev_err(ice_pf_to_dev(pf), "Failed to enable the %u TX clock for the %u PHY\n", clk, phy); return err; } } mutex_unlock(&ctrl_pf->dplls.lock); if (!clk_in_use) { err = ice_txclk_enable_peer(pf, clk); if (err) return err; } /* We are ready to switch to the new TX clk. */ err = ice_aq_set_link_restart_an(port_info, true, NULL, ICE_REFCLK_USER_TO_AQ_IDX(clk)); if (err) { dev_err(ice_pf_to_dev(pf), "AN restart AQ command failed with err %d\n", err); return err; } /* Clear txclk_switch_requested only after the AN restart AQ has been * accepted by FW. Clearing earlier would race with any asynchronous * link-up event: ice_txclk_update_and_notify() would observe the * cleared flag, read the stale SERDES selector, and misinterpret the * not-yet-applied switch as a HW rejection. Only clear if no newer * request has overwritten tx_clk_req while we were dropping locks. */ mutex_lock(&pf->dplls.lock); if (pf->ptp.port.tx_clk_req == clk) pf->dplls.txclk_switch_requested = false; mutex_unlock(&pf->dplls.lock); return 0; } /** * ice_txclk_update_and_notify - Validate TX reference clock switching * @pf: pointer to PF structure * * After a link-up event, verify whether the previously requested TX reference * clock transition actually succeeded. The SERDES reference selector reflects * the effective hardware choice, which may differ from the requested clock * when Auto-Negotiation or firmware applies additional policy. * * If the hardware-selected clock differs from the requested one, update the * software state accordingly and stop further processing. * * When the switch is successful, update the per‑PHY usage bitmaps so that the * driver knows which reference clock is currently in use by this port. * * This function does not initiate a clock switch; it only validates the result * of a previously triggered transition and performs cleanup of unused clocks. */ void ice_txclk_update_and_notify(struct ice_pf *pf) { struct ice_ptp_port *ptp_port = &pf->ptp.port; struct ice_pf *ctrl_pf = ice_get_ctrl_pf(pf); struct dpll_pin *old_pin = NULL; struct dpll_pin *new_pin = NULL; struct ice_hw *hw = &pf->hw; enum ice_e825c_ref_clk clk; bool notify_dpll = false; int err; u8 phy; phy = ptp_port->port_num / hw->ptp.ports_per_phy; /* Hold txclk_notify_rwsem for read across the entire critical * region, including the out-of-lock dpll_*_change_ntf() calls * below. ice_dpll_deinit() takes the write side to wait for all * in-flight notifications to complete before freeing pins and the * TXC DPLL device, preventing a use-after-free on rmmod. */ down_read(&pf->dplls.txclk_notify_rwsem); mutex_lock(&pf->dplls.lock); /* Bail out if DPLL subsystem is being torn down. ice_dpll_deinit() * clears ICE_FLAG_DPLL before freeing pins and the dpll device, so a * cleared flag under the lock means those objects can no longer be * safely dereferenced. */ if (!test_bit(ICE_FLAG_DPLL, pf->flags)) { mutex_unlock(&pf->dplls.lock); goto out; } /* If a switch is still pending, the link-up event preceded the * worker's AN restart. Hardware hasn't applied the new clock yet, * so reading the SERDES selector now would produce a false failure. * Let the worker run first; the link-up that follows the AN restart * will trigger the verification. */ if (pf->dplls.txclk_switch_requested) { mutex_unlock(&pf->dplls.lock); goto out; } /* no TX clock change requested */ if (pf->ptp.port.tx_clk == pf->ptp.port.tx_clk_req) { mutex_unlock(&pf->dplls.lock); goto out; } /* verify current Tx reference settings */ err = ice_get_serdes_ref_sel_e825c(hw, ptp_port->port_num, &clk); if (err) { mutex_unlock(&pf->dplls.lock); goto out; } if (clk != pf->ptp.port.tx_clk_req) { dev_warn(ice_pf_to_dev(pf), "Failed to switch tx-clk for phy %d and clk %u (current: %u)\n", phy, pf->ptp.port.tx_clk_req, clk); old_pin = ice_txclk_get_pin(pf, pf->ptp.port.tx_clk_req); new_pin = ice_txclk_get_pin(pf, clk); pf->ptp.port.tx_clk = clk; pf->ptp.port.tx_clk_req = clk; /* Update the reference clock bitmap to match the hardware * clock that was actually accepted, so that * ice_txclk_any_port_uses() reflects reality even on failure. * The map is owned by ctrl_pf; take its lock per documented * order (pf->dplls.lock first, then ctrl_pf->dplls.lock) so * readers on other PFs observe a consistent snapshot. */ if (!IS_ERR_OR_NULL(ctrl_pf)) { if (ctrl_pf != pf) mutex_lock(&ctrl_pf->dplls.lock); for (int i = 0; i < ICE_REF_CLK_MAX; i++) { if (clk == i) set_bit(ptp_port->port_num, &ctrl_pf->ptp.tx_refclks[phy][i]); else clear_bit(ptp_port->port_num, &ctrl_pf->ptp.tx_refclks[phy][i]); } if (ctrl_pf != pf) mutex_unlock(&ctrl_pf->dplls.lock); } goto err_notify; } old_pin = ice_txclk_get_pin(pf, pf->ptp.port.tx_clk); pf->ptp.port.tx_clk = clk; pf->ptp.port.tx_clk_req = clk; if (IS_ERR_OR_NULL(ctrl_pf)) { dev_err(ice_pf_to_dev(pf), "Can't set tx-clk: no controlling PF\n"); goto err_notify; } /* update Tx reference clock usage map; map is owned by ctrl_pf, * take its lock per documented order so readers on other PFs see * a consistent view. */ if (ctrl_pf != pf) mutex_lock(&ctrl_pf->dplls.lock); for (int i = 0; i < ICE_REF_CLK_MAX; i++) if (clk == i) set_bit(ptp_port->port_num, &ctrl_pf->ptp.tx_refclks[phy][i]); else clear_bit(ptp_port->port_num, &ctrl_pf->ptp.tx_refclks[phy][i]); if (ctrl_pf != pf) mutex_unlock(&ctrl_pf->dplls.lock); err_notify: /* Update TXC DPLL lock status based on effective TX clk, while still * holding the lock to prevent concurrent link-up events from racing * on dpll_state. */ if (!IS_ERR_OR_NULL(pf->dplls.txc.dpll)) { enum dpll_lock_status new_lock = ice_txclk_lock_status(clk); if (pf->dplls.txc.dpll_state != new_lock) { pf->dplls.txc.dpll_state = new_lock; notify_dpll = true; } } mutex_unlock(&pf->dplls.lock); /* Notify TX clk pins state transition */ if (old_pin) dpll_pin_change_ntf(old_pin); if (new_pin) dpll_pin_change_ntf(new_pin); if (notify_dpll && !IS_ERR_OR_NULL(pf->dplls.txc.dpll)) dpll_device_change_ntf(pf->dplls.txc.dpll); out: up_read(&pf->dplls.txclk_notify_rwsem); }