// 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);
}