[RFC PATCH 4/8] xilinx: tsn: Add Ethernet MAC (EMAC) and MDIO support to the TSN driver

From: Srinivas Neeli

Date: Thu Feb 19 2026 - 00:52:38 EST


Introduce support for the Ethernet MAC (EMAC) and MDIO controller used by
the TSN driver.

This patch adds:
- EMAC initialization, reset handling, and register access helpers
- MAC address configuration support
- MDIO read/write functions for PHY register access
- Basic PHY detection and link status handling
- Error handling and resource cleanup during initialization failures
- Build system updates to compile the new EMAC/MDIO support

Signed-off-by: Srinivas Neeli <srinivas.neeli@xxxxxxx>
---
drivers/net/ethernet/xilinx/tsn/Makefile | 2 +-
drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h | 124 +++++++
.../net/ethernet/xilinx/tsn/xilinx_tsn_emac.c | 326 ++++++++++++++++++
.../net/ethernet/xilinx/tsn/xilinx_tsn_main.c | 19 +-
.../net/ethernet/xilinx/tsn/xilinx_tsn_mdio.c | 308 +++++++++++++++++
5 files changed, 774 insertions(+), 5 deletions(-)
create mode 100644 drivers/net/ethernet/xilinx/tsn/xilinx_tsn_emac.c
create mode 100644 drivers/net/ethernet/xilinx/tsn/xilinx_tsn_mdio.c

diff --git a/drivers/net/ethernet/xilinx/tsn/Makefile b/drivers/net/ethernet/xilinx/tsn/Makefile
index 099526877948..5eb6dde67061 100644
--- a/drivers/net/ethernet/xilinx/tsn/Makefile
+++ b/drivers/net/ethernet/xilinx/tsn/Makefile
@@ -1,2 +1,2 @@
obj-$(CONFIG_XILINX_TSN) :=xilinx_tsn.o
-xilinx_tsn-objs := xilinx_tsn_main.o xilinx_tsn_ep.o
+xilinx_tsn-objs := xilinx_tsn_main.o xilinx_tsn_ep.o xilinx_tsn_emac.o xilinx_tsn_mdio.o
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h
index 054f74b97a38..c8435c09ed2c 100644
--- a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h
@@ -25,6 +25,7 @@
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_dma.h>
+#include <linux/of_mdio.h>
#include <linux/of_net.h>
#include <linux/of_platform.h>
#include <linux/platform_device.h>
@@ -52,6 +53,91 @@
#define TSN_TUSER_PORT_MAC1 0x1 /* MAC-1 Port */
#define TSN_TUSER_PORT_MAC2 0x2 /* MAC-2 Port */

+/* TSN MAC Registers */
+#define TSN_RAF_OFFSET 0x00000000 /* Reset and Address filter */
+#define TSN_STATS_OFFSET 0x00000200 /* Statistics counters */
+#define TSN_RCW0_OFFSET 0x00000400 /* Rx Configuration Word 0 */
+#define TSN_RCW1_OFFSET 0x00000404 /* Rx Configuration Word 1 */
+#define TSN_TC_OFFSET 0x00000408 /* Tx Configuration */
+#define TSN_FCC_OFFSET 0x0000040C /* Flow Control Configuration */
+#define TSN_EMMC_OFFSET 0x00000410 /* MAC speed configuration */
+#define TSN_PHYC_OFFSET 0x00000414 /* RX Max Frame Configuration */
+#define TSN_ID_OFFSET 0x000004F8 /* Identification register */
+#define TSN_ABILITY_OFFSET 0x000004FC /* Ability Register offset */
+#define TSN_MDIO_MC_OFFSET 0x00000500 /* MDIO Setup */
+#define TSN_MDIO_MCR_OFFSET 0x00000504 /* MDIO Control */
+#define TSN_MDIO_MWD_OFFSET 0x00000508 /* MDIO Write Data */
+#define TSN_MDIO_MRD_OFFSET 0x0000050C /* MDIO Read Data */
+
+/* Bit masks for TSN Ethernet MDIO interface MC register */
+#define TSN_MDIO_MC_MDIOEN BIT(6) /* MII management enable */
+#define TSN_MDIO_MC_CLOCK_DIVIDE_MAX 0x3F /* Maximum MDIO divisor */
+
+/* Bit masks for TSN Ethernet MDIO interface MCR register */
+#define TSN_MDIO_MCR_PHYAD_SHIFT 24 /* Phy Address Shift */
+#define TSN_MDIO_MCR_PHYAD_MASK GENMASK(28, 24) /* Phy Address Mask */
+#define TSN_MDIO_MCR_REGAD_SHIFT 16 /* Reg Address Shift */
+#define TSN_MDIO_MCR_REGAD_MASK GENMASK(20, 16) /* Reg Address Mask */
+#define TSN_MDIO_MCR_OP_SHIFT 14 /* Operation Code Shift */
+#define TSN_MDIO_MCR_OP_MASK GENMASK(15, 14) /* Operation Code Mask */
+#define TSN_MDIO_MCR_OP_READ BIT(15) /* Op Code Read */
+#define TSN_MDIO_MCR_OP_WRITE BIT(14) /* Op Code Write */
+#define TSN_MDIO_MCR_INITIATE BIT(11) /* Initiate MDIO transaction */
+#define TSN_MDIO_MCR_READY BIT(7) /* MDIO Ready */
+
+/* Bit masks for TSN Ethernet MDIO Write Data Register */
+#define TSN_MDIO_MWD_SHIFT 0 /* Write Data Shift */
+#define TSN_MDIO_MWD_MASK GENMASK(15, 0) /* Write Data Mask */
+
+/* Bit masks for TSN Ethernet MDIO Read Data Register */
+#define TSN_MDIO_MRD_SHIFT 0 /* Read Data Shift */
+#define TSN_MDIO_MRD_MASK GENMASK(15, 0) /* Read Data Mask */
+
+/* Bit masks for Ethernet UAW1 register */
+/* Station address bits [47:32]; Station address
+ * bits [31:0] are stored in register UAW0
+ */
+#define TSN_UAW1_UNICASTADDR_MASK GENMASK(15, 0)
+
+/* Bit masks for TSN Ethernet EMMC register */
+#define TSN_EMMC_LINKSPEED_SHIFT 30 /* Link speed shift */
+#define TSN_EMMC_LINKSPEED_MASK GENMASK(31, 30) /* Link speed mask */
+#define TSN_EMMC_LINKSPEED_10 0x0 /* 10 Mbit */
+#define TSN_EMMC_LINKSPEED_100 BIT(30) /* 100 Mbit */
+#define TSN_EMMC_LINKSPEED_1000 BIT(31) /* 1000 Mbit */
+
+#define TSN_MAX_EMAC_NO 2
+
+/*
+ * struct tsn_emac - TSN Ethernet MAC configuration structure
+ * @ndev: Network device associated with this EMAC instance
+ * @common: Pointer to the main TSN private data structure
+ * @phy_node: Device tree node for the connected PHY device
+ * @phy_mode: PHY interface mode (RGMII, SGMII, etc.)
+ * @phy_flags: PHY-specific configuration flags
+ * @regs: Virtual address mapping of EMAC register space
+ * @regs_start: Physical start address of EMAC register space
+ * @mii_bus: MDIO bus controller for PHY management
+ * @last_link: Previous link state for change detection
+ * @mii_clk_div: MDIO clock divider value
+ * @emac_num: EMAC instance number (1 or 2)
+ * @irq: Interrupt number for this EMAC
+ */
+struct tsn_emac {
+ struct net_device *ndev;
+ struct tsn_priv *common;
+ struct device_node *phy_node;
+ phy_interface_t phy_mode;
+ u32 phy_flags;
+ void __iomem *regs;
+ resource_size_t regs_start;
+ struct mii_bus *mii_bus;
+ u32 last_link;
+ u8 mii_clk_div;
+ int emac_num;
+ int irq;
+};
+
/*
* struct skbuf_dma_descriptor - skb for each dma descriptor
* @sgl: Pointer for sglist.
@@ -106,6 +192,7 @@ struct tsn_endpoint {
* @regs_start: Start address (physical) of mapped region
* @regs: ioremap()'d base pointer
* @ep: Pointer to TSN endpoint structure
+ * @emacs: Array of EMAC instances (up to 2)
* @clks: Bulk clock data for all required clocks
* @tx_lock: Spinlock protecting TX rings and related TX state
* @rx_lock: Spinlock protecting RX rings and related RX state
@@ -117,6 +204,7 @@ struct tsn_endpoint {
* @max_frm_size: Maximum frame size supported
* @tx_chans: Array of TX DMA channels
* @rx_chans: Array of RX DMA channels
+ * @num_emacs: Number of EMAC instances
*/
struct tsn_priv {
struct platform_device *pdev;
@@ -125,6 +213,7 @@ struct tsn_priv {
resource_size_t regs_start;
void __iomem *regs;
struct tsn_endpoint *ep;
+ struct tsn_emac *emacs[TSN_MAX_EMAC_NO];
struct clk_bulk_data clks[TSN_NUM_CLOCKS];
spinlock_t tx_lock; /* Protects TX ring buffers */
spinlock_t rx_lock; /* Protects RX ring buffers */
@@ -136,6 +225,7 @@ struct tsn_priv {
u32 max_frm_size;
struct tsn_dma_chan **tx_chans;
struct tsn_dma_chan **rx_chans;
+ u32 num_emacs;
};

/**
@@ -168,6 +258,40 @@ static inline int tsn_ndo_set_mac_address(struct net_device *ndev, void *p)
netdev_tx_t tsn_start_xmit_dmaengine(struct tsn_priv *common,
struct sk_buff *skb,
struct net_device *ndev);
+
+/**
+ * emac_iow - Memory mapped TSN EMAC register write
+ * @emac: Pointer to TSN EMAC structure
+ * @off: Address offset from the base address of EMAC registers
+ * @val: Value to be written into the EMAC register
+ *
+ * This function writes the desired value into the corresponding TSN
+ * EMAC register.
+ */
+static inline void emac_iow(struct tsn_emac *emac, off_t off, u32 val)
+{
+ iowrite32(val, emac->regs + off);
+}
+
+/**
+ * emac_ior - Memory mapped TSN EMAC register read
+ * @emac: Pointer to TSN EMAC structure
+ * @off: Address offset from the base address of EMAC registers
+ *
+ * This function reads a value from the corresponding TSN EMAC
+ * register.
+ *
+ * Return: Value read from the EMAC register
+ */
+static inline u32 emac_ior(struct tsn_emac *emac, u32 off)
+{
+ return ioread32(emac->regs + off);
+}
+
int tsn_ep_init(struct platform_device *pdev);
void tsn_ep_exit(struct platform_device *pdev);
+int tsn_emac_init(struct platform_device *pdev);
+void tsn_emac_exit(struct platform_device *pdev);
+int tsn_mdio_setup(struct tsn_emac *emac, struct device_node *mac_np);
+void tsn_mdio_teardown(struct tsn_emac *emac);
#endif /* XILINX_TSN_H */
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_emac.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_emac.c
new file mode 100644
index 000000000000..26a533e313a2
--- /dev/null
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_emac.c
@@ -0,0 +1,326 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "xilinx_tsn.h"
+
+#define DRIVER_NAME "xilinx_tsn_emac"
+#define DRIVER_DESCRIPTION "Xilinx TSN driver"
+#define DRIVER_VERSION "1.0"
+
+/**
+ * tsn_adjust_link_tsn - Adjust link parameters
+ * @ndev: Pointer to the net_device structure
+ *
+ * This function is called when the PHY link state changes. It configures
+ * the EMAC link speed register based on the current PHY settings and
+ * updates link status information.
+ */
+static void tsn_adjust_link_tsn(struct net_device *ndev)
+{
+ struct tsn_emac *emac = netdev_priv(ndev);
+ struct phy_device *phy = ndev->phydev;
+ u32 emmc_reg;
+
+ if (!phy || emac->last_link == phy->link)
+ return;
+
+ if (phy->link) {
+ emmc_reg = emac_ior(emac, TSN_EMMC_OFFSET);
+ emmc_reg &= ~TSN_EMMC_LINKSPEED_MASK;
+
+ switch (phy->speed) {
+ case SPEED_1000:
+ emmc_reg |= TSN_EMMC_LINKSPEED_1000;
+ break;
+ case SPEED_100:
+ emmc_reg |= TSN_EMMC_LINKSPEED_100;
+ break;
+ default:
+ dev_warn(&ndev->dev, "Unsupported speed: %d\n", phy->speed);
+ break;
+ }
+
+ emac_iow(emac, TSN_EMMC_OFFSET, emmc_reg);
+ dev_info(&ndev->dev, "Link up: %d Mbps, %s duplex\n",
+ phy->speed, phy->duplex ? "full" : "half");
+ } else {
+ dev_info(&ndev->dev, "Link down\n");
+ }
+
+ emac->last_link = phy->link;
+}
+
+/**
+ * emac_open - Open the network interface
+ * @ndev: Pointer to the net_device structure
+ *
+ * This function is called when the network interface is brought up.
+ * It connects to the PHY device and starts the PHY if available.
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+static int emac_open(struct net_device *ndev)
+{
+ struct tsn_emac *emac = netdev_priv(ndev);
+ struct phy_device *phydev = NULL;
+
+ if (emac->phy_node) {
+ phydev = of_phy_connect(emac->ndev, emac->phy_node,
+ tsn_adjust_link_tsn,
+ emac->phy_flags,
+ emac->phy_mode);
+ if (!phydev)
+ dev_err(emac->common->dev, "of_phy_connect() failed\n");
+ else
+ phy_start(phydev);
+ }
+
+ return 0;
+}
+
+/**
+ * emac_stop - Stop the network interface
+ * @ndev: Pointer to the net_device structure
+ *
+ * This function is called when the network interface is brought down.
+ * It disconnects the PHY device to stop link monitoring.
+ *
+ * Return: 0 on success
+ */
+static int emac_stop(struct net_device *ndev)
+{
+ if (ndev->phydev)
+ phy_disconnect(ndev->phydev);
+
+ return 0;
+}
+
+/**
+ * emac_validate_addr - Validate the MAC address
+ * @ndev: Pointer to the net_device structure
+ *
+ * This function validates the current MAC address of the device.
+ *
+ * Return: 0 if address is valid, negative error code otherwise
+ */
+static int emac_validate_addr(struct net_device *ndev)
+{
+ return eth_validate_addr(ndev);
+}
+
+/**
+ * emac_start_xmit - Transmit packet handler
+ * @skb: Socket buffer containing the packet
+ * @ndev: Pointer to the net_device structure
+ *
+ * This function handles packet transmission for EMAC interfaces.
+ * Currently drops packets and updates statistics as EMAC is not
+ * configured for actual transmission.
+ *
+ * Return: NETDEV_TX_OK always
+ */
+static netdev_tx_t emac_start_xmit(struct sk_buff *skb, struct net_device *ndev)
+{
+ struct tsn_emac *emac = netdev_priv(ndev);
+
+ return tsn_start_xmit_dmaengine(emac->common, skb, ndev);
+}
+
+static const struct net_device_ops emac_netdev_ops = {
+ .ndo_open = emac_open,
+ .ndo_stop = emac_stop,
+ .ndo_start_xmit = emac_start_xmit,
+ .ndo_set_mac_address = tsn_ndo_set_mac_address,
+ .ndo_validate_addr = emac_validate_addr,
+};
+
+/**
+ * emac_get_drvinfo - Get various TSN Ethernet driver information.
+ * @ndev: Pointer to net_device structure
+ * @ed: Pointer to ethtool_drvinfo structure
+ *
+ * This implements ethtool command for getting the driver information.
+ * Issue "ethtool -i ethX" under linux prompt to execute this function.
+ */
+static void emac_get_drvinfo(struct net_device *ndev,
+ struct ethtool_drvinfo *ed)
+{
+ strscpy(ed->driver, DRIVER_NAME, sizeof(ed->driver));
+ strscpy(ed->version, DRIVER_VERSION, sizeof(ed->version));
+}
+
+static const struct ethtool_ops emac_ethtool_ops = {
+ .get_drvinfo = emac_get_drvinfo,
+ .get_link = ethtool_op_get_link,
+ .get_link_ksettings = phy_ethtool_get_link_ksettings,
+ .set_link_ksettings = phy_ethtool_set_link_ksettings,
+};
+
+/**
+ * tsn_emac_init - Initialize TSN EMAC interfaces
+ * @pdev: Platform device pointer
+ *
+ * This function initializes all EMAC interfaces found in the device tree.
+ * For each EMAC, it allocates a network device, maps register regions,
+ * sets up PHY connections, configures MDIO bus, and registers the
+ * network interface with the kernel.
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int tsn_emac_init(struct platform_device *pdev)
+{
+ struct tsn_priv *common = platform_get_drvdata(pdev);
+ struct device *dev = &pdev->dev;
+ struct device_node *emac_np;
+ int ret, array_idx = 0;
+
+ for_each_child_of_node(dev->of_node, emac_np) {
+ struct net_device *ndev;
+ struct tsn_emac *emac;
+ u8 mac_addr[ETH_ALEN];
+ struct resource res;
+ u32 mac_id = 0;
+
+ if (!of_node_name_eq(emac_np, "ethernet-mac"))
+ continue;
+
+ ret = of_property_read_u32(emac_np, "xlnx,mac-id", &mac_id);
+ if (ret) {
+ dev_err(dev, "Missing mandatory property 'xlnx,mac-id' for EMAC %d\n",
+ array_idx + 1);
+ of_node_put(emac_np);
+ goto err_cleanup_all;
+ }
+
+ ndev = alloc_etherdev(sizeof(*emac));
+ if (!ndev) {
+ ret = -ENOMEM;
+ of_node_put(emac_np);
+ goto err_cleanup_all;
+ }
+
+ ret = of_address_to_resource(emac_np, 0, &res);
+ if (ret) {
+ dev_err_probe(dev, ret, "failed to get emac resource\n");
+ goto err_free_ndev_put_node;
+ }
+
+ emac = netdev_priv(ndev);
+ memset(emac, 0, sizeof(*emac));
+ emac->ndev = ndev;
+ emac->common = common;
+ emac->regs_start = common->regs_start + res.start;
+ emac->regs = common->regs + res.start;
+ emac->emac_num = mac_id;
+ /* basic netdev config */
+ ndev->netdev_ops = &emac_netdev_ops;
+ ndev->ethtool_ops = &emac_ethtool_ops;
+ ndev->min_mtu = ETH_ZLEN - ETH_HLEN;
+ ndev->max_mtu = ETH_DATA_LEN;
+ SET_NETDEV_DEV(ndev, dev);
+
+ /* Retrieve the MAC address */
+ ret = of_get_mac_address(emac_np, mac_addr);
+ if (ret == 0 && is_valid_ether_addr(mac_addr))
+ eth_hw_addr_set(ndev, mac_addr);
+
+ emac->phy_node = of_parse_phandle(emac_np, "phy-handle", 0);
+ if (!emac->phy_node) {
+ dev_err(&pdev->dev, "Failed to get 'phy-handle' from device tree\n");
+
+ } else {
+ ret = tsn_mdio_setup(emac, emac_np);
+ if (ret) {
+ dev_warn(&pdev->dev, "error registering MDIO bus for EMAC %d: %d\n",
+ mac_id, ret);
+ goto err_put_phy_node;
+ }
+ }
+
+ ret = register_netdev(ndev);
+ if (ret) {
+ dev_err(dev, "Failed to register net device for MAC %d\n", mac_id);
+ goto err_teardown_mdio;
+ }
+
+ common->emacs[array_idx] = emac;
+ array_idx++;
+ common->num_emacs = array_idx;
+ continue;
+
+err_teardown_mdio:
+ if (emac->phy_node)
+ tsn_mdio_teardown(emac);
+err_put_phy_node:
+ if (emac->phy_node)
+ of_node_put(emac->phy_node);
+err_free_ndev_put_node:
+ free_netdev(ndev);
+ of_node_put(emac_np);
+ dev_warn(dev, "EMAC %d initialization failed, rolling back\n", mac_id);
+ goto err_cleanup_all;
+ }
+
+ if (array_idx == 0)
+ return -ENODEV;
+
+ return 0;
+
+err_cleanup_all:
+ /* Cleanup all initialized EMACs in reverse order */
+ while (array_idx > 0) {
+ struct tsn_emac *old = common->emacs[--array_idx];
+
+ if (!old)
+ continue;
+
+ dev_info(dev, "Cleaning up MAC %u (array[%d])\n", old->emac_num, array_idx);
+
+ unregister_netdev(old->ndev);
+
+ if (old->phy_node) {
+ tsn_mdio_teardown(old);
+ of_node_put(old->phy_node);
+ }
+
+ free_netdev(old->ndev);
+ common->emacs[array_idx] = NULL;
+ }
+
+ common->num_emacs = 0;
+
+ return ret;
+}
+
+/**
+ * tsn_emac_exit - Cleanup TSN EMAC interfaces
+ * @pdev: Platform device pointer
+ *
+ * This function performs cleanup for all initialized EMAC interfaces.
+ * It unregisters network devices, tears down MDIO buses, releases
+ * PHY connections, and frees allocated memory for each EMAC instance.
+ */
+void tsn_emac_exit(struct platform_device *pdev)
+{
+ struct tsn_priv *common = platform_get_drvdata(pdev);
+ struct device *dev = &pdev->dev;
+ int i;
+
+ /* Cleanup only the EMACs that were actually initialized */
+ for (i = 0; i < common->num_emacs; i++) {
+ struct tsn_emac *emac = common->emacs[i];
+
+ if (!emac)
+ continue;
+
+ dev_info(dev, "Cleaning up MAC %u (array[%d])\n", emac->emac_num, i);
+
+ unregister_netdev(emac->ndev);
+ if (emac->phy_node) {
+ tsn_mdio_teardown(emac);
+ of_node_put(emac->phy_node);
+ }
+ free_netdev(emac->ndev);
+ common->emacs[i] = NULL;
+ }
+
+ common->num_emacs = 0;
+}
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
index 9e674f99d83f..7cb07e330f57 100644
--- a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
@@ -92,13 +92,14 @@ static void tsn_rx_submit_desc(struct tsn_dma_chan *xchan)
* @tuser: TUSER metadata word from DMA descriptor
*
* Extract Input Port ID from TUSER bits[5:4] and return corresponding netdev.
- * Currently only EP is supported; MAC ports will return NULL until implemented.
+ * Supports EP (endpoint) and MAC1/MAC2 (EMAC) ports.
*
* Return: net_device pointer on success, NULL if port not available
*/
static inline struct net_device *tsn_classify_rx_packet(struct tsn_priv *common, u32 tuser)
{
u32 port_id;
+ int i;

/* Extract Input Port ID from TUSER bits[5:4] */
port_id = FIELD_GET(TSN_TUSER_PORT_ID_MASK, tuser);
@@ -112,9 +113,13 @@ static inline struct net_device *tsn_classify_rx_packet(struct tsn_priv *common,

case TSN_TUSER_PORT_MAC1:
case TSN_TUSER_PORT_MAC2:
- /* MAC ports not yet implemented */
+ for (i = 0; i < common->num_emacs; i++) {
+ if (common->emacs[i] &&
+ common->emacs[i]->emac_num == port_id)
+ return common->emacs[i]->ndev;
+ }
if (net_ratelimit())
- dev_warn(common->dev, "RX from MAC port %u not yet supported\n", port_id);
+ dev_warn(common->dev, "RX from MAC port %u not found\n", port_id);
return NULL;

default:
@@ -726,13 +731,18 @@ static int tsn_ip_probe(struct platform_device *pdev)
goto free_clk;
}

- /* Initialize EP - now safe to register because DMA is ready */
ret = tsn_ep_init(pdev);
if (ret)
goto exit_dma;

+ ret = tsn_emac_init(pdev);
+ if (ret)
+ goto exit_ep;
+
return 0;

+exit_ep:
+ tsn_ep_exit(pdev);
exit_dma:
tsn_exit_dmaengine(pdev);
free_clk:
@@ -748,6 +758,7 @@ static void tsn_ip_remove(struct platform_device *pdev)
{
struct tsn_priv *common = platform_get_drvdata(pdev);

+ tsn_emac_exit(pdev);
/* Tear down DMA channels and endpoint */
if (common->ep)
tsn_ep_exit(pdev);
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_mdio.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_mdio.c
new file mode 100644
index 000000000000..a057378c9d22
--- /dev/null
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_mdio.c
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * TSN MDIO bus driver
+ */
+
+#include "xilinx_tsn.h"
+
+#define MAX_MDIO_FREQ 2500000 /* 2.5 MHz */
+#define DEFAULT_AXI_CLK_FREQ 150000000 /* 150 MHz */
+
+/**
+ * emac_ior_read_mcr - Read MDIO Control Register
+ * @emac: Pointer to TSN EMAC structure
+ *
+ * This function reads the MDIO Control Register (MCR) and is used
+ * as a callback for polling operations.
+ *
+ * Return: Value of MCR register
+ */
+static inline u32 emac_ior_read_mcr(struct tsn_emac *emac)
+{
+ return emac_ior(emac, TSN_MDIO_MCR_OFFSET);
+}
+
+/**
+ * tsn_mdio_wait_until_ready - Wait for MDIO interface to be ready
+ * @emac: Pointer to TSN EMAC structure
+ *
+ * This function polls the MDIO Control Register until the READY bit
+ * is set, indicating the interface is ready for a new transaction.
+ *
+ * Return: 0 on success, -ETIMEDOUT on timeout
+ */
+static int tsn_mdio_wait_until_ready(struct tsn_emac *emac)
+{
+ u32 val;
+
+ return readx_poll_timeout(emac_ior_read_mcr, emac,
+ val, val & TSN_MDIO_MCR_READY,
+ 1, 20000);
+}
+
+/**
+ * tsn_mdio_mdc_enable - Enable MDIO MDC clock
+ * @emac: Pointer to TSN EMAC structure
+ *
+ * This function enables the MDIO Management Data Clock (MDC) by setting
+ * the appropriate bits in the MDIO Control register. Called prior to
+ * read/write operations.
+ */
+static void tsn_mdio_mdc_enable(struct tsn_emac *emac)
+{
+ emac_iow(emac, TSN_MDIO_MC_OFFSET,
+ ((u32)emac->mii_clk_div | TSN_MDIO_MC_MDIOEN));
+}
+
+/**
+ * tsn_mdio_mdc_disable - Disable MDIO MDC clock
+ * @emac: Pointer to TSN EMAC structure
+ *
+ * This function disables the MDIO Management Data Clock (MDC) by clearing
+ * the enable bit in the MDIO Control register. Called after read/write
+ * operations to save power.
+ */
+static void tsn_mdio_mdc_disable(struct tsn_emac *emac)
+{
+ u32 mc_reg;
+
+ mc_reg = emac_ior(emac, TSN_MDIO_MC_OFFSET);
+ emac_iow(emac, TSN_MDIO_MC_OFFSET,
+ (mc_reg & ~TSN_MDIO_MC_MDIOEN));
+}
+
+/**
+ * tsn_mdio_read - MDIO interface read function
+ * @bus: Pointer to mii bus structure
+ * @phy_id: Address of the PHY device
+ * @reg: PHY register to read
+ *
+ * Return: The register contents on success, -ETIMEDOUT on a timeout
+ *
+ * Reads the contents of the requested register from the requested PHY
+ * address by first writing the details into MCR register. After a while
+ * the register MRD is read to obtain the PHY register content.
+ */
+static int tsn_mdio_read(struct mii_bus *bus, int phy_id, int reg)
+{
+ u32 rc;
+ int ret;
+ struct tsn_emac *emac = bus->priv;
+ struct tsn_priv *common = emac->common;
+
+ scoped_guard(mutex, &common->mdio_lock) {
+ tsn_mdio_mdc_enable(emac);
+
+ ret = tsn_mdio_wait_until_ready(emac);
+ if (ret < 0) {
+ tsn_mdio_mdc_disable(emac);
+ return ret;
+ }
+
+ emac_iow(emac, 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 = tsn_mdio_wait_until_ready(emac);
+ if (ret < 0) {
+ tsn_mdio_mdc_disable(emac);
+ return ret;
+ }
+
+ rc = FIELD_GET(TSN_MDIO_MRD_MASK,
+ emac_ior(emac, TSN_MDIO_MRD_OFFSET));
+ tsn_mdio_mdc_disable(emac);
+ }
+ dev_dbg(common->dev, "%s (phy_id=%i, reg=%x) == %x\n",
+ __func__, phy_id, reg, rc);
+
+ return rc;
+}
+
+/**
+ * tsn_mdio_write - MDIO interface write function
+ * @bus: Pointer to mii bus structure
+ * @phy_id: Address of the PHY device
+ * @reg: PHY register to write to
+ * @val: Value to be written into the register
+ *
+ * Return: 0 on success, -ETIMEDOUT on a timeout
+ *
+ * Writes the value to the requested register by first writing the value
+ * into MWD register. The MCR register is then appropriately setup
+ * to finish the write operation.
+ */
+static int tsn_mdio_write(struct mii_bus *bus, int phy_id, int reg,
+ u16 val)
+{
+ struct tsn_emac *emac = bus->priv;
+ struct tsn_priv *common = emac->common;
+ int ret;
+
+ dev_dbg(common->dev, "%s (phy_id=%i, reg=%x, val=%x)\n",
+ __func__, phy_id, reg, val);
+ scoped_guard(mutex, &common->mdio_lock) {
+ tsn_mdio_mdc_enable(emac);
+
+ ret = tsn_mdio_wait_until_ready(emac);
+ if (ret < 0) {
+ tsn_mdio_mdc_disable(emac);
+ return ret;
+ }
+
+ emac_iow(emac, TSN_MDIO_MWD_OFFSET, (u32)val);
+ emac_iow(emac, 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);
+
+ ret = tsn_mdio_wait_until_ready(emac);
+ if (ret < 0) {
+ tsn_mdio_mdc_disable(emac);
+ return ret;
+ }
+ tsn_mdio_mdc_disable(emac);
+ }
+ return 0;
+}
+
+/**
+ * tsn_mdio_enable - Configure and enable MDIO controller
+ * @emac: Pointer to TSN EMAC structure
+ *
+ * This function calculates the appropriate clock divisor for MDIO timing
+ * based on the host clock frequency, programs the divisor, and enables
+ * the MDIO controller. It ensures MDIO frequency does not exceed 2.5 MHz.
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+static int tsn_mdio_enable(struct tsn_emac *emac)
+{
+ struct tsn_priv *common = emac->common;
+ u32 axi_clk_freq;
+ u32 clk_div;
+ int i;
+
+ emac->mii_clk_div = 0;
+
+ /* Pick the right clock for MDIO timing */
+ axi_clk_freq = 0;
+ for (i = 0; i < TSN_NUM_CLOCKS; i++) {
+ const char *id = common->clks[i].id;
+
+ if (id && !strcmp(id, "s_axi_aclk") && common->clks[i].clk) {
+ axi_clk_freq = clk_get_rate(common->clks[i].clk);
+ break;
+ }
+ }
+
+ if (!axi_clk_freq) {
+ dev_warn(common->dev,
+ "Could not get s_axi_aclk, assuming %d Hz\n",
+ DEFAULT_AXI_CLK_FREQ);
+ axi_clk_freq = DEFAULT_AXI_CLK_FREQ;
+ }
+
+ /* Equation: fMDIO = fHOST / ((1 + clk_div) * 2)
+ * Must ensure fMDIO <= 2.5 MHz
+ */
+ clk_div = (axi_clk_freq / (MAX_MDIO_FREQ * 2)) - 1;
+ if (axi_clk_freq % (MAX_MDIO_FREQ * 2))
+ clk_div++;
+
+ emac->mii_clk_div = clk_div;
+
+ dev_dbg(common->dev,
+ "MDIO: host_clk=%u Hz, clk_div=%u\n",
+ axi_clk_freq, clk_div);
+
+ /* Program divisor and enable MDIO controller */
+ dev_info(common->dev,
+ "MDIO: writing to offset=0x%x, value=0x%lx\n",
+ TSN_MDIO_MC_OFFSET,
+ (unsigned long)(emac->mii_clk_div | TSN_MDIO_MC_MDIOEN));
+
+ /* Program divisor and enable MDIO controller */
+ emac_iow(emac, TSN_MDIO_MC_OFFSET,
+ emac->mii_clk_div | TSN_MDIO_MC_MDIOEN);
+ return tsn_mdio_wait_until_ready(emac);
+}
+
+/**
+ * tsn_mdio_setup - Setup MDIO bus for TSN EMAC
+ * @emac: Pointer to TSN EMAC structure
+ * @mac_np: Device tree node for MAC
+ *
+ * This function initializes the MDIO bus for the TSN EMAC interface.
+ * It allocates an MII bus structure, configures MDIO timing, finds
+ * the MDIO device tree node, and registers the MDIO bus with the kernel.
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int tsn_mdio_setup(struct tsn_emac *emac, struct device_node *mac_np)
+{
+ struct tsn_priv *common = emac->common;
+ struct device_node *mdio_node;
+ struct mii_bus *bus;
+ int ret;
+
+ bus = mdiobus_alloc();
+ if (!bus)
+ return -ENOMEM;
+
+ snprintf(bus->id, MII_BUS_ID_SIZE, "tsn-mac-%.8llx",
+ (unsigned long long)emac->regs_start);
+
+ bus->priv = emac;
+ bus->name = "Xilinx TSN Ethernet MDIO";
+ bus->read = tsn_mdio_read;
+ bus->write = tsn_mdio_write;
+ bus->parent = common->dev;
+ emac->mii_bus = bus;
+
+ mdio_node = of_get_child_by_name(mac_np, "mdio");
+ if (!mdio_node) {
+ dev_err(common->dev, "MAC%d: missing 'mdio' child node\n",
+ emac->emac_num);
+ ret = -ENODEV;
+ goto unregister;
+ }
+ ret = tsn_mdio_enable(emac);
+ if (ret < 0)
+ goto unregister;
+ ret = of_mdiobus_register(bus, mdio_node);
+ if (ret) {
+ dev_err(common->dev, "Failed to register MDIO bus for MAC%d\n",
+ emac->emac_num);
+ goto unregister_mdio_enabled;
+ }
+ of_node_put(mdio_node);
+ tsn_mdio_mdc_disable(emac);
+ return 0;
+
+unregister_mdio_enabled:
+ tsn_mdio_mdc_disable(emac);
+unregister:
+ of_node_put(mdio_node);
+ mdiobus_free(bus);
+ emac->mii_bus = NULL;
+ return ret;
+}
+
+/**
+ * tsn_mdio_teardown - Cleanup MDIO bus for TSN EMAC
+ * @emac: Pointer to TSN EMAC structure
+ *
+ * This function performs cleanup operations for the MDIO bus.
+ * It unregisters the MDIO bus from the kernel and frees any
+ * associated memory for the MII bus structure.
+ */
+void tsn_mdio_teardown(struct tsn_emac *emac)
+{
+ mdiobus_unregister(emac->mii_bus);
+ mdiobus_free(emac->mii_bus);
+ emac->mii_bus = NULL;
+}
--
2.25.1