mirror of
https://github.com/torvalds/linux.git
synced 2026-01-12 00:42:35 +08:00
Testing in two circumstances: 1. back to back optical SFP+ connection between two LS1028A-QDS ports with the SCH-26908 riser card 2. T1042 with on-board AQR115 PHY using "OCSGMII", as per https://lore.kernel.org/lkml/aIuEvaSCIQdJWcZx@FUE-ALEWI-WINX/ strongly suggests that enabling in-band auto-negotiation is actually possible when the lane baud rate is 3.125 Gbps. It was previously thought that this would not be the case, because it was only tested on 2500base-x links with on-board Aquantia PHYs, where it was noticed that MII_LPA is always reported as zero, and it was thought that this is because of the PCS. Test case #1 above shows it is not, and the configured MII_ADVERTISE on system A ends up in the MII_LPA on system B, when in 2500base-x mode (IF_MODE=0). Test case #2, which uses "SGMII" auto-negotiation (IF_MODE=3) for the 3.125 Gbps lane, is actually a misconfiguration, but it is what led to the discovery. There is actually an old bug in the Lynx PCS driver - it expects all register values to contain their default out-of-reset values, as if the PCS were initialized by the Reset Configuration Word (RCW) settings. There are 2 cases in which this is problematic: - if the bootloader (or previous kexec-enabled Linux) wrote a different IF_MODE value - if dynamically changing the SerDes protocol from 1000base-x to 2500base-x, e.g. by replacing the optical SFP module. Specifically in test case #2, an accidental alignment between the bootloader configuring the PCS to expect SGMII in-band code words, and the AQR115 PHY actually transmitting SGMII in-band code words when operating in the "OCSGMII" system interface protocol, led to the PCS transmitting replicated symbols at 3.125 Gbps baud rate. This could only have happened if the PCS saw and reacted to the SGMII code words in the first place. Since test #2 is invalid from a protocol perspective (there seems to be no standard way of negotiating the data rate of 2500 Mbps with SGMII, and the lower data rates should remain 10/100/1000), in-band auto-negotiation for 2500base-x effectively means Clause 37 (i.e. IF_MODE=0). Make 2500base-x be treated like 1000base-x in this regard, by removing all prior limitations and calling lynx_pcs_config_giga(). This adds a new feature: LINK_INBAND_ENABLE and at the same time fixes the Lynx PCS's long standing problem that the registers (specifically IF_MODE, but others could be misconfigured as well) are not written by the driver to the known valid values for 2500base-x. Co-developed-by: Alexander Wilhelm <alexander.wilhelm@westermo.com> Signed-off-by: Alexander Wilhelm <alexander.wilhelm@westermo.com> Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com> Link: https://patch.msgid.link/20251125103507.749654-1-vladimir.oltean@nxp.com Signed-off-by: Jakub Kicinski <kuba@kernel.org>
376 lines
9.5 KiB
C
376 lines
9.5 KiB
C
// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
|
|
/* Copyright 2020 NXP
|
|
* Lynx PCS MDIO helpers
|
|
*/
|
|
|
|
#include <linux/mdio.h>
|
|
#include <linux/phylink.h>
|
|
#include <linux/pcs-lynx.h>
|
|
#include <linux/property.h>
|
|
|
|
#define SGMII_CLOCK_PERIOD_NS 8 /* PCS is clocked at 125 MHz */
|
|
#define LINK_TIMER_VAL(ns) ((u32)((ns) / SGMII_CLOCK_PERIOD_NS))
|
|
|
|
#define LINK_TIMER_LO 0x12
|
|
#define LINK_TIMER_HI 0x13
|
|
#define IF_MODE 0x14
|
|
#define IF_MODE_SGMII_EN BIT(0)
|
|
#define IF_MODE_USE_SGMII_AN BIT(1)
|
|
#define IF_MODE_SPEED(x) (((x) << 2) & GENMASK(3, 2))
|
|
#define IF_MODE_SPEED_MSK GENMASK(3, 2)
|
|
#define IF_MODE_HALF_DUPLEX BIT(4)
|
|
|
|
struct lynx_pcs {
|
|
struct phylink_pcs pcs;
|
|
struct mdio_device *mdio;
|
|
};
|
|
|
|
enum sgmii_speed {
|
|
SGMII_SPEED_10 = 0,
|
|
SGMII_SPEED_100 = 1,
|
|
SGMII_SPEED_1000 = 2,
|
|
SGMII_SPEED_2500 = 2,
|
|
};
|
|
|
|
#define phylink_pcs_to_lynx(pl_pcs) container_of((pl_pcs), struct lynx_pcs, pcs)
|
|
#define lynx_to_phylink_pcs(lynx) (&(lynx)->pcs)
|
|
|
|
static unsigned int lynx_pcs_inband_caps(struct phylink_pcs *pcs,
|
|
phy_interface_t interface)
|
|
{
|
|
switch (interface) {
|
|
case PHY_INTERFACE_MODE_1000BASEX:
|
|
case PHY_INTERFACE_MODE_2500BASEX:
|
|
case PHY_INTERFACE_MODE_SGMII:
|
|
case PHY_INTERFACE_MODE_QSGMII:
|
|
return LINK_INBAND_DISABLE | LINK_INBAND_ENABLE;
|
|
|
|
case PHY_INTERFACE_MODE_10GBASER:
|
|
return LINK_INBAND_DISABLE;
|
|
|
|
case PHY_INTERFACE_MODE_USXGMII:
|
|
case PHY_INTERFACE_MODE_10G_QXGMII:
|
|
return LINK_INBAND_ENABLE;
|
|
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static void lynx_pcs_get_state_usxgmii(struct mdio_device *pcs,
|
|
struct phylink_link_state *state)
|
|
{
|
|
struct mii_bus *bus = pcs->bus;
|
|
int addr = pcs->addr;
|
|
int status, lpa;
|
|
|
|
status = mdiobus_c45_read(bus, addr, MDIO_MMD_VEND2, MII_BMSR);
|
|
if (status < 0)
|
|
return;
|
|
|
|
state->link = !!(status & MDIO_STAT1_LSTATUS);
|
|
state->an_complete = !!(status & MDIO_AN_STAT1_COMPLETE);
|
|
if (!state->link || !state->an_complete)
|
|
return;
|
|
|
|
lpa = mdiobus_c45_read(bus, addr, MDIO_MMD_VEND2, MII_LPA);
|
|
if (lpa < 0)
|
|
return;
|
|
|
|
phylink_decode_usxgmii_word(state, lpa);
|
|
}
|
|
|
|
static void lynx_pcs_get_state(struct phylink_pcs *pcs, unsigned int neg_mode,
|
|
struct phylink_link_state *state)
|
|
{
|
|
struct lynx_pcs *lynx = phylink_pcs_to_lynx(pcs);
|
|
|
|
switch (state->interface) {
|
|
case PHY_INTERFACE_MODE_1000BASEX:
|
|
case PHY_INTERFACE_MODE_2500BASEX:
|
|
case PHY_INTERFACE_MODE_SGMII:
|
|
case PHY_INTERFACE_MODE_QSGMII:
|
|
phylink_mii_c22_pcs_get_state(lynx->mdio, neg_mode, state);
|
|
break;
|
|
case PHY_INTERFACE_MODE_USXGMII:
|
|
case PHY_INTERFACE_MODE_10G_QXGMII:
|
|
lynx_pcs_get_state_usxgmii(lynx->mdio, state);
|
|
break;
|
|
case PHY_INTERFACE_MODE_10GBASER:
|
|
phylink_mii_c45_pcs_get_state(lynx->mdio, state);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
dev_dbg(&lynx->mdio->dev,
|
|
"mode=%s/%s/%s link=%u an_complete=%u\n",
|
|
phy_modes(state->interface),
|
|
phy_speed_to_str(state->speed),
|
|
phy_duplex_to_str(state->duplex),
|
|
state->link, state->an_complete);
|
|
}
|
|
|
|
static int lynx_pcs_config_giga(struct mdio_device *pcs,
|
|
phy_interface_t interface,
|
|
const unsigned long *advertising,
|
|
unsigned int neg_mode)
|
|
{
|
|
int link_timer_ns;
|
|
u32 link_timer;
|
|
u16 if_mode;
|
|
int err;
|
|
|
|
link_timer_ns = phylink_get_link_timer_ns(interface);
|
|
if (link_timer_ns > 0) {
|
|
link_timer = LINK_TIMER_VAL(link_timer_ns);
|
|
|
|
mdiodev_write(pcs, LINK_TIMER_LO, link_timer & 0xffff);
|
|
mdiodev_write(pcs, LINK_TIMER_HI, link_timer >> 16);
|
|
}
|
|
|
|
if (interface == PHY_INTERFACE_MODE_1000BASEX ||
|
|
interface == PHY_INTERFACE_MODE_2500BASEX) {
|
|
if_mode = 0;
|
|
} else {
|
|
/* SGMII and QSGMII */
|
|
if_mode = IF_MODE_SGMII_EN;
|
|
if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
|
|
if_mode |= IF_MODE_USE_SGMII_AN;
|
|
}
|
|
|
|
err = mdiodev_modify(pcs, IF_MODE,
|
|
IF_MODE_SGMII_EN | IF_MODE_USE_SGMII_AN,
|
|
if_mode);
|
|
if (err)
|
|
return err;
|
|
|
|
return phylink_mii_c22_pcs_config(pcs, interface, advertising,
|
|
neg_mode);
|
|
}
|
|
|
|
static int lynx_pcs_config_usxgmii(struct mdio_device *pcs,
|
|
phy_interface_t interface,
|
|
const unsigned long *advertising,
|
|
unsigned int neg_mode)
|
|
{
|
|
struct mii_bus *bus = pcs->bus;
|
|
int addr = pcs->addr;
|
|
|
|
if (neg_mode != PHYLINK_PCS_NEG_INBAND_ENABLED) {
|
|
dev_err(&pcs->dev, "%s only supports in-band AN for now\n",
|
|
phy_modes(interface));
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
/* Configure device ability for the USXGMII Replicator */
|
|
return mdiobus_c45_write(bus, addr, MDIO_MMD_VEND2, MII_ADVERTISE,
|
|
MDIO_USXGMII_10G | MDIO_USXGMII_LINK |
|
|
MDIO_USXGMII_FULL_DUPLEX |
|
|
ADVERTISE_SGMII | ADVERTISE_LPACK);
|
|
}
|
|
|
|
static int lynx_pcs_config(struct phylink_pcs *pcs, unsigned int neg_mode,
|
|
phy_interface_t ifmode,
|
|
const unsigned long *advertising, bool permit)
|
|
{
|
|
struct lynx_pcs *lynx = phylink_pcs_to_lynx(pcs);
|
|
|
|
switch (ifmode) {
|
|
case PHY_INTERFACE_MODE_1000BASEX:
|
|
case PHY_INTERFACE_MODE_SGMII:
|
|
case PHY_INTERFACE_MODE_QSGMII:
|
|
case PHY_INTERFACE_MODE_2500BASEX:
|
|
return lynx_pcs_config_giga(lynx->mdio, ifmode, advertising,
|
|
neg_mode);
|
|
case PHY_INTERFACE_MODE_USXGMII:
|
|
case PHY_INTERFACE_MODE_10G_QXGMII:
|
|
return lynx_pcs_config_usxgmii(lynx->mdio, ifmode, advertising,
|
|
neg_mode);
|
|
case PHY_INTERFACE_MODE_10GBASER:
|
|
/* Nothing to do here for 10GBASER */
|
|
break;
|
|
default:
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void lynx_pcs_an_restart(struct phylink_pcs *pcs)
|
|
{
|
|
struct lynx_pcs *lynx = phylink_pcs_to_lynx(pcs);
|
|
|
|
phylink_mii_c22_pcs_an_restart(lynx->mdio);
|
|
}
|
|
|
|
static void lynx_pcs_link_up_sgmii(struct mdio_device *pcs,
|
|
unsigned int neg_mode,
|
|
int speed, int duplex)
|
|
{
|
|
u16 if_mode = 0, sgmii_speed;
|
|
|
|
/* The PCS needs to be configured manually only
|
|
* when not operating on in-band mode
|
|
*/
|
|
if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED)
|
|
return;
|
|
|
|
if (duplex == DUPLEX_HALF)
|
|
if_mode |= IF_MODE_HALF_DUPLEX;
|
|
|
|
switch (speed) {
|
|
case SPEED_1000:
|
|
sgmii_speed = SGMII_SPEED_1000;
|
|
break;
|
|
case SPEED_100:
|
|
sgmii_speed = SGMII_SPEED_100;
|
|
break;
|
|
case SPEED_10:
|
|
sgmii_speed = SGMII_SPEED_10;
|
|
break;
|
|
case SPEED_UNKNOWN:
|
|
/* Silently don't do anything */
|
|
return;
|
|
default:
|
|
dev_err(&pcs->dev, "Invalid PCS speed %d\n", speed);
|
|
return;
|
|
}
|
|
if_mode |= IF_MODE_SPEED(sgmii_speed);
|
|
|
|
mdiodev_modify(pcs, IF_MODE,
|
|
IF_MODE_HALF_DUPLEX | IF_MODE_SPEED_MSK,
|
|
if_mode);
|
|
}
|
|
|
|
static void lynx_pcs_link_up(struct phylink_pcs *pcs, unsigned int neg_mode,
|
|
phy_interface_t interface,
|
|
int speed, int duplex)
|
|
{
|
|
struct lynx_pcs *lynx = phylink_pcs_to_lynx(pcs);
|
|
|
|
switch (interface) {
|
|
case PHY_INTERFACE_MODE_SGMII:
|
|
case PHY_INTERFACE_MODE_QSGMII:
|
|
lynx_pcs_link_up_sgmii(lynx->mdio, neg_mode, speed, duplex);
|
|
break;
|
|
case PHY_INTERFACE_MODE_USXGMII:
|
|
case PHY_INTERFACE_MODE_10G_QXGMII:
|
|
/* At the moment, only in-band AN is supported for USXGMII
|
|
* so nothing to do in link_up
|
|
*/
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static const struct phylink_pcs_ops lynx_pcs_phylink_ops = {
|
|
.pcs_inband_caps = lynx_pcs_inband_caps,
|
|
.pcs_get_state = lynx_pcs_get_state,
|
|
.pcs_config = lynx_pcs_config,
|
|
.pcs_an_restart = lynx_pcs_an_restart,
|
|
.pcs_link_up = lynx_pcs_link_up,
|
|
};
|
|
|
|
static const phy_interface_t lynx_interfaces[] = {
|
|
PHY_INTERFACE_MODE_SGMII,
|
|
PHY_INTERFACE_MODE_QSGMII,
|
|
PHY_INTERFACE_MODE_1000BASEX,
|
|
PHY_INTERFACE_MODE_2500BASEX,
|
|
PHY_INTERFACE_MODE_10GBASER,
|
|
PHY_INTERFACE_MODE_USXGMII,
|
|
PHY_INTERFACE_MODE_10G_QXGMII,
|
|
};
|
|
|
|
static struct phylink_pcs *lynx_pcs_create(struct mdio_device *mdio)
|
|
{
|
|
struct lynx_pcs *lynx;
|
|
int i;
|
|
|
|
lynx = kzalloc(sizeof(*lynx), GFP_KERNEL);
|
|
if (!lynx)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
mdio_device_get(mdio);
|
|
lynx->mdio = mdio;
|
|
lynx->pcs.ops = &lynx_pcs_phylink_ops;
|
|
lynx->pcs.poll = true;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(lynx_interfaces); i++)
|
|
__set_bit(lynx_interfaces[i], lynx->pcs.supported_interfaces);
|
|
|
|
return lynx_to_phylink_pcs(lynx);
|
|
}
|
|
|
|
struct phylink_pcs *lynx_pcs_create_mdiodev(struct mii_bus *bus, int addr)
|
|
{
|
|
struct mdio_device *mdio;
|
|
struct phylink_pcs *pcs;
|
|
|
|
mdio = mdio_device_create(bus, addr);
|
|
if (IS_ERR(mdio))
|
|
return ERR_CAST(mdio);
|
|
|
|
pcs = lynx_pcs_create(mdio);
|
|
|
|
/* lynx_create() has taken a refcount on the mdiodev if it was
|
|
* successful. If lynx_create() fails, this will free the mdio
|
|
* device here. In any case, we don't need to hold our reference
|
|
* anymore, and putting it here will allow mdio_device_put() in
|
|
* lynx_destroy() to automatically free the mdio device.
|
|
*/
|
|
mdio_device_put(mdio);
|
|
|
|
return pcs;
|
|
}
|
|
EXPORT_SYMBOL(lynx_pcs_create_mdiodev);
|
|
|
|
/*
|
|
* lynx_pcs_create_fwnode() creates a lynx PCS instance from the fwnode
|
|
* device indicated by node.
|
|
*
|
|
* Returns:
|
|
* -ENODEV if the fwnode is marked unavailable
|
|
* -EPROBE_DEFER if we fail to find the device
|
|
* -ENOMEM if we fail to allocate memory
|
|
* pointer to a phylink_pcs on success
|
|
*/
|
|
struct phylink_pcs *lynx_pcs_create_fwnode(struct fwnode_handle *node)
|
|
{
|
|
struct mdio_device *mdio;
|
|
struct phylink_pcs *pcs;
|
|
|
|
if (!fwnode_device_is_available(node))
|
|
return ERR_PTR(-ENODEV);
|
|
|
|
mdio = fwnode_mdio_find_device(node);
|
|
if (!mdio)
|
|
return ERR_PTR(-EPROBE_DEFER);
|
|
|
|
pcs = lynx_pcs_create(mdio);
|
|
|
|
/* lynx_create() has taken a refcount on the mdiodev if it was
|
|
* successful. If lynx_create() fails, this will free the mdio
|
|
* device here. In any case, we don't need to hold our reference
|
|
* anymore, and putting it here will allow mdio_device_put() in
|
|
* lynx_destroy() to automatically free the mdio device.
|
|
*/
|
|
mdio_device_put(mdio);
|
|
|
|
return pcs;
|
|
}
|
|
EXPORT_SYMBOL_GPL(lynx_pcs_create_fwnode);
|
|
|
|
void lynx_pcs_destroy(struct phylink_pcs *pcs)
|
|
{
|
|
struct lynx_pcs *lynx = phylink_pcs_to_lynx(pcs);
|
|
|
|
mdio_device_put(lynx->mdio);
|
|
kfree(lynx);
|
|
}
|
|
EXPORT_SYMBOL(lynx_pcs_destroy);
|
|
|
|
MODULE_DESCRIPTION("NXP Lynx PCS phylink library");
|
|
MODULE_LICENSE("Dual BSD/GPL");
|