1
1

realtek: migrate PCS to fwnode_pcs provider/consumer API

PCS driver registers each SerDes as an fwnode_pcs provider in probe;
the resolver returns the cached or freshly-allocated rtpcs_link for
the requested (sds, link_idx) cell. DSA glue stops calling
rtpcs_create directly, drops .mac_select_pcs, and instead populates
phylink_config.num_available_pcs / fill_available_pcs from each
port's pcs-handle in phylink_get_caps. The rtl838x_port.pcs pointer
becomes a has_pcs bool populated at port probe via fwnode_property_
present, since nothing assigns the actual phylink_pcs anymore but the
"does this port use a PCS?" checks elsewhere still need a presence
flag.

Without .mac_select_pcs, phylink_major_config only searches the
pcs_list when state->interface is set in phylink_config.pcs_interfaces
(drivers/net/phy/phylink.c:1378). Populate it per port whenever the
port has a pcs-handle, listing the SerDes-routable interface modes for
each SoC variant -- without this, pcs_config / pcs_link_up are never
called and the SerDes is left unconfigured.

pcs_get_state still needs the MAC port number to index per-port link
status registers. Recover it at probe via rtpcs_map_links: walk the
sibling switch's ethernet-ports subtree (same backwards topology
lookup the sibling MDIO driver does for phy-handle), and for every
port whose pcs-handle resolves to one of our SerDes, store the port's
reg in sds->link_port[]. The resolver consults link_port[] when
allocating rtpcs_link and fails with -ENODEV if a consumer requested
a link the map step didn't record. Avoids a driver-side port_base
table that would have to encode per-SoC SerDes-to-port wiring (and
would silently break on non-contiguous variants); the DT is the
single source of truth.

Kconfig selects FWNODE_PCS.

Assisted-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Link: https://github.com/openwrt/openwrt/pull/23539
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
This commit is contained in:
Jonas Jelonek 2026-05-25 21:58:14 +00:00
parent c4f5c34fa4
commit a4965dbf48
No known key found for this signature in database
7 changed files with 184 additions and 61 deletions

View File

@ -255,8 +255,6 @@ static bool rtldsa_phys_load_deferred(void)
static int rtl83xx_mdio_probe(struct rtl838x_switch_priv *priv)
{
struct device_node *dn, *phy_node, *led_node;
struct of_phandle_args pcs_args;
bool has_pcs;
u32 pn;
/* Check if all busses of Realtek mdio controller are registered */
@ -294,26 +292,14 @@ static int rtl83xx_mdio_probe(struct rtl838x_switch_priv *priv)
if (of_property_read_u32(dn, "reg", &pn))
continue;
has_pcs = !of_parse_phandle_with_args(dn, "pcs-handle", "#pcs-cells",
0, &pcs_args);
phy_node = of_parse_phandle(dn, "phy-handle", 0);
if (pn != priv->r->cpu_port && !phy_node && !has_pcs) {
priv->ports[pn].has_pcs = fwnode_property_present(of_fwnode_handle(dn),
"pcs-handle");
if (pn != priv->r->cpu_port && !phy_node && !priv->ports[pn].has_pcs) {
dev_err(priv->dev, "Port node %d has neither pcs-handle nor phy-handle\n", pn);
continue;
}
if (has_pcs) {
priv->ports[pn].pcs = rtpcs_create(priv->dev, pcs_args.np,
pcs_args.args[0], pn);
of_node_put(pcs_args.np);
if (IS_ERR(priv->ports[pn].pcs)) {
dev_err(priv->dev, "port %u failed to create PCS instance: %ld\n",
pn, PTR_ERR(priv->ports[pn].pcs));
priv->ports[pn].pcs = NULL;
continue;
}
}
priv->ports[pn].leds_on_this_port = 0;
if (led_node) {
if (of_property_read_u32(dn, "led-set", &led_set))

View File

@ -3,6 +3,7 @@
#include <net/dsa.h>
#include <linux/etherdevice.h>
#include <linux/if_bridge.h>
#include <linux/pcs/pcs.h>
#include <asm/mach-rtl-otto/mach-rtl-otto.h>
#include "rtl-otto.h"
@ -50,7 +51,7 @@ static void rtldsa_enable_phy_polling(struct rtl838x_switch_priv *priv)
msleep(1000);
/* Enable all ports with a PHY, including the SFP-ports */
for (int i = 0; i < priv->r->cpu_port; i++) {
if (priv->ports[i].phy || priv->ports[i].pcs)
if (priv->ports[i].phy || priv->ports[i].has_pcs)
v |= BIT_ULL(i);
}
@ -180,7 +181,7 @@ static int rtldsa_83xx_setup(struct dsa_switch *ds)
* they will work in isolated mode (only traffic between port and CPU).
*/
for (int i = 0; i < priv->r->cpu_port; i++) {
if (priv->ports[i].phy || priv->ports[i].pcs) {
if (priv->ports[i].phy || priv->ports[i].has_pcs) {
priv->ports[i].pm = BIT_ULL(priv->r->cpu_port);
priv->r->traffic_set(i, BIT_ULL(i));
}
@ -253,7 +254,7 @@ static int rtldsa_93xx_setup(struct dsa_switch *ds)
* they will work in isolated mode (only traffic between port and CPU).
*/
for (int i = 0; i < priv->r->cpu_port; i++) {
if (priv->ports[i].phy || priv->ports[i].pcs) {
if (priv->ports[i].phy || priv->ports[i].has_pcs) {
priv->ports[i].pm = BIT_ULL(priv->r->cpu_port);
priv->r->traffic_set(i, BIT_ULL(i));
}
@ -285,18 +286,21 @@ static int rtldsa_93xx_setup(struct dsa_switch *ds)
return 0;
}
static struct phylink_pcs *rtldsa_phylink_mac_select_pcs(struct phylink_config *config,
phy_interface_t interface)
static int rtldsa_phylink_fill_available_pcs(struct phylink_config *config,
struct phylink_pcs **available_pcs,
unsigned int num_available_pcs)
{
struct dsa_port *dp = dsa_phylink_to_port(config);
struct rtl838x_switch_priv *priv = dp->ds->priv;
return priv->ports[dp->index].pcs;
return fwnode_phylink_pcs_parse(of_fwnode_handle(dp->dn),
available_pcs, &num_available_pcs);
}
static void rtldsa_83xx_phylink_get_caps(struct dsa_switch *ds, int port,
struct phylink_config *config)
{
struct dsa_port *dp = dsa_to_port(ds, port);
/*
* TODO: This needs to take into account the MAC to SERDES mapping and the
* specific SoC capabilities. Right now we just assume all RTL83xx ports
@ -311,11 +315,21 @@ static void rtldsa_83xx_phylink_get_caps(struct dsa_switch *ds, int port,
__set_bit(PHY_INTERFACE_MODE_INTERNAL, config->supported_interfaces);
__set_bit(PHY_INTERFACE_MODE_SGMII, config->supported_interfaces);
__set_bit(PHY_INTERFACE_MODE_QSGMII, config->supported_interfaces);
if (!fwnode_phylink_pcs_parse(of_fwnode_handle(dp->dn), NULL,
&config->num_available_pcs)) {
config->fill_available_pcs = rtldsa_phylink_fill_available_pcs;
__set_bit(PHY_INTERFACE_MODE_SGMII, config->pcs_interfaces);
__set_bit(PHY_INTERFACE_MODE_QSGMII, config->pcs_interfaces);
__set_bit(PHY_INTERFACE_MODE_1000BASEX, config->pcs_interfaces);
}
}
static void rtldsa_93xx_phylink_get_caps(struct dsa_switch *ds, int port,
struct phylink_config *config)
{
struct dsa_port *dp = dsa_to_port(ds, port);
/*
* TODO: This needs to take into account the MAC to SERDES mapping and the
* specific SoC capabilities. Right now we just assume all RTL93xx ports
@ -334,6 +348,18 @@ static void rtldsa_93xx_phylink_get_caps(struct dsa_switch *ds, int port,
__set_bit(PHY_INTERFACE_MODE_2500BASEX, config->supported_interfaces);
__set_bit(PHY_INTERFACE_MODE_USXGMII, config->supported_interfaces);
__set_bit(PHY_INTERFACE_MODE_10G_QXGMII, config->supported_interfaces);
if (!fwnode_phylink_pcs_parse(of_fwnode_handle(dp->dn), NULL,
&config->num_available_pcs)) {
config->fill_available_pcs = rtldsa_phylink_fill_available_pcs;
__set_bit(PHY_INTERFACE_MODE_SGMII, config->pcs_interfaces);
__set_bit(PHY_INTERFACE_MODE_QSGMII, config->pcs_interfaces);
__set_bit(PHY_INTERFACE_MODE_1000BASEX, config->pcs_interfaces);
__set_bit(PHY_INTERFACE_MODE_2500BASEX, config->pcs_interfaces);
__set_bit(PHY_INTERFACE_MODE_USXGMII, config->pcs_interfaces);
__set_bit(PHY_INTERFACE_MODE_10GBASER, config->pcs_interfaces);
__set_bit(PHY_INTERFACE_MODE_10G_QXGMII, config->pcs_interfaces);
}
}
static void rtldsa_83xx_phylink_mac_config(struct phylink_config *config,
@ -776,7 +802,7 @@ static void rtldsa_poll_counters(struct work_struct *work)
counters_work);
for (int port = 0; port < priv->r->cpu_port; port++) {
if (!priv->ports[port].phy && !priv->ports[port].pcs)
if (!priv->ports[port].phy && !priv->ports[port].has_pcs)
continue;
rtldsa_counters_lock(priv, port);
@ -793,7 +819,7 @@ static void rtldsa_init_counters(struct rtl838x_switch_priv *priv)
struct rtldsa_counter_state *counters;
for (int port = 0; port < priv->r->cpu_port; port++) {
if (!priv->ports[port].phy && !priv->ports[port].pcs)
if (!priv->ports[port].phy && !priv->ports[port].has_pcs)
continue;
counters = &priv->ports[port].counters;
@ -2593,7 +2619,6 @@ unlock:
}
const struct phylink_mac_ops rtldsa_83xx_phylink_mac_ops = {
.mac_select_pcs = rtldsa_phylink_mac_select_pcs,
.mac_config = rtldsa_83xx_phylink_mac_config,
.mac_link_down = rtldsa_83xx_phylink_mac_link_down,
.mac_link_up = rtldsa_83xx_phylink_mac_link_up,
@ -2653,7 +2678,6 @@ const struct dsa_switch_ops rtldsa_83xx_switch_ops = {
};
const struct phylink_mac_ops rtldsa_93xx_phylink_mac_ops = {
.mac_select_pcs = rtldsa_phylink_mac_select_pcs,
.mac_config = rtldsa_93xx_phylink_mac_config,
.mac_link_down = rtldsa_93xx_phylink_mac_link_down,
.mac_link_up = rtldsa_93xx_phylink_mac_link_up,

View File

@ -1003,7 +1003,7 @@ struct rtldsa_port {
u64 pm;
u16 pvid;
bool eee_enabled;
struct phylink_pcs *pcs;
bool has_pcs;
int led_set;
int leds_on_this_port;
struct rtldsa_counter_state counters;

View File

@ -2668,7 +2668,7 @@ static void rtl930x_led_init(struct rtl838x_switch_priv *priv)
sw_w32_mask(0x3 << pos, 0, RTL930X_LED_PORT_FIB_SET_SEL_CTRL(i));
sw_w32_mask(0x3 << pos, 0, RTL930X_LED_PORT_COPR_SET_SEL_CTRL(i));
if (!priv->ports[i].phy && !priv->ports[i].pcs && !(forced_leds_per_port[i]))
if (!priv->ports[i].phy && !priv->ports[i].has_pcs && !(forced_leds_per_port[i]))
continue;
if (forced_leds_per_port[i] > 0)

View File

@ -1660,7 +1660,7 @@ static void rtldsa_931x_led_init(struct rtl838x_switch_priv *priv)
sw_w32_mask(0x3 << pos, 0, RTL931X_LED_PORT_COPR_SET_SEL_CTRL(i));
/* Skip port if not present (auto-detect) or not in forced mask */
if (!priv->ports[i].phy && !priv->ports[i].pcs && !(forced_leds_per_port[i]))
if (!priv->ports[i].phy && !priv->ports[i].has_pcs && !(forced_leds_per_port[i]))
continue;
if (forced_leds_per_port[i] > 0)

View File

@ -5,11 +5,14 @@
#include <linux/of.h>
#include <linux/of_mdio.h>
#include <linux/of_platform.h>
#include <linux/pcs/pcs-provider.h>
#include <linux/phy.h>
#include <linux/phy/phy-common-props.h>
#include <linux/phylink.h>
#include <linux/platform_device.h>
#include <linux/property.h>
#include <linux/regmap.h>
#include <linux/rtnetlink.h>
#define RTPCS_SDS_CNT 14
#define RTPCS_MAX_LINKS_PER_SDS 8
@ -4241,39 +4244,133 @@ static void rtpcs_sds_put_fwnode(void *data)
fwnode_handle_put(sds->fwnode);
}
static void rtpcs_count_links(struct rtpcs_ctrl *ctrl)
static void rtpcs_del_provider_action(void *data)
{
struct device_node *consumer __free(device_node) = NULL;
struct of_phandle_args args;
struct rtpcs_serdes *sds = data;
for_each_node_with_property(consumer, "pcs-handle") {
int idx = 0;
fwnode_pcs_del_provider(sds->fwnode);
if (!of_device_is_available(consumer))
rtnl_lock();
for (int i = 0; i < RTPCS_MAX_LINKS_PER_SDS; i++) {
if (!sds->link[i])
continue;
while (!of_parse_phandle_with_args(consumer, "pcs-handle",
"#pcs-cells", idx++, &args)) {
struct device_node *arg_np __free(device_node) = args.np;
for (int s = 0; s < ctrl->cfg->serdes_count; s++) {
struct rtpcs_serdes *sds = &ctrl->serdes[s];
if (of_fwnode_handle(arg_np) != sds->fwnode)
continue;
if (sds->num_of_links >= RTPCS_MAX_LINKS_PER_SDS) {
dev_warn(ctrl->dev,
"%pOF: pcs-handle to sds%u exceeds max %u, clamping\n",
consumer, sds->id, RTPCS_MAX_LINKS_PER_SDS);
break;
phylink_release_pcs(&sds->link[i]->pcs);
}
rtnl_unlock();
}
static struct rtpcs_serdes *rtpcs_find_serdes(struct rtpcs_ctrl *ctrl,
struct fwnode_handle *fwnode)
{
for (int i = 0; i < ctrl->cfg->serdes_count; i++) {
if (ctrl->serdes[i].fwnode == fwnode)
return &ctrl->serdes[i];
}
return NULL;
}
/*
* Walk the sibling switch's ethernet-ports subtree to learn which MAC port
* each (SerDes, link_idx) pair serves. Same "backwards" topology lookup the
* sibling MDIO driver does for phy-handle: the DT already encodes the
* mapping via per-port pcs-handle properties, so the driver doesn't need a
* parallel per-SoC table. pcs_get_state still needs the port number to
* index MAC-side link status registers; it reads link_port[] populated
* here.
*/
static int rtpcs_map_links(struct device *dev, struct rtpcs_ctrl *ctrl)
{
struct fwnode_handle *fw_dev = dev_fwnode(dev);
struct fwnode_handle *fw_parent __free(fwnode_handle) = fwnode_get_parent(fw_dev);
if (!fw_parent)
return -ENODEV;
struct fwnode_handle *fw_switch __free(fwnode_handle) =
fwnode_get_named_child_node(fw_parent, "ethernet-switch");
if (!fw_switch)
return dev_err_probe(dev, -ENODEV, "%pfwP missing ethernet-switch\n", fw_parent);
struct fwnode_handle *fw_ports __free(fwnode_handle) =
fwnode_get_named_child_node(fw_switch, "ethernet-ports");
if (!fw_ports)
return dev_err_probe(dev, -ENODEV, "%pfwP missing ethernet-ports\n", fw_switch);
fwnode_for_each_child_node_scoped(fw_ports, fw_port) {
struct fwnode_reference_args args;
struct rtpcs_serdes *sds;
int link_idx, ret;
u32 pn;
if (fwnode_property_read_u32(fw_port, "reg", &pn))
continue;
ret = fwnode_property_get_reference_args(fw_port, "pcs-handle", "#pcs-cells",
-1, 0, &args);
if (ret)
continue;
struct fwnode_handle *fw_pcs __free(fwnode_handle) = args.fwnode;
link_idx = args.args[0];
if (link_idx >= RTPCS_MAX_LINKS_PER_SDS)
return dev_err_probe(dev, -ERANGE,
"%pfwP: pcs-handle link %d exceeds max %u\n",
fw_port, link_idx, RTPCS_MAX_LINKS_PER_SDS);
sds = rtpcs_find_serdes(ctrl, fw_pcs);
if (!sds)
continue;
if (sds->link_port[link_idx] >= 0)
return dev_err_probe(dev, -EEXIST,
"%pfwP: sds%u link %d already assigned to port %d\n",
fw_port, sds->id, link_idx, sds->link_port[link_idx]);
sds->link_port[link_idx] = pn;
sds->num_of_links++;
break;
}
return 0;
}
static struct phylink_pcs *rtpcs_pcs_get(struct fwnode_reference_args *pcsspec, void *data)
{
struct rtpcs_serdes *sds = data;
struct rtpcs_link *link;
unsigned int link_idx;
struct device *dev;
dev = sds->ctrl->dev;
if (!pcsspec->nargs) {
dev_err(dev, "invalid number of cells in 'pcs' property\n");
return ERR_PTR(-EINVAL);
}
link_idx = pcsspec->args[0];
if (link_idx >= RTPCS_MAX_LINKS_PER_SDS)
return ERR_PTR(-EINVAL);
if (sds->link_port[link_idx] < 0) {
dev_err(dev, "sds %u link %d not associated with any port\n",
sds->id, link_idx);
return ERR_PTR(-ENODEV);
}
if (!sds->link[link_idx]) {
link = devm_kzalloc(dev, sizeof(*link), GFP_KERNEL);
if (!link)
return ERR_PTR(-ENOMEM);
link->ctrl = sds->ctrl;
link->port = sds->link_port[link_idx];
link->sds = sds;
link->pcs.ops = sds->ctrl->cfg->pcs_ops;
sds->link[link_idx] = link;
}
return &sds->link[link_idx]->pcs;
}
static int rtpcs_probe(struct platform_device *pdev)
@ -4332,7 +4429,9 @@ static int rtpcs_probe(struct platform_device *pdev)
return ret;
}
rtpcs_count_links(ctrl);
ret = rtpcs_map_links(dev, ctrl);
if (ret)
return ret;
if (ctrl->cfg->init) {
ret = ctrl->cfg->init(ctrl);
@ -4346,8 +4445,21 @@ static int rtpcs_probe(struct platform_device *pdev)
*/
platform_set_drvdata(pdev, ctrl);
dev_info(dev, "Realtek PCS driver initialized\n");
for (i = 0; i < ctrl->cfg->serdes_count; i++) {
sds = &ctrl->serdes[i];
if (!sds->fwnode)
continue;
ret = fwnode_pcs_add_provider(sds->fwnode, rtpcs_pcs_get, sds);
if (ret)
return ret;
ret = devm_add_action_or_reset(dev, rtpcs_del_provider_action,
sds);
if (ret)
return ret;
}
dev_info(dev, "Realtek PCS driver initialized\n");
return 0;
}

View File

@ -11,13 +11,14 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
---
--- a/drivers/net/pcs/Kconfig
+++ b/drivers/net/pcs/Kconfig
@@ -46,6 +46,15 @@ config PCS_MTK_USXGMII
@@ -46,6 +46,16 @@ config PCS_MTK_USXGMII
1000Base-X, 2500Base-X and Cisco SGMII are supported on the same
differential pairs via an embedded LynxI PCS.
+config PCS_RTL_OTTO
+ tristate "Realtek Otto SerDes PCS"
+ depends on MACH_REALTEK_RTL || COMPILE_TEST
+ select FWNODE_PCS
+ select PHY_COMMON_PROPS
+ select PHYLINK
+ select REGMAP