[PATCH 12/20] net: dsa: xilinx: register per-MAC MDIO buses

Nagadheeraj Rottela nagadheeraj.rottela at amd.com
Fri Aug 7 03:44:23 PDT 2026


The TSN IP exposes one MDIO controller per MAC at +0x500 inside
each MAC's register window. Register each bus under the matching
mdio-mac1 / mdio-mac2 child of the switch node.

A new struct xlnx_tsn_mac groups the per-MAC register window and
the back-pointer to the parent switch. MDIO callbacks and log
helpers pull what they need from it. The clock divisor comes from
the IP-wide s_axi clock, owned by the parent IP wrapper node, so
read its rate from there rather than holding a private reference.

Co-developed-by: Srinivas Neeli <srinivas.neeli at amd.com>
Signed-off-by: Srinivas Neeli <srinivas.neeli at amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela at amd.com>
---
 drivers/net/dsa/xilinx/xilinx_tsn.c | 203 +++++++++++++++++++++++++++-
 drivers/net/dsa/xilinx/xilinx_tsn.h |  54 +++++++-
 2 files changed, 251 insertions(+), 6 deletions(-)

diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 020b1622670c..542b74514ed3 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -4,17 +4,24 @@
  */
 
 #include <linux/bitfield.h>
+#include <linux/clk.h>
 #include <linux/if_bridge.h>
 #include <linux/io.h>
 #include <linux/iopoll.h>
 #include <linux/kernel.h>
+#include <linux/mdio.h>
 #include <linux/module.h>
 #include <linux/of.h>
+#include <linux/of_mdio.h>
+#include <linux/phy.h>
 #include <linux/platform_device.h>
 #include <net/dsa.h>
 
 #include "xilinx_tsn.h"
 
+#define TSN_MDIO_MAX_FREQ_HZ		2500000
+#define TSN_MDIO_READY_TIMEOUT_US	20000
+
 static void sw_iow(struct xlnx_tsn *sw, u32 off, u32 val)
 {
 	iowrite32(val, sw->sw_base + off);
@@ -95,6 +102,190 @@ static int xlnx_tsn_set_port_state(struct xlnx_tsn *sw, int port,
 	return 0;
 }
 
+static int xlnx_tsn_mdio_wait_ready(struct xlnx_tsn_mac *m)
+{
+	u32 val;
+
+	return readl_poll_timeout(m->regs + TSN_MDIO_MCR_OFFSET, val,
+				  val & TSN_MDIO_MCR_READY, 1,
+				  TSN_MDIO_READY_TIMEOUT_US);
+}
+
+static int xlnx_tsn_mdio_read(struct mii_bus *bus, int phy_id, int reg)
+{
+	struct xlnx_tsn_mac *m = bus->priv;
+	int ret;
+
+	ret = xlnx_tsn_mdio_wait_ready(m);
+	if (ret < 0)
+		return ret;
+
+	mac_iow(m, TSN_MDIO_MCR_OFFSET,
+		FIELD_PREP(TSN_MDIO_MCR_PHYAD_MASK, phy_id) |
+		FIELD_PREP(TSN_MDIO_MCR_REGAD_MASK, reg) |
+		TSN_MDIO_MCR_INITIATE | TSN_MDIO_MCR_OP_READ);
+
+	ret = xlnx_tsn_mdio_wait_ready(m);
+	if (ret < 0)
+		return ret;
+
+	return FIELD_GET(TSN_MDIO_MRD_MASK,
+			 mac_ior(m, TSN_MDIO_MRD_OFFSET));
+}
+
+static int xlnx_tsn_mdio_write(struct mii_bus *bus, int phy_id, int reg,
+			       u16 val)
+{
+	struct xlnx_tsn_mac *m = bus->priv;
+	int ret;
+
+	ret = xlnx_tsn_mdio_wait_ready(m);
+	if (ret < 0)
+		return ret;
+
+	mac_iow(m, TSN_MDIO_MWD_OFFSET, val);
+	mac_iow(m, TSN_MDIO_MCR_OFFSET,
+		FIELD_PREP(TSN_MDIO_MCR_PHYAD_MASK, phy_id) |
+		FIELD_PREP(TSN_MDIO_MCR_REGAD_MASK, reg) |
+		TSN_MDIO_MCR_INITIATE | TSN_MDIO_MCR_OP_WRITE);
+
+	return xlnx_tsn_mdio_wait_ready(m);
+}
+
+/* Round up so the MDC frequency stays at or below TSN_MDIO_MAX_FREQ_HZ,
+ * then clamp to the 6-bit field maximum so the value stays within the
+ * field and does not corrupt TSN_MDIO_MC_MDIOEN.
+ */
+static u32 xlnx_tsn_mdio_clk_div(struct xlnx_tsn *sw, unsigned long host_hz)
+{
+	u32 div;
+
+	if (!host_hz) {
+		dev_warn(sw->dev,
+			 "s_axi clock rate unknown; clamping MDIO divisor to max\n");
+		return TSN_MDIO_MC_CLOCK_DIVIDE_MAX;
+	}
+
+	div = DIV_ROUND_UP(host_hz, TSN_MDIO_MAX_FREQ_HZ * 2) - 1;
+
+	/* HW ignores MDIO Enable when Clock Divide is 0 */
+	if (!div)
+		div = 1;
+
+	if (div > TSN_MDIO_MC_CLOCK_DIVIDE_MAX) {
+		dev_warn(sw->dev,
+			 "MDIO divisor %u exceeds max %u, clamping\n",
+			 div, TSN_MDIO_MC_CLOCK_DIVIDE_MAX);
+		div = TSN_MDIO_MC_CLOCK_DIVIDE_MAX;
+	}
+
+	return div;
+}
+
+static int xlnx_tsn_mdio_register_one(struct xlnx_tsn *sw, int port,
+				      const char *child_name,
+				      unsigned long host_hz)
+{
+	struct xlnx_tsn_mac *m = &sw->mac[port];
+	struct device_node *mdio_np;
+	struct mii_bus *bus;
+	int ret;
+
+	mdio_np = of_get_child_by_name(sw->dev->of_node, child_name);
+	if (!mdio_np)
+		return 0;
+
+	bus = devm_mdiobus_alloc(sw->dev);
+	if (!bus) {
+		of_node_put(mdio_np);
+		return -ENOMEM;
+	}
+
+	snprintf(bus->id, MII_BUS_ID_SIZE, "%s:%s",
+		 dev_name(sw->dev), child_name);
+	bus->name = "Xilinx TSN MDIO";
+	bus->priv = m;
+	bus->parent = sw->dev;
+	bus->read = xlnx_tsn_mdio_read;
+	bus->write = xlnx_tsn_mdio_write;
+
+	mac_iow(m, TSN_MDIO_MC_OFFSET,
+		xlnx_tsn_mdio_clk_div(sw, host_hz) | TSN_MDIO_MC_MDIOEN);
+
+	ret = xlnx_tsn_mdio_wait_ready(m);
+	if (ret) {
+		dev_err(sw->dev, "%s: MDIO controller not ready: %d\n",
+			child_name, ret);
+		goto err_put_np;
+	}
+
+	ret = of_mdiobus_register(bus, mdio_np);
+	if (ret) {
+		dev_err(sw->dev, "%s: failed to register MDIO bus: %d\n",
+			child_name, ret);
+		goto err_put_np;
+	}
+
+	m->mii_bus = bus;
+	of_node_put(mdio_np);
+	return 0;
+
+err_put_np:
+	of_node_put(mdio_np);
+	return ret;
+}
+
+static void xlnx_tsn_mdio_unregister_all(struct xlnx_tsn *sw)
+{
+	int port;
+
+	for (port = XLNX_TSN_PORT_MAC1; port <= XLNX_TSN_PORT_MAC2; port++) {
+		struct xlnx_tsn_mac *m = &sw->mac[port];
+
+		if (m->mii_bus) {
+			mdiobus_unregister(m->mii_bus);
+			m->mii_bus = NULL;
+		}
+
+		/* clear the enable bit even when no bus was registered (failed probe) */
+		mac_iow(m, TSN_MDIO_MC_OFFSET, 0);
+	}
+}
+
+static int xlnx_tsn_mdio_register_all(struct xlnx_tsn *sw)
+{
+	unsigned long host_hz;
+	struct clk *s_axi;
+	int ret;
+
+	/* per-MAC MDIO divisor comes from the wrapper node's s_axi
+	 * clock
+	 */
+	s_axi = clk_get(sw->dev->parent, "s_axi");
+	if (IS_ERR(s_axi))
+		return dev_err_probe(sw->dev, PTR_ERR(s_axi),
+				     "failed to get s_axi clock\n");
+
+	host_hz = clk_get_rate(s_axi);
+	clk_put(s_axi);
+
+	ret = xlnx_tsn_mdio_register_one(sw, XLNX_TSN_PORT_MAC1, "mdio-mac1",
+					 host_hz);
+	if (ret)
+		goto err_unregister;
+
+	ret = xlnx_tsn_mdio_register_one(sw, XLNX_TSN_PORT_MAC2, "mdio-mac2",
+					 host_hz);
+	if (ret)
+		goto err_unregister;
+
+	return 0;
+
+err_unregister:
+	xlnx_tsn_mdio_unregister_all(sw);
+	return ret;
+}
+
 static enum dsa_tag_protocol xlnx_tsn_get_tag_protocol(struct dsa_switch *ds,
 						       int port,
 						       enum dsa_tag_protocol mp)
@@ -160,7 +351,7 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
 			return ret;
 	}
 
-	return 0;
+	return xlnx_tsn_mdio_register_all(sw);
 }
 
 static void xlnx_tsn_teardown(struct dsa_switch *ds)
@@ -168,6 +359,8 @@ static void xlnx_tsn_teardown(struct dsa_switch *ds)
 	struct xlnx_tsn *sw = ds->priv;
 	struct dsa_port *dp;
 
+	xlnx_tsn_mdio_unregister_all(sw);
+
 	dsa_switch_for_each_user_port(dp, ds)
 		xlnx_tsn_set_port_state(sw, dp->index, TSN_PORT_STATE_DISABLED);
 
@@ -207,16 +400,20 @@ static int xlnx_tsn_probe(struct platform_device *pdev)
 		return -ENOMEM;
 
 	sw->dev = dev;
+	sw->mac[XLNX_TSN_PORT_MAC1].sw = sw;
+	sw->mac[XLNX_TSN_PORT_MAC2].sw = sw;
 
 	ret = xlnx_tsn_map_reg(pdev, "switch", &sw->sw_base);
 	if (ret)
 		return ret;
 
-	ret = xlnx_tsn_map_reg(pdev, "mac1", &sw->mac_base[XLNX_TSN_PORT_MAC1]);
+	ret = xlnx_tsn_map_reg(pdev, "mac1",
+			       &sw->mac[XLNX_TSN_PORT_MAC1].regs);
 	if (ret)
 		return ret;
 
-	ret = xlnx_tsn_map_reg(pdev, "mac2", &sw->mac_base[XLNX_TSN_PORT_MAC2]);
+	ret = xlnx_tsn_map_reg(pdev, "mac2",
+			       &sw->mac[XLNX_TSN_PORT_MAC2].regs);
 	if (ret)
 		return ret;
 
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index 1064cf1ce66e..025645a83992 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -7,6 +7,7 @@
 
 #include <linux/bitfield.h>
 #include <linux/bits.h>
+#include <linux/io.h>
 #include <linux/types.h>
 #include <net/dsa.h>
 
@@ -45,19 +46,66 @@ enum tsn_port_state {
 	TSN_PORT_STATE_FORWARDING,
 };
 
+/* Per-MAC MDIO controller register window, sitting at +0x500 inside
+ * each MAC's reg space owned via reg-names = "mac1", "mac2".
+ */
+#define TSN_MDIO_MC_OFFSET		0x00000500
+#define TSN_MDIO_MCR_OFFSET		0x00000504
+#define TSN_MDIO_MWD_OFFSET		0x00000508
+#define TSN_MDIO_MRD_OFFSET		0x0000050c
+
+#define TSN_MDIO_MC_MDIOEN		BIT(6)
+#define TSN_MDIO_MC_CLOCK_DIVIDE_MAX	0x3f
+
+#define TSN_MDIO_MCR_PHYAD_MASK		GENMASK(28, 24)
+#define TSN_MDIO_MCR_REGAD_MASK		GENMASK(20, 16)
+#define TSN_MDIO_MCR_OP_READ		BIT(15)
+#define TSN_MDIO_MCR_OP_WRITE		BIT(14)
+#define TSN_MDIO_MCR_INITIATE		BIT(11)
+#define TSN_MDIO_MCR_READY		BIT(7)
+
+#define TSN_MDIO_MRD_MASK		GENMASK(15, 0)
+
+struct mii_bus;
+struct xlnx_tsn;
+
+/**
+ * struct xlnx_tsn_mac - per-MAC switch-side state
+ * @sw: back-pointer to the parent switch (for dev_* logging in
+ *	bus callbacks)
+ * @regs: per-MAC register window, from reg-name "macN"
+ * @mii_bus: MDIO bus registered under the "mdio-macN" DT child,
+ *	     or NULL if absent
+ */
+struct xlnx_tsn_mac {
+	struct xlnx_tsn *sw;
+	void __iomem *regs;
+	struct mii_bus *mii_bus;
+};
+
 /**
  * struct xlnx_tsn - per-IP switch state
  * @ds: DSA switch
  * @dev: backing device
  * @sw_base: switch fabric register window
- * @mac_base: per-MAC register windows, indexed by user-port number
- *	      (index 0 unused; MAC1 at [1], MAC2 at [2])
+ * @mac: per-MAC state, indexed by user-port number (index 0 unused;
+ *	 MAC1 at [1], MAC2 at [2])
  */
 struct xlnx_tsn {
 	struct dsa_switch ds;
 	struct device *dev;
 	void __iomem *sw_base;
-	void __iomem *mac_base[XLNX_TSN_NUM_PORTS];
+	struct xlnx_tsn_mac mac[XLNX_TSN_NUM_PORTS];
 };
 
+static inline void mac_iow(struct xlnx_tsn_mac *m, u32 off, u32 val)
+{
+	iowrite32(val, m->regs + off);
+}
+
+static inline u32 mac_ior(struct xlnx_tsn_mac *m, u32 off)
+{
+	return ioread32(m->regs + off);
+}
+
 #endif /* _XILINX_TSN_H */
-- 
2.34.1




More information about the linux-arm-kernel mailing list