446 lines
11 KiB
C
446 lines
11 KiB
C
// SPDX-License-Identifier: GPL-2.0+
|
|
/* Copyright 2025-2026 NXP */
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/platform_device.h>
|
|
|
|
#include "phy-fsl-lynx-core.h"
|
|
|
|
const char *lynx_lane_mode_str(enum lynx_lane_mode lane_mode)
|
|
{
|
|
switch (lane_mode) {
|
|
case LANE_MODE_1000BASEX_SGMII:
|
|
return "1000Base-X/SGMII";
|
|
case LANE_MODE_2500BASEX:
|
|
return "2500Base-X";
|
|
case LANE_MODE_QSGMII:
|
|
return "QSGMII";
|
|
case LANE_MODE_10G_QXGMII:
|
|
return "10G-QXGMII";
|
|
case LANE_MODE_10GBASER:
|
|
return "10GBase-R";
|
|
case LANE_MODE_USXGMII:
|
|
return "USXGMII";
|
|
case LANE_MODE_25GBASER:
|
|
return "25GBase-R";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_lane_mode_str, "PHY_FSL_LYNX");
|
|
|
|
enum lynx_lane_mode phy_interface_to_lane_mode(phy_interface_t intf)
|
|
{
|
|
switch (intf) {
|
|
case PHY_INTERFACE_MODE_SGMII:
|
|
case PHY_INTERFACE_MODE_1000BASEX:
|
|
return LANE_MODE_1000BASEX_SGMII;
|
|
case PHY_INTERFACE_MODE_2500BASEX:
|
|
return LANE_MODE_2500BASEX;
|
|
case PHY_INTERFACE_MODE_QSGMII:
|
|
return LANE_MODE_QSGMII;
|
|
case PHY_INTERFACE_MODE_10G_QXGMII:
|
|
return LANE_MODE_10G_QXGMII;
|
|
case PHY_INTERFACE_MODE_10GBASER:
|
|
return LANE_MODE_10GBASER;
|
|
case PHY_INTERFACE_MODE_USXGMII:
|
|
return LANE_MODE_USXGMII;
|
|
case PHY_INTERFACE_MODE_25GBASER:
|
|
return LANE_MODE_25GBASER;
|
|
default:
|
|
return LANE_MODE_UNKNOWN;
|
|
}
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(phy_interface_to_lane_mode, "PHY_FSL_LYNX");
|
|
|
|
/* By default, assume that if we know how to get the PCCR register and
|
|
* protocol converter for a lane, that protocol is supported.
|
|
*/
|
|
static bool lynx_lane_supports_mode_default(struct lynx_lane *lane,
|
|
enum lynx_lane_mode mode)
|
|
{
|
|
struct lynx_priv *priv = lane->priv;
|
|
struct lynx_pccr pccr;
|
|
|
|
if (!priv->info->get_pccr || !priv->info->get_pcvt_offset)
|
|
return false;
|
|
|
|
if (priv->info->get_pccr(mode, lane->id, &pccr) < 0)
|
|
return false;
|
|
|
|
if (priv->info->get_pcvt_offset(lane->id, mode) < 0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/* A lane mode is supported if we have a PLL that can provide its required
|
|
* clock net, and if there is a protocol converter for that mode on that lane.
|
|
*/
|
|
bool lynx_lane_supports_mode(struct lynx_lane *lane, enum lynx_lane_mode mode)
|
|
{
|
|
struct lynx_priv *priv = lane->priv;
|
|
int i;
|
|
|
|
if (priv->info->lane_supports_mode) {
|
|
if (!priv->info->lane_supports_mode(lane->id, mode))
|
|
return false;
|
|
} else if (!lynx_lane_supports_mode_default(lane, mode)) {
|
|
return false;
|
|
}
|
|
|
|
for (i = 0; i < LYNX_NUM_PLL; i++) {
|
|
if (!priv->pll[i].enabled)
|
|
continue;
|
|
|
|
if (test_bit(mode, priv->pll[i].supported))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_lane_supports_mode, "PHY_FSL_LYNX");
|
|
|
|
/* The quad protocols are fixed because the lane has multiple consumers, and
|
|
* one phy_set_mode_ext() affects the other consumers as well. We have no use
|
|
* case for dynamic protocol changing here, so disallow it.
|
|
*/
|
|
static enum lynx_lane_mode lynx_fixed_protocols[] = {
|
|
LANE_MODE_QSGMII,
|
|
LANE_MODE_10G_QXGMII,
|
|
};
|
|
|
|
static bool lynx_lane_restrict_fixed_mode_change(struct lynx_lane *lane,
|
|
enum lynx_lane_mode new)
|
|
{
|
|
enum lynx_lane_mode curr = lane->mode;
|
|
|
|
for (int i = 0; i < ARRAY_SIZE(lynx_fixed_protocols); i++)
|
|
if ((curr == lynx_fixed_protocols[i] ||
|
|
new == lynx_fixed_protocols[i]) &&
|
|
curr != new)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Translate the mode/submode from phy_validate() and phy_set_mode_ext() to a
|
|
* lane_mode and return 0 if it is supported and we can transition to it from
|
|
* the current lane mode, or return negative error otherwise.
|
|
*/
|
|
int lynx_phy_mode_to_lane_mode(struct phy *phy, enum phy_mode mode,
|
|
int submode, enum lynx_lane_mode *lane_mode)
|
|
{
|
|
struct lynx_lane *lane = phy_get_drvdata(phy);
|
|
enum lynx_lane_mode tmp_lane_mode;
|
|
|
|
/* The protocol configuration tables are incomplete for full lane
|
|
* reconfiguration from an arbitrary protocol.
|
|
*/
|
|
if (lane->mode == LANE_MODE_UNKNOWN)
|
|
return -EINVAL;
|
|
|
|
if (mode != PHY_MODE_ETHERNET)
|
|
return -EINVAL;
|
|
|
|
tmp_lane_mode = phy_interface_to_lane_mode(submode);
|
|
if (!lynx_lane_supports_mode(lane, tmp_lane_mode))
|
|
return -EINVAL;
|
|
|
|
if (lynx_lane_restrict_fixed_mode_change(lane, tmp_lane_mode))
|
|
return -EINVAL;
|
|
|
|
if (lane_mode)
|
|
*lane_mode = tmp_lane_mode;
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_phy_mode_to_lane_mode, "PHY_FSL_LYNX");
|
|
|
|
struct lynx_pll *lynx_pll_get(struct lynx_priv *priv, enum lynx_lane_mode mode)
|
|
{
|
|
struct lynx_pll *pll;
|
|
int i;
|
|
|
|
for (i = 0; i < LYNX_NUM_PLL; i++) {
|
|
pll = &priv->pll[i];
|
|
|
|
if (!pll->enabled)
|
|
continue;
|
|
|
|
if (test_bit(mode, pll->supported))
|
|
return pll;
|
|
}
|
|
|
|
/* no pll supports requested mode, either caller forgot to check
|
|
* lynx_lane_supports_mode(), or this is a bug.
|
|
*/
|
|
dev_WARN_ONCE(priv->dev, 1, "no pll for lane mode %s\n",
|
|
lynx_lane_mode_str(mode));
|
|
return NULL;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_pll_get, "PHY_FSL_LYNX");
|
|
|
|
int lynx_pccr_read(struct lynx_lane *lane, enum lynx_lane_mode mode, u32 *val)
|
|
{
|
|
struct lynx_priv *priv = lane->priv;
|
|
struct lynx_pccr pccr;
|
|
u32 tmp;
|
|
int err;
|
|
|
|
err = priv->info->get_pccr(mode, lane->id, &pccr);
|
|
if (err)
|
|
return err;
|
|
|
|
tmp = lynx_read(priv, pccr.offset);
|
|
*val = (tmp >> pccr.shift) & GENMASK(pccr.width - 1, 0);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_pccr_read, "PHY_FSL_LYNX");
|
|
|
|
int lynx_pccr_write(struct lynx_lane *lane, enum lynx_lane_mode mode, u32 val)
|
|
{
|
|
struct lynx_priv *priv = lane->priv;
|
|
struct lynx_pccr pccr;
|
|
u32 old, tmp, mask;
|
|
int err;
|
|
|
|
err = priv->info->get_pccr(mode, lane->id, &pccr);
|
|
if (err)
|
|
return err;
|
|
|
|
old = lynx_read(priv, pccr.offset);
|
|
mask = GENMASK(pccr.width - 1, 0) << pccr.shift;
|
|
tmp = (old & ~mask) | (val << pccr.shift);
|
|
lynx_write(priv, pccr.offset, tmp);
|
|
|
|
dev_dbg(&lane->phy->dev, "PCCR@0x%x: 0x%x -> 0x%x\n",
|
|
pccr.offset, old, tmp);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_pccr_write, "PHY_FSL_LYNX");
|
|
|
|
int lynx_pcvt_read(struct lynx_lane *lane, enum lynx_lane_mode mode, int cr,
|
|
u32 *val)
|
|
{
|
|
struct lynx_priv *priv = lane->priv;
|
|
int offset;
|
|
|
|
offset = priv->info->get_pcvt_offset(lane->id, mode);
|
|
if (offset < 0)
|
|
return offset;
|
|
|
|
*val = lynx_read(priv, offset + cr);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_pcvt_read, "PHY_FSL_LYNX");
|
|
|
|
int lynx_pcvt_write(struct lynx_lane *lane, enum lynx_lane_mode mode, int cr,
|
|
u32 val)
|
|
{
|
|
struct lynx_priv *priv = lane->priv;
|
|
int offset;
|
|
|
|
offset = priv->info->get_pcvt_offset(lane->id, mode);
|
|
if (offset < 0)
|
|
return offset;
|
|
|
|
lynx_write(priv, offset + cr, val);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_pcvt_write, "PHY_FSL_LYNX");
|
|
|
|
int lynx_pcvt_rmw(struct lynx_lane *lane, enum lynx_lane_mode mode, int cr,
|
|
u32 val, u32 mask)
|
|
{
|
|
int err;
|
|
u32 tmp;
|
|
|
|
err = lynx_pcvt_read(lane, mode, cr, &tmp);
|
|
if (err)
|
|
return err;
|
|
|
|
tmp &= ~mask;
|
|
tmp |= val;
|
|
|
|
return lynx_pcvt_write(lane, mode, cr, tmp);
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_pcvt_rmw, "PHY_FSL_LYNX");
|
|
|
|
#define work_to_lynx(w) container_of((w), struct lynx_priv, cdr_check.work)
|
|
|
|
static void lynx_cdr_lock_check(struct work_struct *work)
|
|
{
|
|
struct lynx_priv *priv = work_to_lynx(work);
|
|
struct lynx_lane *lane;
|
|
|
|
for (int i = priv->info->first_lane; i < priv->info->num_lanes; i++) {
|
|
lane = &priv->lane[i];
|
|
if (!lane->phy)
|
|
continue;
|
|
|
|
mutex_lock(&lane->phy->mutex);
|
|
|
|
if (!lane->init || !lane->powered_up) {
|
|
mutex_unlock(&lane->phy->mutex);
|
|
continue;
|
|
}
|
|
|
|
priv->info->cdr_lock_check(lane);
|
|
|
|
mutex_unlock(&lane->phy->mutex);
|
|
}
|
|
|
|
queue_delayed_work(system_power_efficient_wq, &priv->cdr_check,
|
|
msecs_to_jiffies(1000));
|
|
}
|
|
|
|
static struct phy *lynx_xlate(struct device *dev,
|
|
const struct of_phandle_args *args)
|
|
{
|
|
struct lynx_priv *priv = dev_get_drvdata(dev);
|
|
int idx;
|
|
|
|
if (args->args_count == 0)
|
|
return of_phy_simple_xlate(dev, args);
|
|
else if (args->args_count != 1)
|
|
return ERR_PTR(-ENODEV);
|
|
|
|
idx = args->args[0];
|
|
|
|
if (WARN_ON(idx >= priv->info->num_lanes ||
|
|
idx < priv->info->first_lane))
|
|
return ERR_PTR(-EINVAL);
|
|
|
|
return priv->lane[idx].phy ?: ERR_PTR(-ENODEV);
|
|
}
|
|
|
|
static int lynx_probe_lane(struct lynx_priv *priv, int id,
|
|
struct device_node *dn,
|
|
const struct phy_ops *phy_ops)
|
|
{
|
|
struct lynx_lane *lane = &priv->lane[id];
|
|
struct phy *phy;
|
|
|
|
phy = devm_phy_create(priv->dev, dn, phy_ops);
|
|
if (IS_ERR(phy))
|
|
return PTR_ERR(phy);
|
|
|
|
lane->priv = priv;
|
|
lane->phy = phy;
|
|
lane->id = id;
|
|
phy_set_drvdata(phy, lane);
|
|
priv->info->lane_read_configuration(lane);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int lynx_probe(struct platform_device *pdev, const struct lynx_info *info,
|
|
const struct phy_ops *phy_ops)
|
|
{
|
|
struct device *dev = &pdev->dev;
|
|
struct phy_provider *provider;
|
|
struct device_node *dn;
|
|
struct lynx_priv *priv;
|
|
int err;
|
|
|
|
dn = dev_of_node(dev);
|
|
if (!dn) {
|
|
dev_err(dev, "Device requires an OF node\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!info)
|
|
return -ENODEV;
|
|
|
|
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
|
|
if (!priv)
|
|
return -ENOMEM;
|
|
|
|
priv->dev = dev;
|
|
priv->info = info;
|
|
priv->big_endian = device_property_read_bool(dev, "big-endian");
|
|
dev_set_drvdata(dev, priv);
|
|
spin_lock_init(&priv->pcc_lock);
|
|
INIT_DELAYED_WORK(&priv->cdr_check, lynx_cdr_lock_check);
|
|
|
|
priv->lane = devm_kcalloc(dev, priv->info->num_lanes,
|
|
sizeof(*priv->lane), GFP_KERNEL);
|
|
if (!priv->lane)
|
|
return -ENOMEM;
|
|
|
|
priv->base = devm_platform_ioremap_resource(pdev, 0);
|
|
if (IS_ERR(priv->base))
|
|
return PTR_ERR(priv->base);
|
|
|
|
for (int i = 0; i < LYNX_NUM_PLL; i++) {
|
|
struct lynx_pll *pll = &priv->pll[i];
|
|
|
|
pll->priv = priv;
|
|
pll->id = i;
|
|
priv->info->pll_read_configuration(pll);
|
|
}
|
|
|
|
if (of_get_child_count(dn)) {
|
|
struct device_node *child;
|
|
|
|
for_each_available_child_of_node(dn, child) {
|
|
u32 reg;
|
|
|
|
/* PHY subnode name must be 'phy'. */
|
|
if (!(of_node_name_eq(child, "phy")))
|
|
continue;
|
|
|
|
if (of_property_read_u32(child, "reg", ®)) {
|
|
dev_err(dev, "No \"reg\" property for %pOF\n", child);
|
|
of_node_put(child);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (reg < priv->info->first_lane || reg >= priv->info->num_lanes) {
|
|
dev_err(dev, "\"reg\" property out of range for %pOF\n", child);
|
|
of_node_put(child);
|
|
return -EINVAL;
|
|
}
|
|
|
|
err = lynx_probe_lane(priv, reg, child, phy_ops);
|
|
if (err) {
|
|
of_node_put(child);
|
|
return err;
|
|
}
|
|
}
|
|
} else {
|
|
for (int i = priv->info->first_lane; i < priv->info->num_lanes; i++) {
|
|
err = lynx_probe_lane(priv, i, NULL, phy_ops);
|
|
if (err)
|
|
return err;
|
|
}
|
|
}
|
|
|
|
provider = devm_of_phy_provider_register(dev, lynx_xlate);
|
|
if (IS_ERR(provider))
|
|
return PTR_ERR(provider);
|
|
|
|
queue_delayed_work(system_power_efficient_wq, &priv->cdr_check,
|
|
msecs_to_jiffies(1000));
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_probe, "PHY_FSL_LYNX");
|
|
|
|
void lynx_remove(struct platform_device *pdev)
|
|
{
|
|
struct device *dev = &pdev->dev;
|
|
struct lynx_priv *priv = dev_get_drvdata(dev);
|
|
|
|
cancel_delayed_work_sync(&priv->cdr_check);
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(lynx_remove, "PHY_FSL_LYNX");
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_DESCRIPTION("Freescale Lynx SerDes core functionality");
|