* [PATCH 01/20] dt-bindings: net: add Xilinx TSN Endpoint Ethernet MAC
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 02/20] net: xilinx: tsn: add TSN endpoint wrapper driver Nagadheeraj Rottela
` (19 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
From: Srinivas Neeli <srinivas.neeli@amd.com>
Add a binding for the Xilinx TSN Endpoint Ethernet MAC IP. The
top-level node describes the IP wrapper, which owns the six shared
IP clocks and the xlnx,num-priorities parameter, and has two
children:
- xlnx,tsn-ep-mac: the endpoint MAC, owning the host-side DMA
channels (dmas / dma-names / tx-queues-config).
- xlnx,tsn-switch: the three-port Ethernet switch covering the
switch fabric and the two per-MAC register windows. The MDIO
controllers and PTP register blocks sit at fixed offsets inside
the per-MAC windows, accessed through the mdio-mac1 / mdio-mac2
children and per-MAC interrupt-names entries. Each external
port@N references its PHY through a phy-handle in the matching
mdio-mac<N> child.
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Co-developed-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
.../net/xlnx,tsn-endpoint-ethernet-mac.yaml | 343 ++++++++++++++++++
MAINTAINERS | 7 +
2 files changed, 350 insertions(+)
create mode 100644 Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
diff --git a/Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml b/Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
new file mode 100644
index 000000000000..00f52b7b623c
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
@@ -0,0 +1,343 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/xlnx,tsn-endpoint-ethernet-mac.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Xilinx TSN Endpoint Ethernet MAC
+
+description:
+ TSN Endpoint Ethernet MAC IP implements IEEE 802.1 Time-Sensitive
+ Networking (TSN) standards and provides low-latency network connectivity
+ in either Endpoint-only or Bridged Endpoint configurations. In the
+ Bridged Endpoint mode, the IP integrates a three-port switch, with two
+ ports connected to the external network and one port connected to an
+ internal endpoint. It also supports GMII/RGMII interfaces for connection
+ to an external PHY, enabling full-duplex operation at 100 Mb/s and
+ 1 Gb/s speeds.
+
+maintainers:
+ - Srinivas Neeli <srinivas.neeli@amd.com>
+ - Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
+
+properties:
+ compatible:
+ const: xlnx,tsn-endpoint-ethernet-mac-3.0
+
+ reg:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: GTX clock for transmitter logic and RTC
+ - description: GTX 90-degree phase shifted clock for RGMII
+ - description: Host RX FIFO clock
+ - description: Host TX FIFO clock
+ - description: Reference clock for IDELAY control
+ - description: AXI-Lite register interface clock
+
+ clock-names:
+ items:
+ - const: gtx
+ - const: gtx90
+ - const: host_rxfifo
+ - const: host_txfifo
+ - const: ref
+ - const: s_axi
+
+ ranges: true
+
+ xlnx,num-priorities:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 2
+ maximum: 8
+ description:
+ Number of traffic classes (priorities) configured in the IP.
+ This is an IP configuration parameter that determines the number of
+ priority queues available for QoS scheduling. Traffic classes map to
+ IEEE 802.1Q priority levels (0-7).
+
+ '#address-cells':
+ const: 1
+
+ '#size-cells':
+ const: 1
+
+patternProperties:
+ "^ep-mac@":
+ type: object
+ $ref: /schemas/net/ethernet-controller.yaml#
+ description:
+ Endpoint MAC sub-node. Provides the host-side data path through the
+ DMA channels described by its dmas / dma-names.
+ properties:
+ compatible:
+ const: xlnx,tsn-ep-mac
+
+ reg:
+ maxItems: 1
+
+ dmas:
+ minItems: 2
+ maxItems: 32
+
+ dma-names:
+ minItems: 2
+ maxItems: 32
+ items:
+ pattern: "^(tx|rx)_chan(1[0-5]|[0-9])$"
+ description: |
+ Names matching the entries in 'dmas', one per DMA channel and in
+ the same order. TX channel names use "tx_chan<N>" and RX channel
+ names use "rx_chan<N>", where N is 0..15. Example: "tx_chan0",
+ "tx_chan1", ..., "rx_chan0", "rx_chan1", ....
+
+ tx-queues-config:
+ type: object
+ description:
+ TX queue configuration node that maps IP priority queues to
+ DMA TX channels. The TSN IP supports multiple priority queues for
+ QoS scheduling, and each queue can be connected to a specific DMA
+ channel. This mapping defines which DMA TX channel is used to
+ transmit packets for each priority queue. For example, queue0
+ with xlnx,dma-channel-num set to 5 means priority queue 0 uses
+ tx_chan5 for data transfer.
+
+ patternProperties:
+ "^queue[0-7]$":
+ description:
+ Each subnode represents a priority queue. The
+ xlnx,dma-channel-num property specifies which DMA TX channel
+ (tx_chan0 to tx_chan15) is connected to this queue for
+ transmitting packets.
+ type: object
+ properties:
+ xlnx,dma-channel-num:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ description:
+ DMA TX channel number connected to this priority queue.
+ minimum: 0
+ maximum: 15
+
+ required:
+ - xlnx,dma-channel-num
+
+ additionalProperties: false
+
+ additionalProperties: false
+
+ required:
+ - compatible
+ - reg
+ - dmas
+ - dma-names
+ - tx-queues-config
+
+ unevaluatedProperties: false
+
+ "^switch@":
+ type: object
+ $ref: /schemas/net/dsa/dsa.yaml#/$defs/ethernet-ports
+ description:
+ Three-port Ethernet switch sub-node. Contains the switch fabric
+ register window and the two per-MAC register windows; each per-MAC
+ window houses an MDIO controller and a PTP register block at fixed
+ offsets. The IP's per-MAC and PTP-timer interrupts are exposed here.
+
+ properties:
+ compatible:
+ const: xlnx,tsn-switch
+
+ reg:
+ items:
+ - description: Switch fabric register window
+ - description: MAC1 register window (includes MDIO and PTP)
+ - description: MAC2 register window (includes MDIO and PTP)
+
+ reg-names:
+ items:
+ - const: switch
+ - const: mac1
+ - const: mac2
+
+ interrupts:
+ maxItems: 7
+
+ interrupt-names:
+ items:
+ - const: ptp_rx_mac1
+ - const: ptp_tx_mac1
+ - const: mac_irq_mac1
+ - const: ptp_timer
+ - const: ptp_rx_mac2
+ - const: ptp_tx_mac2
+ - const: mac_irq_mac2
+
+ mdio-mac1:
+ $ref: /schemas/net/mdio.yaml#
+ description:
+ MDIO bus controller for MAC1. Registers reside inside the mac1
+ reg-names window.
+ unevaluatedProperties: false
+
+ mdio-mac2:
+ $ref: /schemas/net/mdio.yaml#
+ description:
+ MDIO bus controller for MAC2. Registers reside inside the mac2
+ reg-names window.
+ unevaluatedProperties: false
+
+ ethernet-ports:
+ type: object
+ additionalProperties: true
+
+ properties:
+ port@0:
+ type: object
+ additionalProperties: true
+ description:
+ CPU port. Wired internally to the ep-mac sibling.
+
+ properties:
+ reg:
+ const: 0
+
+ phy-mode:
+ const: internal
+
+ patternProperties:
+ "^port@[1-2]$":
+ type: object
+ additionalProperties: true
+ description:
+ External Ethernet port (port@1 is wired to MAC1, port@2 to
+ MAC2). Its PHY is reached through the corresponding
+ mdio-mac<N> bus.
+
+ properties:
+ phy-mode:
+ enum:
+ - gmii
+ - rgmii
+ - rgmii-id
+
+ required:
+ - compatible
+ - reg
+ - reg-names
+ - interrupts
+ - interrupt-names
+
+ unevaluatedProperties: false
+
+additionalProperties: false
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - clock-names
+ - xlnx,num-priorities
+ - ranges
+ - '#address-cells'
+ - '#size-cells'
+
+examples:
+ - |
+ tsn_ip: tsn@80040000 {
+ compatible = "xlnx,tsn-endpoint-ethernet-mac-3.0";
+ reg = <0x80040000 0x40000>;
+ clocks = <&misc_clk_2>, <&misc_clk_2>, <&misc_clk_1>, <&misc_clk_1>,
+ <&misc_clk_3>, <&misc_clk_0>;
+ clock-names = "gtx", "gtx90", "host_rxfifo", "host_txfifo",
+ "ref", "s_axi";
+ xlnx,num-priorities = <8>;
+ #address-cells = <1>;
+ #size-cells = <1>;
+ ranges = <0x0 0x80040000 0x40000>;
+
+ ep_mac: ep-mac@16000 {
+ compatible = "xlnx,tsn-ep-mac";
+ reg = <0x16000 0xa000>;
+ dmas = <&axi_mcdma_0 0>, <&axi_mcdma_0 1>, <&axi_mcdma_0 2>,
+ <&axi_mcdma_0 3>, <&axi_mcdma_0 4>, <&axi_mcdma_0 5>,
+ <&axi_mcdma_0 6>, <&axi_mcdma_0 7>, <&axi_mcdma_0 16>,
+ <&axi_mcdma_0 17>, <&axi_mcdma_0 18>, <&axi_mcdma_0 19>,
+ <&axi_mcdma_0 20>, <&axi_mcdma_0 21>, <&axi_mcdma_0 22>,
+ <&axi_mcdma_0 23>;
+ dma-names = "tx_chan0", "tx_chan1", "tx_chan2", "tx_chan3",
+ "tx_chan4", "tx_chan5", "tx_chan6", "tx_chan7",
+ "rx_chan0", "rx_chan1", "rx_chan2", "rx_chan3",
+ "rx_chan4", "rx_chan5", "rx_chan6", "rx_chan7";
+ tx-queues-config {
+ queue0 { xlnx,dma-channel-num = <0x7>; };
+ queue1 { xlnx,dma-channel-num = <0x6>; };
+ queue2 { xlnx,dma-channel-num = <0x5>; };
+ queue3 { xlnx,dma-channel-num = <0x4>; };
+ queue4 { xlnx,dma-channel-num = <0x3>; };
+ queue5 { xlnx,dma-channel-num = <0x2>; };
+ queue6 { xlnx,dma-channel-num = <0x1>; };
+ queue7 { xlnx,dma-channel-num = <0x0>; };
+ };
+ };
+
+ tsn_switch: switch@38000 {
+ compatible = "xlnx,tsn-switch";
+ reg = <0x38000 0x8000>,
+ <0x00000 0x14000>,
+ <0x20000 0x14000>;
+ reg-names = "switch", "mac1", "mac2";
+
+ interrupt-parent = <&intc>;
+ interrupts = <0 2>, <2 2>, <4 2>, <6 2>,
+ <1 2>, <3 2>, <5 2>;
+ interrupt-names = "ptp_rx_mac1", "ptp_tx_mac1",
+ "mac_irq_mac1", "ptp_timer",
+ "ptp_rx_mac2", "ptp_tx_mac2",
+ "mac_irq_mac2";
+
+ mdio-mac1 {
+ #address-cells = <1>;
+ #size-cells = <0>;
+ phy0: ethernet-phy@0 {
+ reg = <0>;
+ };
+ };
+
+ mdio-mac2 {
+ #address-cells = <1>;
+ #size-cells = <0>;
+ phy1: ethernet-phy@1 {
+ reg = <1>;
+ };
+ };
+
+ ethernet-ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+ ethernet = <&ep_mac>;
+ phy-mode = "internal";
+
+ fixed-link {
+ speed = <1000>;
+ full-duplex;
+ };
+ };
+
+ port@1 {
+ reg = <1>;
+ phy-handle = <&phy0>;
+ phy-mode = "rgmii-id";
+ };
+
+ port@2 {
+ reg = <2>;
+ phy-handle = <&phy1>;
+ phy-mode = "rgmii-id";
+ };
+ };
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 08e43bc09735..ba41c224dd02 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -29607,6 +29607,13 @@ M: Harsh Jain <h.jain@amd.com>
S: Maintained
F: drivers/char/hw_random/xilinx-trng.c
+XILINX TSN ENDPOINT ETHERNET MAC DRIVER
+M: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
+M: Srinivas Neeli <srinivas.neeli@amd.com>
+L: netdev@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
+
XILINX UARTLITE SERIAL DRIVER
M: Peter Korsgaard <jacmet@sunsite.dk>
L: linux-serial@vger.kernel.org
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 02/20] net: xilinx: tsn: add TSN endpoint wrapper driver
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 01/20] dt-bindings: net: add Xilinx TSN Endpoint Ethernet MAC Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 20:58 ` Uwe Kleine-König
2026-08-07 10:44 ` [PATCH 03/20] net: xilinx: tsn: add endpoint MAC driver skeleton Nagadheeraj Rottela
` (18 subsequent siblings)
20 siblings, 1 reply; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
From: Srinivas Neeli <srinivas.neeli@amd.com>
The TSN Endpoint Ethernet MAC IP groups several sub-nodes under one IP
node with a set of IP-wide clocks. Each sub-node needs its own driver,
and the shared clocks must be running before any of them probe.
Add a thin wrapper platform driver for the IP node. Enable the IP-wide
clocks from the device tree and bind each sub-node to its own driver.
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Co-developed-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
MAINTAINERS | 1 +
drivers/net/ethernet/xilinx/Kconfig | 1 +
drivers/net/ethernet/xilinx/Makefile | 1 +
drivers/net/ethernet/xilinx/tsn/Kconfig | 15 +++
drivers/net/ethernet/xilinx/tsn/Makefile | 2 +
.../net/ethernet/xilinx/tsn/xilinx_tsn_main.c | 105 ++++++++++++++++++
6 files changed, 125 insertions(+)
create mode 100644 drivers/net/ethernet/xilinx/tsn/Kconfig
create mode 100644 drivers/net/ethernet/xilinx/tsn/Makefile
create mode 100644 drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
diff --git a/MAINTAINERS b/MAINTAINERS
index ba41c224dd02..e5b95b3bfa21 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -29613,6 +29613,7 @@ M: Srinivas Neeli <srinivas.neeli@amd.com>
L: netdev@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
+F: drivers/net/ethernet/xilinx/tsn/
XILINX UARTLITE SERIAL DRIVER
M: Peter Korsgaard <jacmet@sunsite.dk>
diff --git a/drivers/net/ethernet/xilinx/Kconfig b/drivers/net/ethernet/xilinx/Kconfig
index 7502214cc7d5..c6d704c8d3d4 100644
--- a/drivers/net/ethernet/xilinx/Kconfig
+++ b/drivers/net/ethernet/xilinx/Kconfig
@@ -41,4 +41,5 @@ config XILINX_LL_TEMAC
This driver supports the Xilinx 10/100/1000 LocalLink TEMAC
core used in Xilinx Spartan and Virtex FPGAs
+source "drivers/net/ethernet/xilinx/tsn/Kconfig"
endif # NET_VENDOR_XILINX
diff --git a/drivers/net/ethernet/xilinx/Makefile b/drivers/net/ethernet/xilinx/Makefile
index 7d7dc1771423..66dab012650b 100644
--- a/drivers/net/ethernet/xilinx/Makefile
+++ b/drivers/net/ethernet/xilinx/Makefile
@@ -8,3 +8,4 @@ obj-$(CONFIG_XILINX_LL_TEMAC) += ll_temac.o
obj-$(CONFIG_XILINX_EMACLITE) += xilinx_emaclite.o
xilinx_emac-objs := xilinx_axienet_main.o xilinx_axienet_mdio.o
obj-$(CONFIG_XILINX_AXI_EMAC) += xilinx_emac.o
+obj-$(CONFIG_XILINX_TSN) += tsn/
diff --git a/drivers/net/ethernet/xilinx/tsn/Kconfig b/drivers/net/ethernet/xilinx/tsn/Kconfig
new file mode 100644
index 000000000000..45af4d3f10e6
--- /dev/null
+++ b/drivers/net/ethernet/xilinx/tsn/Kconfig
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Xilinx TSN device configuration
+#
+
+config XILINX_TSN
+ tristate "Xilinx TSN Ethernet driver"
+ depends on OF && HAS_IOMEM
+ help
+ This driver supports the AMD/Xilinx Time-Sensitive Networking
+ (TSN) Endpoint Ethernet MAC IP. It provides the wrapper device
+ and the endpoint MAC that connects the IP to the host CPU. The
+ three-port switch is supported by NET_DSA_XILINX_TSN.
+
+ If unsure, say N.
diff --git a/drivers/net/ethernet/xilinx/tsn/Makefile b/drivers/net/ethernet/xilinx/tsn/Makefile
new file mode 100644
index 000000000000..6f99226f3dc8
--- /dev/null
+++ b/drivers/net/ethernet/xilinx/tsn/Makefile
@@ -0,0 +1,2 @@
+obj-$(CONFIG_XILINX_TSN) += xilinx_tsn.o
+xilinx_tsn-y := xilinx_tsn_main.o
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
new file mode 100644
index 000000000000..1ef1b0c2ee6e
--- /dev/null
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * Time Sensitive Networking (TSN) Ethernet MAC wrapper driver.
+ *
+ * Copyright (C) 2026 Advanced Micro Devices, Inc.
+ */
+
+#include <linux/clk.h>
+#include <linux/device.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_platform.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+
+#define TSN_NUM_CLOCKS 6
+
+/**
+ * struct xlnx_tsn_ip - wrapper-private IP state
+ * @clks: bulk-managed IP clocks
+ */
+struct xlnx_tsn_ip {
+ struct clk_bulk_data clks[TSN_NUM_CLOCKS];
+};
+
+static const char * const tsn_clk_names[TSN_NUM_CLOCKS] = {
+ "gtx",
+ "gtx90",
+ "host_rxfifo",
+ "host_txfifo",
+ "ref",
+ "s_axi",
+};
+
+static void tsn_clk_bulk_disable(void *data)
+{
+ struct xlnx_tsn_ip *w = data;
+
+ clk_bulk_disable_unprepare(TSN_NUM_CLOCKS, w->clks);
+}
+
+static int tsn_ip_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct xlnx_tsn_ip *w;
+ int ret;
+
+ w = devm_kzalloc(dev, sizeof(*w), GFP_KERNEL);
+ if (!w)
+ return -ENOMEM;
+
+ for (int i = 0; i < TSN_NUM_CLOCKS; i++)
+ w->clks[i].id = tsn_clk_names[i];
+
+ ret = devm_clk_bulk_get(dev, TSN_NUM_CLOCKS, w->clks);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to get clocks\n");
+
+ ret = clk_bulk_prepare_enable(TSN_NUM_CLOCKS, w->clks);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to enable clocks\n");
+
+ ret = devm_add_action_or_reset(dev, tsn_clk_bulk_disable, w);
+ if (ret)
+ return ret;
+
+ return devm_of_platform_populate(dev);
+}
+
+static const struct of_device_id tsn_of_match[] = {
+ { .compatible = "xlnx,tsn-endpoint-ethernet-mac-3.0" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, tsn_of_match);
+
+static struct platform_driver tsn_driver = {
+ .probe = tsn_ip_probe,
+ .driver = {
+ .name = "xilinx-tsn",
+ .of_match_table = tsn_of_match,
+ },
+};
+
+static struct platform_driver * const tsn_drivers[] = {
+ &tsn_driver,
+};
+
+static int __init xlnx_tsn_init(void)
+{
+ return platform_register_drivers(tsn_drivers, ARRAY_SIZE(tsn_drivers));
+}
+module_init(xlnx_tsn_init);
+
+static void __exit xlnx_tsn_exit(void)
+{
+ platform_unregister_drivers(tsn_drivers, ARRAY_SIZE(tsn_drivers));
+}
+module_exit(xlnx_tsn_exit);
+
+MODULE_AUTHOR("Srinivas Neeli <srinivas.neeli@amd.com>");
+MODULE_DESCRIPTION("AMD/Xilinx TSN Endpoint Ethernet MAC wrapper driver");
+MODULE_LICENSE("GPL");
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH 02/20] net: xilinx: tsn: add TSN endpoint wrapper driver
2026-08-07 10:44 ` [PATCH 02/20] net: xilinx: tsn: add TSN endpoint wrapper driver Nagadheeraj Rottela
@ 2026-08-07 20:58 ` Uwe Kleine-König
0 siblings, 0 replies; 24+ messages in thread
From: Uwe Kleine-König @ 2026-08-07 20:58 UTC (permalink / raw)
To: Nagadheeraj Rottela
Cc: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King, netdev, devicetree,
linux-kernel, linux-arm-kernel, git-dev
[-- Attachment #1: Type: text/plain, Size: 1472 bytes --]
Hello,
On Fri, Aug 07, 2026 at 04:14:13PM +0530, Nagadheeraj Rottela wrote:
> diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
> new file mode 100644
> index 000000000000..1ef1b0c2ee6e
> --- /dev/null
> +++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
> @@ -0,0 +1,105 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +/*
> + * Time Sensitive Networking (TSN) Ethernet MAC wrapper driver.
> + *
> + * Copyright (C) 2026 Advanced Micro Devices, Inc.
> + */
> +
> +#include <linux/clk.h>
> +#include <linux/device.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/of_platform.h>
> +#include <linux/platform_device.h>
> +#include <linux/slab.h>
> +#include <linux/types.h>
Please drop <linux/mod_devicetable.h>. This header is planned to go away
soon and you can rely on <linux/platform_device.h> to provide
of_device_id.
> [...]
> +static struct platform_driver * const tsn_drivers[] = {
> + &tsn_driver,
> +};
> +
> +static int __init xlnx_tsn_init(void)
> +{
> + return platform_register_drivers(tsn_drivers, ARRAY_SIZE(tsn_drivers));
> +}
> +module_init(xlnx_tsn_init);
> +
> +static void __exit xlnx_tsn_exit(void)
> +{
> + platform_unregister_drivers(tsn_drivers, ARRAY_SIZE(tsn_drivers));
> +}
> +module_exit(xlnx_tsn_exit);
I assume you expect tsn_drivers[] to grow? If not, use
module_platform_driver() to reduce boilerplate.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH 03/20] net: xilinx: tsn: add endpoint MAC driver skeleton
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 01/20] dt-bindings: net: add Xilinx TSN Endpoint Ethernet MAC Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 02/20] net: xilinx: tsn: add TSN endpoint wrapper driver Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 21:00 ` Uwe Kleine-König
2026-08-07 10:44 ` [PATCH 04/20] net: xilinx: tsn: parse endpoint DMA channel configuration Nagadheeraj Rottela
` (17 subsequent siblings)
20 siblings, 1 reply; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
From: Srinivas Neeli <srinivas.neeli@amd.com>
The TSN Endpoint MAC owns the IP's host-side MCDMA data path and is the
netdev physically wired to the CPU. The DSA switch needs this netdev to
exist as its conduit.
Add a platform driver (compatible "xlnx,tsn-ep-mac") for the endpoint.
Register the netdev named "ep", set its MAC address, and provide minimal
netdev and ethtool ops. This skeleton handles device bring-up only, with
no data path yet. So ndo_open just starts the queues and ndo_start_xmit
drops frames.
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Co-developed-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
drivers/net/ethernet/xilinx/tsn/Makefile | 2 +-
drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h | 15 ++
.../net/ethernet/xilinx/tsn/xilinx_tsn_ep.c | 152 ++++++++++++++++++
.../net/ethernet/xilinx/tsn/xilinx_tsn_main.c | 3 +
4 files changed, 171 insertions(+), 1 deletion(-)
create mode 100644 drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h
create mode 100644 drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
diff --git a/drivers/net/ethernet/xilinx/tsn/Makefile b/drivers/net/ethernet/xilinx/tsn/Makefile
index 6f99226f3dc8..5886828b386f 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-y := xilinx_tsn_main.o
+xilinx_tsn-y := xilinx_tsn_main.o xilinx_tsn_ep.o
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h
new file mode 100644
index 000000000000..b0757e22d1fd
--- /dev/null
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * AMD/Xilinx TSN Endpoint Ethernet MAC driver, shared definitions.
+ *
+ * Copyright (C) 2026 Advanced Micro Devices, Inc.
+ */
+
+#ifndef _XILINX_TSN_H
+#define _XILINX_TSN_H
+
+#include <linux/platform_device.h>
+
+extern struct platform_driver xlnx_tsn_ep_driver;
+
+#endif /* _XILINX_TSN_H */
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
new file mode 100644
index 000000000000..9b556edf5423
--- /dev/null
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * AMD/Xilinx TSN Endpoint MAC driver.
+ *
+ * Copyright (C) 2026 Advanced Micro Devices, Inc.
+ */
+
+#include <linux/etherdevice.h>
+#include <linux/ethtool.h>
+#include <linux/if_ether.h>
+#include <linux/kernel.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <linux/of.h>
+#include <linux/of_net.h>
+#include <linux/platform_device.h>
+#include <linux/string.h>
+#include <linux/types.h>
+
+#include "xilinx_tsn.h"
+
+#define DRIVER_NAME "xilinx_tsn_ep"
+
+/**
+ * struct xlnx_tsn_ep - EP MAC private data, embedded in net_device priv area
+ * @ndev: the conduit netdev ("ep")
+ * @dev: backing device
+ * @regs: EP MAC register window
+ */
+struct xlnx_tsn_ep {
+ struct net_device *ndev;
+ struct device *dev;
+ void __iomem *regs;
+};
+
+static netdev_tx_t ep_start_xmit(struct sk_buff *skb, struct net_device *ndev)
+{
+ dev_kfree_skb(skb);
+ DEV_STATS_INC(ndev, tx_dropped);
+ return NETDEV_TX_OK;
+}
+
+static int ep_open(struct net_device *ndev)
+{
+ netif_tx_start_all_queues(ndev);
+
+ return 0;
+}
+
+static int ep_stop(struct net_device *ndev)
+{
+ netif_tx_disable(ndev);
+
+ return 0;
+}
+
+static void ep_get_drvinfo(struct net_device *ndev, struct ethtool_drvinfo *ed)
+{
+ strscpy(ed->driver, DRIVER_NAME, sizeof(ed->driver));
+}
+
+static const struct net_device_ops ep_netdev_ops = {
+ .ndo_open = ep_open,
+ .ndo_stop = ep_stop,
+ .ndo_start_xmit = ep_start_xmit,
+ .ndo_validate_addr = eth_validate_addr,
+ .ndo_set_mac_address = eth_mac_addr,
+};
+
+static const struct ethtool_ops ep_ethtool_ops = {
+ .get_drvinfo = ep_get_drvinfo,
+};
+
+static int xlnx_tsn_ep_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct net_device *ndev;
+ struct xlnx_tsn_ep *ep;
+ u8 mac_addr[ETH_ALEN];
+ int ret;
+
+ ndev = alloc_netdev(sizeof(*ep), "ep", NET_NAME_UNKNOWN, ether_setup);
+ if (!ndev)
+ return -ENOMEM;
+
+ SET_NETDEV_DEV(ndev, dev);
+ ndev->netdev_ops = &ep_netdev_ops;
+ ndev->ethtool_ops = &ep_ethtool_ops;
+ ndev->features = NETIF_F_SG;
+
+ ep = netdev_priv(ndev);
+ ep->ndev = ndev;
+ ep->dev = dev;
+
+ ep->regs = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(ep->regs)) {
+ ret = PTR_ERR(ep->regs);
+ goto err_free_ndev;
+ }
+
+ ret = of_get_mac_address(dev->of_node, mac_addr);
+ if (ret == -EPROBE_DEFER) {
+ goto err_free_ndev;
+ } else if (!ret && is_valid_ether_addr(mac_addr)) {
+ eth_hw_addr_set(ndev, mac_addr);
+ } else {
+ eth_hw_addr_random(ndev);
+ dev_info(dev, "no valid MAC in DT, using random address %pM\n",
+ ndev->dev_addr);
+ }
+
+ platform_set_drvdata(pdev, ep);
+
+ ret = register_netdev(ndev);
+ if (ret) {
+ dev_err_probe(dev, ret, "failed to register net device\n");
+ goto err_free_ndev;
+ }
+
+ return 0;
+
+err_free_ndev:
+ free_netdev(ndev);
+ return ret;
+}
+
+static void xlnx_tsn_ep_remove(struct platform_device *pdev)
+{
+ struct xlnx_tsn_ep *ep = platform_get_drvdata(pdev);
+
+ if (!ep)
+ return;
+
+ unregister_netdev(ep->ndev);
+ free_netdev(ep->ndev);
+}
+
+static const struct of_device_id xlnx_tsn_ep_of_match[] = {
+ { .compatible = "xlnx,tsn-ep-mac" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, xlnx_tsn_ep_of_match);
+
+struct platform_driver xlnx_tsn_ep_driver = {
+ .probe = xlnx_tsn_ep_probe,
+ .remove = xlnx_tsn_ep_remove,
+ .driver = {
+ .name = DRIVER_NAME,
+ .of_match_table = xlnx_tsn_ep_of_match,
+ },
+};
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
index 1ef1b0c2ee6e..3df8c026741f 100644
--- a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
@@ -16,6 +16,8 @@
#include <linux/slab.h>
#include <linux/types.h>
+#include "xilinx_tsn.h"
+
#define TSN_NUM_CLOCKS 6
/**
@@ -86,6 +88,7 @@ static struct platform_driver tsn_driver = {
static struct platform_driver * const tsn_drivers[] = {
&tsn_driver,
+ &xlnx_tsn_ep_driver,
};
static int __init xlnx_tsn_init(void)
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH 03/20] net: xilinx: tsn: add endpoint MAC driver skeleton
2026-08-07 10:44 ` [PATCH 03/20] net: xilinx: tsn: add endpoint MAC driver skeleton Nagadheeraj Rottela
@ 2026-08-07 21:00 ` Uwe Kleine-König
0 siblings, 0 replies; 24+ messages in thread
From: Uwe Kleine-König @ 2026-08-07 21:00 UTC (permalink / raw)
To: Nagadheeraj Rottela
Cc: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King, netdev, devicetree,
linux-kernel, linux-arm-kernel, git-dev
[-- Attachment #1: Type: text/plain, Size: 3219 bytes --]
On Fri, Aug 07, 2026 at 04:14:14PM +0530, Nagadheeraj Rottela wrote:
> From: Srinivas Neeli <srinivas.neeli@amd.com>
>
> The TSN Endpoint MAC owns the IP's host-side MCDMA data path and is the
> netdev physically wired to the CPU. The DSA switch needs this netdev to
> exist as its conduit.
>
> Add a platform driver (compatible "xlnx,tsn-ep-mac") for the endpoint.
> Register the netdev named "ep", set its MAC address, and provide minimal
> netdev and ethtool ops. This skeleton handles device bring-up only, with
> no data path yet. So ndo_open just starts the queues and ndo_start_xmit
> drops frames.
>
> Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
> Co-developed-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
> Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
> ---
> drivers/net/ethernet/xilinx/tsn/Makefile | 2 +-
> drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h | 15 ++
> .../net/ethernet/xilinx/tsn/xilinx_tsn_ep.c | 152 ++++++++++++++++++
> .../net/ethernet/xilinx/tsn/xilinx_tsn_main.c | 3 +
> 4 files changed, 171 insertions(+), 1 deletion(-)
> create mode 100644 drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h
> create mode 100644 drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
>
> diff --git a/drivers/net/ethernet/xilinx/tsn/Makefile b/drivers/net/ethernet/xilinx/tsn/Makefile
> index 6f99226f3dc8..5886828b386f 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-y := xilinx_tsn_main.o
> +xilinx_tsn-y := xilinx_tsn_main.o xilinx_tsn_ep.o
> diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h
> new file mode 100644
> index 000000000000..b0757e22d1fd
> --- /dev/null
> +++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h
> @@ -0,0 +1,15 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * AMD/Xilinx TSN Endpoint Ethernet MAC driver, shared definitions.
> + *
> + * Copyright (C) 2026 Advanced Micro Devices, Inc.
> + */
> +
> +#ifndef _XILINX_TSN_H
> +#define _XILINX_TSN_H
> +
> +#include <linux/platform_device.h>
> +
> +extern struct platform_driver xlnx_tsn_ep_driver;
> +
> +#endif /* _XILINX_TSN_H */
> diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
> new file mode 100644
> index 000000000000..9b556edf5423
> --- /dev/null
> +++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
> @@ -0,0 +1,152 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * AMD/Xilinx TSN Endpoint MAC driver.
> + *
> + * Copyright (C) 2026 Advanced Micro Devices, Inc.
> + */
> +
> +#include <linux/etherdevice.h>
> +#include <linux/ethtool.h>
> +#include <linux/if_ether.h>
> +#include <linux/kernel.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/module.h>
> +#include <linux/netdevice.h>
> +#include <linux/of.h>
> +#include <linux/of_net.h>
> +#include <linux/platform_device.h>
> +#include <linux/string.h>
> +#include <linux/types.h>
As for patch #2: Please drop <linux/mod_devicetable.h>.
Best regards
Uwe
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH 04/20] net: xilinx: tsn: parse endpoint DMA channel configuration
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (2 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 03/20] net: xilinx: tsn: add endpoint MAC driver skeleton Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 05/20] net: xilinx: tsn: bring up the endpoint MCDMA channels Nagadheeraj Rottela
` (16 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
From: Srinivas Neeli <srinivas.neeli@amd.com>
The endpoint has one MCDMA channel per TX queue and per RX queue, and
the device tree lists them in "dma-names". The transmit path needs to
know which physical DMA channel backs each logical TX queue.
Count the TX and RX channels in "dma-names" and allocate the conduit
netdev with that many hardware queues. Parse the "tx-queues-config"
child node to map each logical TX queue to its physical DMA channel.
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Co-developed-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
.../net/ethernet/xilinx/tsn/xilinx_tsn_ep.c | 138 +++++++++++++++++-
1 file changed, 137 insertions(+), 1 deletion(-)
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
index 9b556edf5423..062cb94c2153 100644
--- a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
@@ -5,9 +5,11 @@
* Copyright (C) 2026 Advanced Micro Devices, Inc.
*/
+#include <linux/bitops.h>
#include <linux/etherdevice.h>
#include <linux/ethtool.h>
#include <linux/if_ether.h>
+#include <linux/if_vlan.h>
#include <linux/kernel.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
@@ -22,16 +24,32 @@
#define DRIVER_NAME "xilinx_tsn_ep"
+#define TSN_DMA_CH_INVALID 0xFFU
+#define TSN_MAX_TX_QUEUE 8
+#define TSN_MAX_RX_QUEUE 16
+
+#define TSN_MAX_VLAN_FRAME_SIZE (ETH_DATA_LEN + VLAN_ETH_HLEN + \
+ ETH_FCS_LEN)
+
/**
* struct xlnx_tsn_ep - EP MAC private data, embedded in net_device priv area
* @ndev: the conduit netdev ("ep")
* @dev: backing device
* @regs: EP MAC register window
+ * @num_tx_queues: number of TX DMA channels (one per priority)
+ * @num_rx_queues: number of RX DMA channels
+ * @tx_dma_chan_map: logical TX queue index -> physical DMA channel number
+ * @max_frm_size: maximum frame size accepted on RX
*/
struct xlnx_tsn_ep {
struct net_device *ndev;
struct device *dev;
void __iomem *regs;
+
+ u32 num_tx_queues;
+ u32 num_rx_queues;
+ u32 tx_dma_chan_map[TSN_MAX_TX_QUEUE];
+ u32 max_frm_size;
};
static netdev_tx_t ep_start_xmit(struct sk_buff *skb, struct net_device *ndev)
@@ -72,15 +90,116 @@ static const struct ethtool_ops ep_ethtool_ops = {
.get_drvinfo = ep_get_drvinfo,
};
+/*
+ * Parse the "tx-queues-config" child of the EP node. The logical queue
+ * index is taken from the "queue<N>" node name, so the mapping does not
+ * depend on the order the child nodes appear in the device tree.
+ */
+static int ep_parse_tx_queue_config(struct xlnx_tsn_ep *ep,
+ struct device_node *txcfg_np)
+{
+ DECLARE_BITMAP(chan_seen, TSN_MAX_TX_QUEUE) = {};
+ DECLARE_BITMAP(queue_seen, TSN_MAX_TX_QUEUE) = {};
+ unsigned int count = 0;
+ int ret;
+
+ for_each_child_of_node_scoped(txcfg_np, qnode) {
+ u32 chan, queue;
+
+ if (!str_has_prefix(qnode->name, "queue") ||
+ kstrtou32(qnode->name + strlen("queue"), 10, &queue) ||
+ queue >= ep->num_tx_queues)
+ return dev_err_probe(ep->dev, -EINVAL,
+ "tx-config: invalid queue node %pOFn (have %u queues)\n",
+ qnode, ep->num_tx_queues);
+
+ if (test_and_set_bit(queue, queue_seen))
+ return dev_err_probe(ep->dev, -EINVAL,
+ "tx-config: queue %u described twice\n",
+ queue);
+
+ ret = of_property_read_u32(qnode, "xlnx,dma-channel-num", &chan);
+ if (ret)
+ return dev_err_probe(ep->dev, ret,
+ "tx-config: queue %u missing xlnx,dma-channel-num\n",
+ queue);
+
+ if (chan >= ep->num_tx_queues)
+ return dev_err_probe(ep->dev, -EINVAL,
+ "tx-config: queue %u channel %u has no matching tx_chan (have %u)\n",
+ queue, chan, ep->num_tx_queues);
+
+ if (test_and_set_bit(chan, chan_seen))
+ return dev_err_probe(ep->dev, -EINVAL,
+ "tx-config: channel %u already assigned to another queue\n",
+ chan);
+
+ ep->tx_dma_chan_map[queue] = chan;
+ count++;
+ }
+
+ if (count != ep->num_tx_queues)
+ return dev_err_probe(ep->dev, -EINVAL,
+ "tx-config: described %u queues but expected %u\n",
+ count, ep->num_tx_queues);
+
+ return 0;
+}
+
+static int ep_count_dma_queues(struct device *dev, u32 *out_tx, u32 *out_rx)
+{
+ u32 tx = 0, rx = 0;
+ int n, i;
+
+ n = of_property_count_strings(dev->of_node, "dma-names");
+ if (n < 0)
+ return dev_err_probe(dev, n, "failed to read dma-names\n");
+
+ for (i = 0; i < n; i++) {
+ const char *name;
+
+ if (of_property_read_string_index(dev->of_node, "dma-names",
+ i, &name))
+ continue;
+ if (str_has_prefix(name, "tx_chan"))
+ tx++;
+ else if (str_has_prefix(name, "rx_chan"))
+ rx++;
+ }
+
+ if (!tx || tx > TSN_MAX_TX_QUEUE)
+ return dev_err_probe(dev, -EINVAL,
+ "invalid TX queue count (%u, max %u)\n",
+ tx, TSN_MAX_TX_QUEUE);
+
+ if (!rx || rx > TSN_MAX_RX_QUEUE)
+ return dev_err_probe(dev, -EINVAL,
+ "invalid RX queue count (%u, max %u)\n",
+ rx, TSN_MAX_RX_QUEUE);
+
+ *out_tx = tx;
+ *out_rx = rx;
+
+ return 0;
+}
+
static int xlnx_tsn_ep_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
+ struct device_node *txcfg_np;
struct net_device *ndev;
struct xlnx_tsn_ep *ep;
u8 mac_addr[ETH_ALEN];
+ u32 num_tx, num_rx;
int ret;
+ int i;
- ndev = alloc_netdev(sizeof(*ep), "ep", NET_NAME_UNKNOWN, ether_setup);
+ ret = ep_count_dma_queues(dev, &num_tx, &num_rx);
+ if (ret)
+ return ret;
+
+ ndev = alloc_netdev_mqs(sizeof(*ep), "ep", NET_NAME_UNKNOWN,
+ ether_setup, num_tx, num_rx);
if (!ndev)
return -ENOMEM;
@@ -92,6 +211,12 @@ static int xlnx_tsn_ep_probe(struct platform_device *pdev)
ep = netdev_priv(ndev);
ep->ndev = ndev;
ep->dev = dev;
+ ep->num_tx_queues = num_tx;
+ ep->num_rx_queues = num_rx;
+ ep->max_frm_size = TSN_MAX_VLAN_FRAME_SIZE;
+
+ for (i = 0; i < TSN_MAX_TX_QUEUE; i++)
+ ep->tx_dma_chan_map[i] = TSN_DMA_CH_INVALID;
ep->regs = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(ep->regs)) {
@@ -99,6 +224,17 @@ static int xlnx_tsn_ep_probe(struct platform_device *pdev)
goto err_free_ndev;
}
+ txcfg_np = of_get_child_by_name(dev->of_node, "tx-queues-config");
+ if (!txcfg_np) {
+ ret = dev_err_probe(dev, -EINVAL,
+ "missing tx-queues-config node\n");
+ goto err_free_ndev;
+ }
+ ret = ep_parse_tx_queue_config(ep, txcfg_np);
+ of_node_put(txcfg_np);
+ if (ret)
+ goto err_free_ndev;
+
ret = of_get_mac_address(dev->of_node, mac_addr);
if (ret == -EPROBE_DEFER) {
goto err_free_ndev;
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 05/20] net: xilinx: tsn: bring up the endpoint MCDMA channels
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (3 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 04/20] net: xilinx: tsn: parse endpoint DMA channel configuration Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 06/20] net: xilinx: tsn: add the endpoint RX data path Nagadheeraj Rottela
` (15 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
From: Srinivas Neeli <srinivas.neeli@amd.com>
Before the endpoint can move frames, each MCDMA channel needs a ring of
buffers and a live dmaengine channel.
Request every TX and RX channel named in the device tree when the
interface opens, and give each one a ring of preallocated SKB
descriptor slots. Reset the controller through tx_chan0 on open, and
release the channels on stop. Add the XILINX_DMA dependency for the
dmaengine and Xilinx DMA APIs.
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Co-developed-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
drivers/net/ethernet/xilinx/tsn/Kconfig | 1 +
.../net/ethernet/xilinx/tsn/xilinx_tsn_ep.c | 274 ++++++++++++++++++
2 files changed, 275 insertions(+)
diff --git a/drivers/net/ethernet/xilinx/tsn/Kconfig b/drivers/net/ethernet/xilinx/tsn/Kconfig
index 45af4d3f10e6..551139a220ef 100644
--- a/drivers/net/ethernet/xilinx/tsn/Kconfig
+++ b/drivers/net/ethernet/xilinx/tsn/Kconfig
@@ -6,6 +6,7 @@
config XILINX_TSN
tristate "Xilinx TSN Ethernet driver"
depends on OF && HAS_IOMEM
+ depends on XILINX_DMA
help
This driver supports the AMD/Xilinx Time-Sensitive Networking
(TSN) Endpoint Ethernet MAC IP. It provides the wrapper device
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
index 062cb94c2153..fa21d59ade16 100644
--- a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
@@ -6,10 +6,15 @@
*/
#include <linux/bitops.h>
+#include <linux/circ_buf.h>
+#include <linux/dma/xilinx_dma.h>
+#include <linux/dma-mapping.h>
+#include <linux/dmaengine.h>
#include <linux/etherdevice.h>
#include <linux/ethtool.h>
#include <linux/if_ether.h>
#include <linux/if_vlan.h>
+#include <linux/io.h>
#include <linux/kernel.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
@@ -17,6 +22,9 @@
#include <linux/of.h>
#include <linux/of_net.h>
#include <linux/platform_device.h>
+#include <linux/scatterlist.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
#include <linux/string.h>
#include <linux/types.h>
@@ -31,6 +39,47 @@
#define TSN_MAX_VLAN_FRAME_SIZE (ETH_DATA_LEN + VLAN_ETH_HLEN + \
ETH_FCS_LEN)
+#define TX_BD_NUM_DEFAULT 64
+#define RX_BD_NUM_DEFAULT 128
+
+/**
+ * struct skbuf_dma_descriptor - skb container for each in-flight DMA descriptor
+ * @sgl: scatter-gather list backing the DMA mapping
+ * @desc: dmaengine descriptor handle
+ * @dma_address: physical address of the first sgl entry (RX path)
+ * @skb: SKB owning the buffer
+ * @sg_len: number of valid entries in @sgl (TX path)
+ */
+struct skbuf_dma_descriptor {
+ struct scatterlist sgl[MAX_SKB_FRAGS + 1];
+ struct dma_async_tx_descriptor *desc;
+ dma_addr_t dma_address;
+ struct sk_buff *skb;
+ int sg_len;
+};
+
+/**
+ * struct xlnx_tsn_ep_dma_chan - one DMA channel and its SKB ring
+ * @skb_ring: per-slot SKB descriptors
+ * @ep: pointer back to the owning EP instance
+ * @chan: dmaengine channel handle
+ * @dma_dev: device used for DMA mapping (the DMA engine, not the EP)
+ * @ring_head: producer index
+ * @ring_tail: consumer index
+ * @ring_size: number of slots in @skb_ring
+ * @is_tx: true for TX channels, false for RX
+ */
+struct xlnx_tsn_ep_dma_chan {
+ struct skbuf_dma_descriptor **skb_ring;
+ struct xlnx_tsn_ep *ep;
+ struct dma_chan *chan;
+ struct device *dma_dev;
+ u32 ring_head;
+ u32 ring_tail;
+ u32 ring_size;
+ bool is_tx;
+};
+
/**
* struct xlnx_tsn_ep - EP MAC private data, embedded in net_device priv area
* @ndev: the conduit netdev ("ep")
@@ -40,6 +89,9 @@
* @num_rx_queues: number of RX DMA channels
* @tx_dma_chan_map: logical TX queue index -> physical DMA channel number
* @max_frm_size: maximum frame size accepted on RX
+ * @tx_chans: array of TX channels (size @num_tx_queues)
+ * @rx_chans: array of RX channels (size @num_rx_queues)
+ * @closing: set in ndo_stop so the RX completion callback stops re-arming
*/
struct xlnx_tsn_ep {
struct net_device *ndev;
@@ -50,8 +102,19 @@ struct xlnx_tsn_ep {
u32 num_rx_queues;
u32 tx_dma_chan_map[TSN_MAX_TX_QUEUE];
u32 max_frm_size;
+
+ struct xlnx_tsn_ep_dma_chan **tx_chans;
+ struct xlnx_tsn_ep_dma_chan **rx_chans;
+
+ bool closing;
};
+static inline struct skbuf_dma_descriptor *
+ep_get_desc(struct xlnx_tsn_ep_dma_chan *xchan, int idx)
+{
+ return xchan->skb_ring[idx];
+}
+
static netdev_tx_t ep_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
dev_kfree_skb(skb);
@@ -59,8 +122,27 @@ static netdev_tx_t ep_start_xmit(struct sk_buff *skb, struct net_device *ndev)
return NETDEV_TX_OK;
}
+static int ep_reset_dma_controller(struct xlnx_tsn_ep *ep);
+static int ep_init_dmaengine(struct xlnx_tsn_ep *ep);
+static void ep_exit_dmaengine(struct xlnx_tsn_ep *ep);
+
static int ep_open(struct net_device *ndev)
{
+ struct xlnx_tsn_ep *ep = netdev_priv(ndev);
+ int ret;
+
+ WRITE_ONCE(ep->closing, false);
+
+ ret = ep_reset_dma_controller(ep);
+ if (ret)
+ return ret;
+
+ ret = ep_init_dmaengine(ep);
+ if (ret) {
+ netdev_err(ndev, "failed to initialize DMA engine\n");
+ return ret;
+ }
+
netif_tx_start_all_queues(ndev);
return 0;
@@ -68,7 +150,11 @@ static int ep_open(struct net_device *ndev)
static int ep_stop(struct net_device *ndev)
{
+ struct xlnx_tsn_ep *ep = netdev_priv(ndev);
+
netif_tx_disable(ndev);
+ WRITE_ONCE(ep->closing, true);
+ ep_exit_dmaengine(ep);
return 0;
}
@@ -90,6 +176,194 @@ static const struct ethtool_ops ep_ethtool_ops = {
.get_drvinfo = ep_get_drvinfo,
};
+static struct xlnx_tsn_ep_dma_chan *
+ep_alloc_dma_chan(struct xlnx_tsn_ep *ep, const char *name, bool is_tx,
+ int ring_size)
+{
+ struct xlnx_tsn_ep_dma_chan *chan;
+ struct dma_chan *err_chan;
+ int i;
+
+ chan = kzalloc_obj(*chan);
+ if (!chan)
+ return ERR_PTR(-ENOMEM);
+
+ chan->chan = dma_request_chan(ep->dev, name);
+ if (IS_ERR(chan->chan)) {
+ err_chan = chan->chan;
+ kfree(chan);
+ return ERR_CAST(err_chan);
+ }
+
+ chan->skb_ring = kcalloc(ring_size, sizeof(*chan->skb_ring), GFP_KERNEL);
+ if (!chan->skb_ring) {
+ dma_release_channel(chan->chan);
+ kfree(chan);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ for (i = 0; i < ring_size; i++) {
+ chan->skb_ring[i] = kzalloc_obj(*chan->skb_ring[i]);
+ if (!chan->skb_ring[i]) {
+ while (--i >= 0)
+ kfree(chan->skb_ring[i]);
+ kfree(chan->skb_ring);
+ dma_release_channel(chan->chan);
+ kfree(chan);
+ return ERR_PTR(-ENOMEM);
+ }
+ }
+
+ chan->is_tx = is_tx;
+ chan->ep = ep;
+ chan->ring_size = ring_size;
+ chan->dma_dev = dmaengine_get_dma_device(chan->chan);
+
+ return chan;
+}
+
+static void ep_free_dma_chan(struct xlnx_tsn_ep_dma_chan *chan)
+{
+ int i;
+
+ if (!chan)
+ return;
+
+ if (chan->chan)
+ dmaengine_terminate_sync(chan->chan);
+
+ if (chan->is_tx) {
+ while (chan->ring_tail != chan->ring_head) {
+ struct skbuf_dma_descriptor *skbuf_dma;
+
+ skbuf_dma = chan->skb_ring[chan->ring_tail &
+ (chan->ring_size - 1)];
+ if (skbuf_dma && skbuf_dma->skb) {
+ dma_unmap_sg(chan->dma_dev, skbuf_dma->sgl,
+ skbuf_dma->sg_len, DMA_TO_DEVICE);
+ dev_kfree_skb_any(skbuf_dma->skb);
+ skbuf_dma->skb = NULL;
+ }
+ chan->ring_tail++;
+ }
+ }
+
+ if (chan->skb_ring) {
+ for (i = 0; i < chan->ring_size; i++) {
+ struct skbuf_dma_descriptor *skbuf_dma = chan->skb_ring[i];
+
+ if (skbuf_dma && !chan->is_tx && skbuf_dma->skb) {
+ dma_unmap_single(chan->dma_dev,
+ skbuf_dma->dma_address,
+ chan->ep->max_frm_size,
+ DMA_FROM_DEVICE);
+ dev_kfree_skb_any(skbuf_dma->skb);
+ }
+ kfree(chan->skb_ring[i]);
+ }
+ kfree(chan->skb_ring);
+ }
+ if (chan->chan)
+ dma_release_channel(chan->chan);
+
+ kfree(chan);
+}
+
+static void ep_exit_dmaengine(struct xlnx_tsn_ep *ep)
+{
+ int i;
+
+ if (ep->tx_chans) {
+ for (i = 0; i < ep->num_tx_queues; i++)
+ ep_free_dma_chan(ep->tx_chans[i]);
+ kfree(ep->tx_chans);
+ ep->tx_chans = NULL;
+ }
+ if (ep->rx_chans) {
+ for (i = 0; i < ep->num_rx_queues; i++)
+ ep_free_dma_chan(ep->rx_chans[i]);
+ kfree(ep->rx_chans);
+ ep->rx_chans = NULL;
+ }
+}
+
+static int ep_init_dmaengine(struct xlnx_tsn_ep *ep)
+{
+ int tx_allocated = 0, rx_allocated = 0;
+ char name[16];
+ int i, ret;
+
+ ep->tx_chans = kcalloc(ep->num_tx_queues, sizeof(*ep->tx_chans),
+ GFP_KERNEL);
+ if (!ep->tx_chans)
+ return -ENOMEM;
+
+ ep->rx_chans = kcalloc(ep->num_rx_queues, sizeof(*ep->rx_chans),
+ GFP_KERNEL);
+ if (!ep->rx_chans) {
+ ret = -ENOMEM;
+ goto err_free_tx;
+ }
+
+ for (i = 0; i < ep->num_tx_queues; i++) {
+ snprintf(name, sizeof(name), "tx_chan%d", i);
+ ep->tx_chans[i] = ep_alloc_dma_chan(ep, name, true,
+ TX_BD_NUM_DEFAULT);
+ if (IS_ERR(ep->tx_chans[i])) {
+ ret = PTR_ERR(ep->tx_chans[i]);
+ ep->tx_chans[i] = NULL;
+ goto err_free_chans;
+ }
+ tx_allocated++;
+ }
+
+ for (i = 0; i < ep->num_rx_queues; i++) {
+ snprintf(name, sizeof(name), "rx_chan%d", i);
+ ep->rx_chans[i] = ep_alloc_dma_chan(ep, name, false,
+ RX_BD_NUM_DEFAULT);
+ if (IS_ERR(ep->rx_chans[i])) {
+ ret = PTR_ERR(ep->rx_chans[i]);
+ ep->rx_chans[i] = NULL;
+ goto err_free_chans;
+ }
+ rx_allocated++;
+ }
+
+ return 0;
+
+err_free_chans:
+ while (--rx_allocated >= 0)
+ ep_free_dma_chan(ep->rx_chans[rx_allocated]);
+ while (--tx_allocated >= 0)
+ ep_free_dma_chan(ep->tx_chans[tx_allocated]);
+ kfree(ep->rx_chans);
+ ep->rx_chans = NULL;
+err_free_tx:
+ kfree(ep->tx_chans);
+ ep->tx_chans = NULL;
+ return ret;
+}
+
+static int ep_reset_dma_controller(struct xlnx_tsn_ep *ep)
+{
+ struct xilinx_vdma_config cfg = { .reset = 1 };
+ struct dma_chan *tx_chan0;
+ int ret;
+
+ tx_chan0 = dma_request_chan(ep->dev, "tx_chan0");
+ if (IS_ERR(tx_chan0))
+ return dev_err_probe(ep->dev, PTR_ERR(tx_chan0),
+ "failed to request tx_chan0 for reset\n");
+
+ ret = xilinx_vdma_channel_set_config(tx_chan0, &cfg);
+ dma_release_channel(tx_chan0);
+ if (ret < 0)
+ return dev_err_probe(ep->dev, ret,
+ "failed to reset DMA controller\n");
+
+ return 0;
+}
+
/*
* Parse the "tx-queues-config" child of the EP node. The logical queue
* index is taken from the "queue<N>" node name, so the mapping does not
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 06/20] net: xilinx: tsn: add the endpoint RX data path
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (4 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 05/20] net: xilinx: tsn: bring up the endpoint MCDMA channels Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 07/20] net: xilinx: tsn: add the endpoint TX " Nagadheeraj Rottela
` (14 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
From: Srinivas Neeli <srinivas.neeli@amd.com>
With the RX channels armed, the endpoint can start receiving frames.
Post RX buffers to every RX channel on open and reap them from the
completion callback. Each inbound frame carries a TUSER port_id in the
descriptor sideband. Accept frames from MAC1 (port_id 1) and MAC2
(port_id 2) and pass them up the stack. Drop any other port_id, since
the bridged-only model never produces it. Drop and count frames with
bad descriptor status, an invalid length or no metadata pointer.
Refill each slot from the completion callback unless the interface is
closing, which keeps the ring full without a separate refill worker.
Delivery to the correct DSA user port needs the per-port metadata,
which is not added yet.
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Co-developed-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
.../net/ethernet/xilinx/tsn/xilinx_tsn_ep.c | 173 +++++++++++++++++-
1 file changed, 171 insertions(+), 2 deletions(-)
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
index fa21d59ade16..207ca3242a12 100644
--- a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
@@ -5,6 +5,7 @@
* Copyright (C) 2026 Advanced Micro Devices, Inc.
*/
+#include <linux/bitfield.h>
#include <linux/bitops.h>
#include <linux/circ_buf.h>
#include <linux/dma/xilinx_dma.h>
@@ -42,6 +43,17 @@
#define TX_BD_NUM_DEFAULT 64
#define RX_BD_NUM_DEFAULT 128
+/*
+ * The DMA descriptor sideband status word packs TID/TDEST/TUSER together;
+ * TUSER occupies the low byte, TID/TDEST sit in the upper bits.
+ */
+#define TSN_TUSER_MASK GENMASK(7, 0)
+/* TUSER Input Port ID field (bits [5:4] of the TUSER field) */
+#define TSN_TUSER_PORT_ID_MASK GENMASK(5, 4)
+#define TSN_TUSER_PORT_EP 0x0
+#define TSN_TUSER_PORT_MAC1 0x1
+#define TSN_TUSER_PORT_MAC2 0x2
+
/**
* struct skbuf_dma_descriptor - skb container for each in-flight DMA descriptor
* @sgl: scatter-gather list backing the DMA mapping
@@ -115,6 +127,147 @@ ep_get_desc(struct xlnx_tsn_ep_dma_chan *xchan, int idx)
return xchan->skb_ring[idx];
}
+static void ep_dma_rx_cb(void *data, const struct dmaengine_result *result);
+
+static void ep_rx_submit_desc(struct xlnx_tsn_ep_dma_chan *xchan)
+{
+ struct dma_async_tx_descriptor *dma_rx_desc;
+ struct skbuf_dma_descriptor *skbuf_dma;
+ struct xlnx_tsn_ep *ep = xchan->ep;
+ struct sk_buff *skb;
+ dma_addr_t addr;
+
+ skbuf_dma = ep_get_desc(xchan, xchan->ring_head & (RX_BD_NUM_DEFAULT - 1));
+ if (!skbuf_dma)
+ return;
+
+ skb = dev_alloc_skb(ep->max_frm_size);
+ if (!skb)
+ return;
+
+ sg_init_table(skbuf_dma->sgl, 1);
+ addr = dma_map_single(xchan->dma_dev, skb->data, ep->max_frm_size,
+ DMA_FROM_DEVICE);
+ if (unlikely(dma_mapping_error(xchan->dma_dev, addr))) {
+ if (net_ratelimit())
+ dev_warn(ep->dev, "DMA mapping error on RX submit\n");
+
+ goto err_free_skb;
+ }
+ sg_dma_address(skbuf_dma->sgl) = addr;
+ sg_dma_len(skbuf_dma->sgl) = ep->max_frm_size;
+ dma_rx_desc = dmaengine_prep_slave_sg(xchan->chan, skbuf_dma->sgl,
+ 1, DMA_DEV_TO_MEM,
+ DMA_PREP_INTERRUPT);
+ if (!dma_rx_desc)
+ goto err_unmap_skb;
+
+ skbuf_dma->skb = skb;
+ skbuf_dma->dma_address = sg_dma_address(skbuf_dma->sgl);
+ skbuf_dma->desc = dma_rx_desc;
+ dma_rx_desc->callback_param = xchan;
+ dma_rx_desc->callback_result = ep_dma_rx_cb;
+ xchan->ring_head++;
+ dmaengine_submit(dma_rx_desc);
+
+ return;
+
+err_unmap_skb:
+ dma_unmap_single(xchan->dma_dev, addr, ep->max_frm_size, DMA_FROM_DEVICE);
+err_free_skb:
+ dev_kfree_skb(skb);
+}
+
+static void ep_dma_rx_cb(void *data, const struct dmaengine_result *result)
+{
+ struct xlnx_tsn_ep_dma_chan *xchan = data;
+ struct skbuf_dma_descriptor *skbuf_dma;
+ size_t meta_len, meta_max_len, rx_len;
+ struct xlnx_tsn_ep *ep = xchan->ep;
+ struct net_device *ndev = ep->ndev;
+ struct sk_buff *skb;
+ u32 port_id, tuser;
+ u32 *metadata;
+ int i, avail;
+
+ skbuf_dma = ep_get_desc(xchan, xchan->ring_tail & (RX_BD_NUM_DEFAULT - 1));
+ xchan->ring_tail++;
+ skb = skbuf_dma->skb;
+ skbuf_dma->skb = NULL;
+
+ dma_unmap_single(xchan->dma_dev, skbuf_dma->dma_address,
+ ep->max_frm_size, DMA_FROM_DEVICE);
+
+ if (result->result != DMA_TRANS_NOERROR) {
+ if (net_ratelimit())
+ dev_warn(ep->dev, "RX DMA transfer error %d\n",
+ result->result);
+
+ dev_kfree_skb_any(skb);
+ DEV_STATS_INC(ndev, rx_dropped);
+ DEV_STATS_INC(ndev, rx_errors);
+ goto submit_new;
+ }
+
+ metadata = dmaengine_desc_get_metadata_ptr(skbuf_dma->desc,
+ &meta_len,
+ &meta_max_len);
+ if (IS_ERR_OR_NULL(metadata)) {
+ if (net_ratelimit())
+ dev_warn(ep->dev, "Failed to get RX metadata pointer\n");
+
+ dev_kfree_skb_any(skb);
+ DEV_STATS_INC(ndev, rx_dropped);
+ DEV_STATS_INC(ndev, rx_errors);
+ goto submit_new;
+ }
+
+ /* MCDMA metadata: [0] = status, [1] = sideband (TID/TDEST/TUSER), [2..] = app */
+ tuser = metadata[1] & TSN_TUSER_MASK;
+ rx_len = ep->max_frm_size - result->residue;
+
+ if (rx_len > ep->max_frm_size || rx_len < ETH_HLEN) {
+ if (net_ratelimit())
+ dev_warn(ep->dev, "Invalid RX length %zu (max=%u, min=%u)\n",
+ rx_len, ep->max_frm_size, ETH_HLEN);
+
+ dev_kfree_skb_any(skb);
+ DEV_STATS_INC(ndev, rx_dropped);
+ DEV_STATS_INC(ndev, rx_errors);
+ goto submit_new;
+ }
+
+ port_id = FIELD_GET(TSN_TUSER_PORT_ID_MASK, tuser);
+ if (port_id != TSN_TUSER_PORT_MAC1 && port_id != TSN_TUSER_PORT_MAC2) {
+ if (net_ratelimit())
+ dev_dbg(ep->dev, "RX dropping unexpected TUSER port_id=%u\n",
+ port_id);
+
+ dev_kfree_skb_any(skb);
+ DEV_STATS_INC(ndev, rx_dropped);
+ goto submit_new;
+ }
+
+ skb_put(skb, rx_len);
+ skb->dev = ndev;
+ skb->protocol = eth_type_trans(skb, ndev);
+ skb->ip_summed = CHECKSUM_NONE;
+ __netif_rx(skb);
+
+ DEV_STATS_INC(ndev, rx_packets);
+ DEV_STATS_ADD(ndev, rx_bytes, rx_len);
+
+submit_new:
+ if (READ_ONCE(ep->closing))
+ return;
+
+ avail = CIRC_SPACE(xchan->ring_head, xchan->ring_tail,
+ RX_BD_NUM_DEFAULT);
+ for (i = 0; i < avail; i++)
+ ep_rx_submit_desc(xchan);
+ dma_async_issue_pending(xchan->chan);
+}
+
static netdev_tx_t ep_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
dev_kfree_skb(skb);
@@ -229,8 +382,18 @@ static void ep_free_dma_chan(struct xlnx_tsn_ep_dma_chan *chan)
if (!chan)
return;
- if (chan->chan)
+ if (chan->chan) {
+ /* Drain the in-flight completion callback, then synchronize in
+ * case that callback re-armed a descriptor before it observed
+ * ep->closing. xilinx_dma_tx_submit() clears chan->terminating
+ * on every submit, so a single terminate does not cover a
+ * re-arm. The final terminate is a barrier before the channel
+ * memory is freed.
+ */
dmaengine_terminate_sync(chan->chan);
+ dmaengine_synchronize(chan->chan);
+ dmaengine_terminate_sync(chan->chan);
+ }
if (chan->is_tx) {
while (chan->ring_tail != chan->ring_head) {
@@ -291,7 +454,7 @@ static int ep_init_dmaengine(struct xlnx_tsn_ep *ep)
{
int tx_allocated = 0, rx_allocated = 0;
char name[16];
- int i, ret;
+ int i, j, ret;
ep->tx_chans = kcalloc(ep->num_tx_queues, sizeof(*ep->tx_chans),
GFP_KERNEL);
@@ -329,6 +492,12 @@ static int ep_init_dmaengine(struct xlnx_tsn_ep *ep)
rx_allocated++;
}
+ for (i = 0; i < ep->num_rx_queues; i++) {
+ for (j = 0; j < RX_BD_NUM_DEFAULT - 1; j++)
+ ep_rx_submit_desc(ep->rx_chans[i]);
+ dma_async_issue_pending(ep->rx_chans[i]->chan);
+ }
+
return 0;
err_free_chans:
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 07/20] net: xilinx: tsn: add the endpoint TX data path
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (5 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 06/20] net: xilinx: tsn: add the endpoint RX data path Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 08/20] net: xilinx: tsn: deliver endpoint RX frames to DSA user ports Nagadheeraj Rottela
` (13 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
From: Srinivas Neeli <srinivas.neeli@amd.com>
Add the transmit path so the endpoint can send frames.
Look up the physical DMA channel for the frame's queue through the map
built at probe, map the frame and submit it to that channel. Stop the
queue when the ring runs out of space, and wake it from the completion
callback once a slot frees up.
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Co-developed-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
.../net/ethernet/xilinx/tsn/xilinx_tsn_ep.c | 146 ++++++++++++++++++
1 file changed, 146 insertions(+)
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
index 207ca3242a12..7615d27f3b36 100644
--- a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
@@ -29,6 +29,8 @@
#include <linux/string.h>
#include <linux/types.h>
+#include <net/netdev_queues.h>
+
#include "xilinx_tsn.h"
#define DRIVER_NAME "xilinx_tsn_ep"
@@ -97,6 +99,7 @@ struct xlnx_tsn_ep_dma_chan {
* @ndev: the conduit netdev ("ep")
* @dev: backing device
* @regs: EP MAC register window
+ * @tx_lock: protects TX ring head/tail and SKB ring slots
* @num_tx_queues: number of TX DMA channels (one per priority)
* @num_rx_queues: number of RX DMA channels
* @tx_dma_chan_map: logical TX queue index -> physical DMA channel number
@@ -110,6 +113,8 @@ struct xlnx_tsn_ep {
struct device *dev;
void __iomem *regs;
+ spinlock_t tx_lock; /* protects TX ring buffers */
+
u32 num_tx_queues;
u32 num_rx_queues;
u32 tx_dma_chan_map[TSN_MAX_TX_QUEUE];
@@ -268,8 +273,145 @@ static void ep_dma_rx_cb(void *data, const struct dmaengine_result *result)
dma_async_issue_pending(xchan->chan);
}
+static void ep_dma_tx_cb(void *data, const struct dmaengine_result *result)
+{
+ struct xlnx_tsn_ep_dma_chan *xchan = data;
+ struct skbuf_dma_descriptor *skbuf_dma;
+ struct xlnx_tsn_ep *ep = xchan->ep;
+ struct netdev_queue *txq;
+ struct net_device *ndev;
+ struct scatterlist *sgl;
+ struct sk_buff *skb;
+ int sg_len;
+ int len;
+
+ scoped_guard(spinlock, &ep->tx_lock) {
+ skbuf_dma = ep_get_desc(xchan,
+ xchan->ring_tail & (TX_BD_NUM_DEFAULT - 1));
+ if (!skbuf_dma || !skbuf_dma->skb)
+ return;
+
+ skb = skbuf_dma->skb;
+ sgl = skbuf_dma->sgl;
+ sg_len = skbuf_dma->sg_len;
+
+ dma_unmap_sg(xchan->dma_dev, sgl, sg_len, DMA_TO_DEVICE);
+
+ skbuf_dma->skb = NULL;
+ xchan->ring_tail++;
+ }
+
+ ndev = skb->dev;
+ txq = netdev_get_tx_queue(ndev, skb_get_queue_mapping(skb));
+ len = skb->len;
+
+ if (unlikely(result->result != DMA_TRANS_NOERROR)) {
+ DEV_STATS_INC(ndev, tx_errors);
+ } else {
+ DEV_STATS_INC(ndev, tx_packets);
+ DEV_STATS_ADD(ndev, tx_bytes, len);
+ }
+
+ dev_consume_skb_any(skb);
+ netif_txq_completed_wake(txq, 1, len,
+ CIRC_SPACE(READ_ONCE(xchan->ring_head),
+ READ_ONCE(xchan->ring_tail),
+ TX_BD_NUM_DEFAULT), 2);
+}
+
static netdev_tx_t ep_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
+ struct dma_async_tx_descriptor *dma_tx_desc;
+ struct xlnx_tsn_ep *ep = netdev_priv(ndev);
+ struct skbuf_dma_descriptor *skbuf_dma;
+ int queue = skb_get_queue_mapping(skb);
+ struct xlnx_tsn_ep_dma_chan *xchan;
+ struct dma_device *dma_dev;
+ struct netdev_queue *txq;
+ int sg_len, nents, ret;
+ u32 phys_chan;
+
+ if (unlikely(queue >= ep->num_tx_queues)) {
+ if (net_ratelimit())
+ netdev_warn(ndev, "Invalid TX queue %d (max %u)\n",
+ queue, ep->num_tx_queues);
+ goto err_drop_skb;
+ }
+
+ phys_chan = ep->tx_dma_chan_map[queue];
+ if (phys_chan == TSN_DMA_CH_INVALID) {
+ if (net_ratelimit())
+ netdev_warn(ndev, "Logical TX queue %d has invalid DMA mapping\n",
+ queue);
+ goto err_drop_skb;
+ }
+
+ xchan = ep->tx_chans[phys_chan];
+ dma_dev = xchan->chan->device;
+
+ sg_len = skb_shinfo(skb)->nr_frags + 1;
+ txq = netdev_get_tx_queue(ndev, queue);
+
+ spin_lock_bh(&ep->tx_lock);
+ if (CIRC_SPACE(xchan->ring_head, READ_ONCE(xchan->ring_tail),
+ TX_BD_NUM_DEFAULT) <= 1) {
+ netif_txq_try_stop(txq,
+ CIRC_SPACE(xchan->ring_head,
+ READ_ONCE(xchan->ring_tail),
+ TX_BD_NUM_DEFAULT),
+ 2);
+ spin_unlock_bh(&ep->tx_lock);
+ if (net_ratelimit())
+ netdev_warn(ndev, "TSN TX ring full\n");
+
+ return NETDEV_TX_BUSY;
+ }
+
+ skbuf_dma = ep_get_desc(xchan, xchan->ring_head & (TX_BD_NUM_DEFAULT - 1));
+ if (!skbuf_dma) {
+ spin_unlock_bh(&ep->tx_lock);
+ goto err_drop_skb;
+ }
+ spin_unlock_bh(&ep->tx_lock);
+
+ sg_init_table(skbuf_dma->sgl, sg_len);
+ ret = skb_to_sgvec(skb, skbuf_dma->sgl, 0, skb->len);
+ if (ret < 0)
+ goto err_drop_skb;
+
+ nents = dma_map_sg(xchan->dma_dev, skbuf_dma->sgl, sg_len, DMA_TO_DEVICE);
+ if (!nents)
+ goto err_drop_skb;
+
+ dma_tx_desc = dma_dev->device_prep_slave_sg(xchan->chan, skbuf_dma->sgl,
+ nents, DMA_MEM_TO_DEV,
+ DMA_PREP_INTERRUPT, NULL);
+ if (!dma_tx_desc)
+ goto err_unmap_sg;
+
+ skbuf_dma->skb = skb;
+ skbuf_dma->sg_len = sg_len;
+ dma_tx_desc->callback_param = xchan;
+ dma_tx_desc->callback_result = ep_dma_tx_cb;
+
+ spin_lock_bh(&ep->tx_lock);
+ xchan->ring_head++;
+ netdev_tx_sent_queue(txq, skb->len);
+ netif_txq_maybe_stop(txq,
+ CIRC_SPACE(xchan->ring_head,
+ READ_ONCE(xchan->ring_tail),
+ TX_BD_NUM_DEFAULT),
+ 2, 2);
+ spin_unlock_bh(&ep->tx_lock);
+
+ dmaengine_submit(dma_tx_desc);
+ dma_async_issue_pending(xchan->chan);
+
+ return NETDEV_TX_OK;
+
+err_unmap_sg:
+ dma_unmap_sg(xchan->dma_dev, skbuf_dma->sgl, sg_len, DMA_TO_DEVICE);
+err_drop_skb:
dev_kfree_skb(skb);
DEV_STATS_INC(ndev, tx_dropped);
return NETDEV_TX_OK;
@@ -304,10 +446,13 @@ static int ep_open(struct net_device *ndev)
static int ep_stop(struct net_device *ndev)
{
struct xlnx_tsn_ep *ep = netdev_priv(ndev);
+ unsigned int i;
netif_tx_disable(ndev);
WRITE_ONCE(ep->closing, true);
ep_exit_dmaengine(ep);
+ for (i = 0; i < ndev->num_tx_queues; i++)
+ netdev_tx_reset_subqueue(ndev, i);
return 0;
}
@@ -657,6 +802,7 @@ static int xlnx_tsn_ep_probe(struct platform_device *pdev)
ep->num_tx_queues = num_tx;
ep->num_rx_queues = num_rx;
ep->max_frm_size = TSN_MAX_VLAN_FRAME_SIZE;
+ spin_lock_init(&ep->tx_lock);
for (i = 0; i < TSN_MAX_TX_QUEUE; i++)
ep->tx_dma_chan_map[i] = TSN_DMA_CH_INVALID;
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 08/20] net: xilinx: tsn: deliver endpoint RX frames to DSA user ports
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (6 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 07/20] net: xilinx: tsn: add the endpoint TX " Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 09/20] net: dsa: tag_xlnx_tsn: add skeleton tag protocol Nagadheeraj Rottela
` (12 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
The DSA core routes an RX frame to a user port from the port metadata
attached to the skb. Without that metadata, frames from the two MACs
cannot reach their swpN netdevs.
Allocate a METADATA_HW_PORT_MUX entry per MAC port at probe and attach
the matching entry to each RX frame by its TUSER port_id. Frames from
MAC1 and MAC2 now reach the correct user netdev, which completes the
conduit data path.
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
.../net/ethernet/xilinx/tsn/xilinx_tsn_ep.c | 51 ++++++++++++++++++-
1 file changed, 50 insertions(+), 1 deletion(-)
diff --git a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
index 7615d27f3b36..051285b47ddb 100644
--- a/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
+++ b/drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
@@ -29,6 +29,7 @@
#include <linux/string.h>
#include <linux/types.h>
+#include <net/dst_metadata.h>
#include <net/netdev_queues.h>
#include "xilinx_tsn.h"
@@ -56,6 +57,9 @@
#define TSN_TUSER_PORT_MAC1 0x1
#define TSN_TUSER_PORT_MAC2 0x2
+/* Sized to index port_md[] by TUSER port_id (1 or 2, slot 0 unused) */
+#define XLNX_TSN_EP_PORT_MD_SLOTS (TSN_TUSER_PORT_MAC2 + 1)
+
/**
* struct skbuf_dma_descriptor - skb container for each in-flight DMA descriptor
* @sgl: scatter-gather list backing the DMA mapping
@@ -107,6 +111,8 @@ struct xlnx_tsn_ep_dma_chan {
* @tx_chans: array of TX channels (size @num_tx_queues)
* @rx_chans: array of RX channels (size @num_rx_queues)
* @closing: set in ndo_stop so the RX completion callback stops re-arming
+ * @port_md: per-TUSER-port METADATA_HW_PORT_MUX entries attached on RX,
+ * indexed by port_id (1 for MAC1, 2 for MAC2)
*/
struct xlnx_tsn_ep {
struct net_device *ndev;
@@ -124,6 +130,8 @@ struct xlnx_tsn_ep {
struct xlnx_tsn_ep_dma_chan **rx_chans;
bool closing;
+
+ struct metadata_dst *port_md[XLNX_TSN_EP_PORT_MD_SLOTS];
};
static inline struct skbuf_dma_descriptor *
@@ -254,6 +262,7 @@ static void ep_dma_rx_cb(void *data, const struct dmaengine_result *result)
}
skb_put(skb, rx_len);
+ skb_dst_set_noref(skb, &ep->port_md[port_id]->dst);
skb->dev = ndev;
skb->protocol = eth_type_trans(skb, ndev);
skb->ip_summed = CHECKSUM_NONE;
@@ -771,6 +780,37 @@ static int ep_count_dma_queues(struct device *dev, u32 *out_tx, u32 *out_rx)
return 0;
}
+static void ep_free_port_md(struct xlnx_tsn_ep *ep)
+{
+ int i;
+
+ for (i = 0; i < XLNX_TSN_EP_PORT_MD_SLOTS; i++) {
+ if (ep->port_md[i]) {
+ metadata_dst_free(ep->port_md[i]);
+ ep->port_md[i] = NULL;
+ }
+ }
+}
+
+static int ep_alloc_port_md(struct xlnx_tsn_ep *ep)
+{
+ int i;
+
+ for (i = TSN_TUSER_PORT_MAC1; i <= TSN_TUSER_PORT_MAC2; i++) {
+ struct metadata_dst *md;
+
+ md = metadata_dst_alloc(0, METADATA_HW_PORT_MUX, GFP_KERNEL);
+ if (!md) {
+ ep_free_port_md(ep);
+ return -ENOMEM;
+ }
+ md->u.port_info.port_id = i;
+ ep->port_md[i] = md;
+ }
+
+ return 0;
+}
+
static int xlnx_tsn_ep_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
@@ -835,16 +875,24 @@ static int xlnx_tsn_ep_probe(struct platform_device *pdev)
ndev->dev_addr);
}
+ ret = ep_alloc_port_md(ep);
+ if (ret) {
+ dev_err_probe(dev, ret, "failed to allocate per-port metadata\n");
+ goto err_free_ndev;
+ }
+
platform_set_drvdata(pdev, ep);
ret = register_netdev(ndev);
if (ret) {
dev_err_probe(dev, ret, "failed to register net device\n");
- goto err_free_ndev;
+ goto err_free_md;
}
return 0;
+err_free_md:
+ ep_free_port_md(ep);
err_free_ndev:
free_netdev(ndev);
return ret;
@@ -858,6 +906,7 @@ static void xlnx_tsn_ep_remove(struct platform_device *pdev)
return;
unregister_netdev(ep->ndev);
+ ep_free_port_md(ep);
free_netdev(ep->ndev);
}
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 09/20] net: dsa: tag_xlnx_tsn: add skeleton tag protocol
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (7 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 08/20] net: xilinx: tsn: deliver endpoint RX frames to DSA user ports Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 10/20] net: dsa: xilinx: add skeleton driver for TSN switch Nagadheeraj Rottela
` (11 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
The AMD/Xilinx TSN Endpoint Ethernet MAC IP does not insert an
in-band CPU tag on the wire. The EP MAC driver attaches a per-port
metadata_dst (METADATA_HW_PORT_MUX) on RX to carry the port
classification out of band. The tag protocol therefore has no tag
bytes to add or strip.
Add DSA_TAG_PROTO_XLNX_TSN and register NET_DSA_TAG_XLNX_TSN as a
no-op tag driver. A later patch adds the PTP TX intercept to reroute
PTP frames through the per-MAC PTP TX path.
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
MAINTAINERS | 1 +
include/net/dsa.h | 2 ++
net/dsa/Kconfig | 9 +++++++++
net/dsa/Makefile | 1 +
net/dsa/tag_xlnx_tsn.c | 33 +++++++++++++++++++++++++++++++++
5 files changed, 46 insertions(+)
create mode 100644 net/dsa/tag_xlnx_tsn.c
diff --git a/MAINTAINERS b/MAINTAINERS
index e5b95b3bfa21..6fbe83fb3953 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -29614,6 +29614,7 @@ L: netdev@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
F: drivers/net/ethernet/xilinx/tsn/
+F: net/dsa/tag_xlnx_tsn.c
XILINX UARTLITE SERIAL DRIVER
M: Peter Korsgaard <jacmet@sunsite.dk>
diff --git a/include/net/dsa.h b/include/net/dsa.h
index 6f7f5c17b532..f0cc5b49407a 100644
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -60,6 +60,7 @@ struct tc_action;
#define DSA_TAG_PROTO_MXL862_VALUE 32
#define DSA_TAG_PROTO_NETC_VALUE 33
#define DSA_TAG_PROTO_KSZ8463_VALUE 34
+#define DSA_TAG_PROTO_XLNX_TSN_VALUE 35
enum dsa_tag_protocol {
DSA_TAG_PROTO_NONE = DSA_TAG_PROTO_NONE_VALUE,
@@ -97,6 +98,7 @@ enum dsa_tag_protocol {
DSA_TAG_PROTO_MXL862 = DSA_TAG_PROTO_MXL862_VALUE,
DSA_TAG_PROTO_NETC = DSA_TAG_PROTO_NETC_VALUE,
DSA_TAG_PROTO_KSZ8463 = DSA_TAG_PROTO_KSZ8463_VALUE,
+ DSA_TAG_PROTO_XLNX_TSN = DSA_TAG_PROTO_XLNX_TSN_VALUE,
};
struct dsa_switch;
diff --git a/net/dsa/Kconfig b/net/dsa/Kconfig
index d5e725b90d78..8ca48ce46ec1 100644
--- a/net/dsa/Kconfig
+++ b/net/dsa/Kconfig
@@ -221,4 +221,13 @@ config NET_DSA_TAG_YT921X
Say Y or M if you want to enable support for tagging frames for
Motorcomm YT921x switches.
+config NET_DSA_TAG_XLNX_TSN
+ tristate "Tag driver for AMD/Xilinx TSN Endpoint Ethernet MAC"
+ help
+ Say Y or M if you want to enable support for the AMD/Xilinx TSN
+ Endpoint Ethernet MAC IP. The IP does not insert an in-band CPU
+ tag; port classification is conveyed via metadata_dst attached on
+ RX by the EP MAC driver, and PTP TX frames are intercepted in the
+ xmit hook to be steered into the per-MAC PTP TX register window.
+
endif
diff --git a/net/dsa/Makefile b/net/dsa/Makefile
index b8c2667cd14a..6ba6c3a95976 100644
--- a/net/dsa/Makefile
+++ b/net/dsa/Makefile
@@ -41,6 +41,7 @@ obj-$(CONFIG_NET_DSA_TAG_RZN1_A5PSW) += tag_rzn1_a5psw.o
obj-$(CONFIG_NET_DSA_TAG_SJA1105) += tag_sja1105.o
obj-$(CONFIG_NET_DSA_TAG_TRAILER) += tag_trailer.o
obj-$(CONFIG_NET_DSA_TAG_VSC73XX_8021Q) += tag_vsc73xx_8021q.o
+obj-$(CONFIG_NET_DSA_TAG_XLNX_TSN) += tag_xlnx_tsn.o
obj-$(CONFIG_NET_DSA_TAG_XRS700X) += tag_xrs700x.o
obj-$(CONFIG_NET_DSA_TAG_YT921X) += tag_yt921x.o
diff --git a/net/dsa/tag_xlnx_tsn.c b/net/dsa/tag_xlnx_tsn.c
new file mode 100644
index 000000000000..de352aa3d9a8
--- /dev/null
+++ b/net/dsa/tag_xlnx_tsn.c
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * AMD/Xilinx TSN Endpoint Ethernet MAC tag protocol.
+ */
+
+#include "tag.h"
+
+#define XLNX_TSN_NAME "xlnx_tsn"
+
+static struct sk_buff *xlnx_tsn_xmit(struct sk_buff *skb,
+ struct net_device *dev)
+{
+ return skb;
+}
+
+static struct sk_buff *xlnx_tsn_rcv(struct sk_buff *skb,
+ struct net_device *dev)
+{
+ kfree_skb(skb);
+ return NULL;
+}
+
+static const struct dsa_device_ops xlnx_tsn_netdev_ops = {
+ .name = XLNX_TSN_NAME,
+ .proto = DSA_TAG_PROTO_XLNX_TSN,
+ .xmit = xlnx_tsn_xmit,
+ .rcv = xlnx_tsn_rcv,
+};
+
+module_dsa_tag_driver(xlnx_tsn_netdev_ops);
+MODULE_ALIAS_DSA_TAG_DRIVER(DSA_TAG_PROTO_XLNX_TSN, XLNX_TSN_NAME);
+MODULE_DESCRIPTION("DSA tag driver for AMD/Xilinx TSN Endpoint Ethernet MAC");
+MODULE_LICENSE("GPL");
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 10/20] net: dsa: xilinx: add skeleton driver for TSN switch
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (8 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 09/20] net: dsa: tag_xlnx_tsn: add skeleton tag protocol Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 11/20] net: dsa: xilinx: implement port_stp_state_set Nagadheeraj Rottela
` (10 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
Add a skeleton DSA driver for the three-port switch block of the
AMD/Xilinx TSN Endpoint Ethernet MAC IP. It binds
"xlnx,tsn-switch", maps the switch fabric and two per-MAC register
windows ("switch", "mac1", "mac2"), and registers a dsa_switch with
num_ports = 3. Port 0 is the CPU port, wired internally to the
endpoint MAC. Ports 1 and 2 are the external user ports backed by
MAC1 and MAC2.
At this stage only get_tag_protocol is wired. setup and teardown
are stubs. MDIO, phylink, frame filtering, and PTP follow in
subsequent patches.
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
MAINTAINERS | 1 +
drivers/net/dsa/Kconfig | 2 +
drivers/net/dsa/Makefile | 1 +
drivers/net/dsa/xilinx/Kconfig | 11 +++
drivers/net/dsa/xilinx/Makefile | 3 +
drivers/net/dsa/xilinx/xilinx_tsn.c | 126 ++++++++++++++++++++++++++++
drivers/net/dsa/xilinx/xilinx_tsn.h | 31 +++++++
7 files changed, 175 insertions(+)
create mode 100644 drivers/net/dsa/xilinx/Kconfig
create mode 100644 drivers/net/dsa/xilinx/Makefile
create mode 100644 drivers/net/dsa/xilinx/xilinx_tsn.c
create mode 100644 drivers/net/dsa/xilinx/xilinx_tsn.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 6fbe83fb3953..0e91cb2a307d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -29613,6 +29613,7 @@ M: Srinivas Neeli <srinivas.neeli@amd.com>
L: netdev@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
+F: drivers/net/dsa/xilinx/
F: drivers/net/ethernet/xilinx/tsn/
F: net/dsa/tag_xlnx_tsn.c
diff --git a/drivers/net/dsa/Kconfig b/drivers/net/dsa/Kconfig
index 4ab567c5bbaf..1404a77579f1 100644
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
@@ -84,6 +84,8 @@ source "drivers/net/dsa/qca/Kconfig"
source "drivers/net/dsa/sja1105/Kconfig"
+source "drivers/net/dsa/xilinx/Kconfig"
+
source "drivers/net/dsa/xrs700x/Kconfig"
source "drivers/net/dsa/realtek/Kconfig"
diff --git a/drivers/net/dsa/Makefile b/drivers/net/dsa/Makefile
index d2975badffc0..75b8449b575e 100644
--- a/drivers/net/dsa/Makefile
+++ b/drivers/net/dsa/Makefile
@@ -26,4 +26,5 @@ obj-y += ocelot/
obj-y += qca/
obj-y += realtek/
obj-y += sja1105/
+obj-y += xilinx/
obj-y += xrs700x/
diff --git a/drivers/net/dsa/xilinx/Kconfig b/drivers/net/dsa/xilinx/Kconfig
new file mode 100644
index 000000000000..0bd5efe8490b
--- /dev/null
+++ b/drivers/net/dsa/xilinx/Kconfig
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+config NET_DSA_XILINX_TSN
+ tristate "AMD/Xilinx TSN Endpoint Ethernet MAC switch support"
+ depends on OF && HAS_IOMEM
+ depends on NET_DSA
+ select NET_DSA_TAG_XLNX_TSN
+ help
+ This enables DSA switch support for the three-port switch
+ block of the AMD/Xilinx TSN Endpoint Ethernet MAC IP. Port 0
+ is the CPU port, wired internally to the endpoint MAC; ports 1
+ and 2 are external Ethernet ports backed by MAC1 and MAC2.
diff --git a/drivers/net/dsa/xilinx/Makefile b/drivers/net/dsa/xilinx/Makefile
new file mode 100644
index 000000000000..334f0f979276
--- /dev/null
+++ b/drivers/net/dsa/xilinx/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+obj-$(CONFIG_NET_DSA_XILINX_TSN) += xlnx_tsn_dsa.o
+xlnx_tsn_dsa-y := xilinx_tsn.o
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
new file mode 100644
index 000000000000..6b285c528724
--- /dev/null
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * AMD/Xilinx TSN Endpoint Ethernet MAC DSA switch driver.
+ */
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <net/dsa.h>
+
+#include "xilinx_tsn.h"
+
+static enum dsa_tag_protocol xlnx_tsn_get_tag_protocol(struct dsa_switch *ds,
+ int port,
+ enum dsa_tag_protocol mp)
+{
+ return DSA_TAG_PROTO_XLNX_TSN;
+}
+
+static int xlnx_tsn_setup(struct dsa_switch *ds)
+{
+ return 0;
+}
+
+static void xlnx_tsn_teardown(struct dsa_switch *ds)
+{
+}
+
+static const struct dsa_switch_ops xlnx_tsn_switch_ops = {
+ .get_tag_protocol = xlnx_tsn_get_tag_protocol,
+ .setup = xlnx_tsn_setup,
+ .teardown = xlnx_tsn_teardown,
+};
+
+static int xlnx_tsn_map_reg(struct platform_device *pdev, const char *name,
+ void __iomem **out)
+{
+ void __iomem *base;
+
+ base = devm_platform_ioremap_resource_byname(pdev, name);
+ if (IS_ERR(base))
+ return dev_err_probe(&pdev->dev, PTR_ERR(base),
+ "failed to map %s reg window\n", name);
+
+ *out = base;
+ return 0;
+}
+
+static int xlnx_tsn_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct dsa_switch *ds;
+ struct xlnx_tsn *sw;
+ int ret;
+
+ sw = devm_kzalloc(dev, sizeof(*sw), GFP_KERNEL);
+ if (!sw)
+ return -ENOMEM;
+
+ sw->dev = dev;
+
+ 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]);
+ if (ret)
+ return ret;
+
+ ret = xlnx_tsn_map_reg(pdev, "mac2", &sw->mac_base[XLNX_TSN_PORT_MAC2]);
+ if (ret)
+ return ret;
+
+ ds = &sw->ds;
+ ds->dev = dev;
+ ds->num_ports = XLNX_TSN_NUM_PORTS;
+ ds->ops = &xlnx_tsn_switch_ops;
+ ds->priv = sw;
+
+ platform_set_drvdata(pdev, sw);
+
+ return dsa_register_switch(ds);
+}
+
+static void xlnx_tsn_remove(struct platform_device *pdev)
+{
+ struct xlnx_tsn *sw = platform_get_drvdata(pdev);
+
+ if (!sw)
+ return;
+
+ dsa_unregister_switch(&sw->ds);
+}
+
+static void xlnx_tsn_shutdown(struct platform_device *pdev)
+{
+ struct xlnx_tsn *sw = platform_get_drvdata(pdev);
+
+ if (!sw)
+ return;
+
+ dsa_switch_shutdown(&sw->ds);
+ platform_set_drvdata(pdev, NULL);
+}
+
+static const struct of_device_id xlnx_tsn_of_match[] = {
+ { .compatible = "xlnx,tsn-switch" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, xlnx_tsn_of_match);
+
+static struct platform_driver xlnx_tsn_driver = {
+ .driver = {
+ .name = "xlnx-tsn-switch",
+ .of_match_table = xlnx_tsn_of_match,
+ },
+ .probe = xlnx_tsn_probe,
+ .remove = xlnx_tsn_remove,
+ .shutdown = xlnx_tsn_shutdown,
+};
+module_platform_driver(xlnx_tsn_driver);
+
+MODULE_AUTHOR("Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>");
+MODULE_DESCRIPTION("AMD/Xilinx TSN Endpoint Ethernet MAC DSA switch driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
new file mode 100644
index 000000000000..0f9af866bd18
--- /dev/null
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * AMD/Xilinx TSN Endpoint Ethernet MAC DSA switch driver.
+ */
+#ifndef _XILINX_TSN_H
+#define _XILINX_TSN_H
+
+#include <linux/types.h>
+#include <net/dsa.h>
+
+#define XLNX_TSN_NUM_PORTS 3
+#define XLNX_TSN_CPU_PORT 0
+#define XLNX_TSN_PORT_MAC1 1
+#define XLNX_TSN_PORT_MAC2 2
+
+/**
+ * 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])
+ */
+struct xlnx_tsn {
+ struct dsa_switch ds;
+ struct device *dev;
+ void __iomem *sw_base;
+ void __iomem *mac_base[XLNX_TSN_NUM_PORTS];
+};
+
+#endif /* _XILINX_TSN_H */
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 11/20] net: dsa: xilinx: implement port_stp_state_set
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (9 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 10/20] net: dsa: xilinx: add skeleton driver for TSN switch Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 12/20] net: dsa: xilinx: register per-MAC MDIO buses Nagadheeraj Rottela
` (9 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
Wire up the bridge STP state machine to the Switch Port State
Control register at +0x004C. Each port has a 3-bit state field and
a self-clearing commit bit. The hardware applies the new state only
when the commit bit transitions from 0 to 1. Follow each write with
readl_poll_timeout() until the hardware acknowledges.
All five STP states map to distinct hardware encodings on the
MAC ports.
Put the CPU port in FORWARDING so host traffic always flows, and
the user ports in DISABLED so the bridge STP machine drives all
further transitions. On teardown, return the user ports to DISABLED.
The bridge STP machine does not manage the CPU port. Its forwarding
state does not change after setup().
Co-developed-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
drivers/net/dsa/xilinx/xilinx_tsn.c | 148 ++++++++++++++++++++++++++++
drivers/net/dsa/xilinx/xilinx_tsn.h | 32 ++++++
2 files changed, 180 insertions(+)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 6b285c528724..020b1622670c 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -3,6 +3,10 @@
* AMD/Xilinx TSN Endpoint Ethernet MAC DSA switch driver.
*/
+#include <linux/bitfield.h>
+#include <linux/if_bridge.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
@@ -11,6 +15,86 @@
#include "xilinx_tsn.h"
+static void sw_iow(struct xlnx_tsn *sw, u32 off, u32 val)
+{
+ iowrite32(val, sw->sw_base + off);
+}
+
+static u32 sw_ior(struct xlnx_tsn *sw, u32 off)
+{
+ return ioread32(sw->sw_base + off);
+}
+
+static int xlnx_tsn_switch_status_ready(struct xlnx_tsn *sw)
+{
+ u32 reg;
+
+ return readl_poll_timeout(sw->sw_base + TSN_SW_STATUS_OFFSET, reg,
+ reg & TSN_SW_STATUS_READY,
+ TSN_SW_POLL_DELAY_US, TSN_SW_POLL_TIMEOUT_US);
+}
+
+static int xlnx_tsn_port_state_bits(int port, u32 *mask, u32 *chg_bit)
+{
+ switch (port) {
+ case XLNX_TSN_CPU_PORT:
+ *mask = EP_PORT_STATUS_MASK;
+ *chg_bit = EP_PORT_STATUS_CHG_BIT;
+ return 0;
+ case XLNX_TSN_PORT_MAC1:
+ *mask = MAC1_PORT_STATUS_MASK;
+ *chg_bit = MAC1_PORT_STATUS_CHG_BIT;
+ return 0;
+ case XLNX_TSN_PORT_MAC2:
+ *mask = MAC2_PORT_STATUS_MASK;
+ *chg_bit = MAC2_PORT_STATUS_CHG_BIT;
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int xlnx_tsn_set_port_state(struct xlnx_tsn *sw, int port,
+ enum tsn_port_state state)
+{
+ u32 chg_bit, mask, val, reg;
+ int err;
+
+ err = xlnx_tsn_port_state_bits(port, &mask, &chg_bit);
+ if (err)
+ return err;
+
+ err = xlnx_tsn_switch_status_ready(sw);
+ if (err) {
+ dev_err(sw->dev, "port %d: switch not ready for state change\n",
+ port);
+ return err;
+ }
+
+ /* Bit won't re-arm if a previous change never cleared it. */
+ val = sw_ior(sw, TSN_PORT_STATE_CTRL_OFFSET);
+ if (val & chg_bit) {
+ dev_err(sw->dev, "port %d: previous state change still pending\n",
+ port);
+ return -EBUSY;
+ }
+
+ val &= ~mask;
+ val |= (state << __ffs(mask)) & mask;
+ val |= chg_bit;
+ sw_iow(sw, TSN_PORT_STATE_CTRL_OFFSET, val);
+
+ err = readl_poll_timeout(sw->sw_base + TSN_PORT_STATE_CTRL_OFFSET, reg,
+ !(reg & chg_bit), TSN_SW_POLL_DELAY_US,
+ TSN_SW_POLL_TIMEOUT_US);
+ if (err) {
+ dev_err(sw->dev, "port %d: state change ack timed out\n", port);
+ return -ETIMEDOUT;
+ }
+
+ return 0;
+}
+
static enum dsa_tag_protocol xlnx_tsn_get_tag_protocol(struct dsa_switch *ds,
int port,
enum dsa_tag_protocol mp)
@@ -18,19 +102,83 @@ static enum dsa_tag_protocol xlnx_tsn_get_tag_protocol(struct dsa_switch *ds,
return DSA_TAG_PROTO_XLNX_TSN;
}
+static void xlnx_tsn_port_stp_state_set(struct dsa_switch *ds, int port,
+ u8 state)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ enum tsn_port_state hw_state;
+
+ switch (state) {
+ case BR_STATE_DISABLED:
+ hw_state = TSN_PORT_STATE_DISABLED;
+ break;
+ case BR_STATE_BLOCKING:
+ hw_state = TSN_PORT_STATE_BLOCKING;
+ break;
+ case BR_STATE_LISTENING:
+ hw_state = TSN_PORT_STATE_LISTENING;
+ break;
+ case BR_STATE_LEARNING:
+ hw_state = TSN_PORT_STATE_LEARNING;
+ break;
+ case BR_STATE_FORWARDING:
+ hw_state = TSN_PORT_STATE_FORWARDING;
+ break;
+ default:
+ dev_warn(sw->dev, "port %d: unsupported STP state %u\n",
+ port, state);
+ return;
+ }
+
+ xlnx_tsn_set_port_state(sw, port, hw_state);
+}
+
static int xlnx_tsn_setup(struct dsa_switch *ds)
{
+ struct xlnx_tsn *sw = ds->priv;
+ struct dsa_port *dp;
+ int ret;
+
+ if (!dsa_is_user_port(ds, XLNX_TSN_PORT_MAC1) ||
+ !dsa_is_user_port(ds, XLNX_TSN_PORT_MAC2))
+ return dev_err_probe(sw->dev, -EINVAL,
+ "both MAC1 and MAC2 must be enabled as switch ports\n");
+
+ /* CPU port stays in FORWARDING so host traffic always flows.
+ * User ports start in DISABLED and transition from there under
+ * bridge STP control.
+ */
+ ret = xlnx_tsn_set_port_state(sw, XLNX_TSN_CPU_PORT,
+ TSN_PORT_STATE_FORWARDING);
+ if (ret)
+ return ret;
+
+ dsa_switch_for_each_user_port(dp, ds) {
+ ret = xlnx_tsn_set_port_state(sw, dp->index,
+ TSN_PORT_STATE_DISABLED);
+ if (ret)
+ return ret;
+ }
+
return 0;
}
static void xlnx_tsn_teardown(struct dsa_switch *ds)
{
+ struct xlnx_tsn *sw = ds->priv;
+ struct dsa_port *dp;
+
+ dsa_switch_for_each_user_port(dp, ds)
+ xlnx_tsn_set_port_state(sw, dp->index, TSN_PORT_STATE_DISABLED);
+
+ xlnx_tsn_set_port_state(sw, XLNX_TSN_CPU_PORT, TSN_PORT_STATE_DISABLED);
}
static const struct dsa_switch_ops xlnx_tsn_switch_ops = {
.get_tag_protocol = xlnx_tsn_get_tag_protocol,
.setup = xlnx_tsn_setup,
.teardown = xlnx_tsn_teardown,
+ .port_stp_state_set = xlnx_tsn_port_stp_state_set,
};
static int xlnx_tsn_map_reg(struct platform_device *pdev, const char *name,
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index 0f9af866bd18..1064cf1ce66e 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -5,6 +5,8 @@
#ifndef _XILINX_TSN_H
#define _XILINX_TSN_H
+#include <linux/bitfield.h>
+#include <linux/bits.h>
#include <linux/types.h>
#include <net/dsa.h>
@@ -13,6 +15,36 @@
#define XLNX_TSN_PORT_MAC1 1
#define XLNX_TSN_PORT_MAC2 2
+#define TSN_SW_STATUS_OFFSET 0x00000
+/* Poll this before changing port state. */
+#define TSN_SW_STATUS_READY BIT(0)
+
+/* Switch Port State Control register: packs per-port STP state and
+ * change-commit bits into one 32-bit word.
+ */
+#define TSN_PORT_STATE_CTRL_OFFSET 0x0004c
+
+#define EP_PORT_STATUS_CHG_BIT BIT(0)
+#define EP_PORT_STATUS_MASK GENMASK(3, 1)
+#define MAC1_PORT_STATUS_CHG_BIT BIT(8)
+#define MAC1_PORT_STATUS_MASK GENMASK(11, 9)
+#define MAC2_PORT_STATUS_CHG_BIT BIT(16)
+#define MAC2_PORT_STATUS_MASK GENMASK(19, 17)
+
+/* readl_poll_timeout() parameters (in microseconds): poll until
+ * the port-state change-commit bit self-clears.
+ */
+#define TSN_SW_POLL_DELAY_US 10
+#define TSN_SW_POLL_TIMEOUT_US 5000
+
+enum tsn_port_state {
+ TSN_PORT_STATE_DISABLED = 0,
+ TSN_PORT_STATE_BLOCKING,
+ TSN_PORT_STATE_LISTENING,
+ TSN_PORT_STATE_LEARNING,
+ TSN_PORT_STATE_FORWARDING,
+};
+
/**
* struct xlnx_tsn - per-IP switch state
* @ds: DSA switch
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 12/20] net: dsa: xilinx: register per-MAC MDIO buses
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (10 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 11/20] net: dsa: xilinx: implement port_stp_state_set Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 13/20] net: dsa: xilinx: wire up phylink for the switch ports Nagadheeraj Rottela
` (8 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
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@amd.com>
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@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
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 13/20] net: dsa: xilinx: wire up phylink for the switch ports
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (11 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 12/20] net: dsa: xilinx: register per-MAC MDIO buses Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 14/20] net: dsa: xilinx: program MAC frame filter and per-port nibbles Nagadheeraj Rottela
` (7 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
Wire up the MAC-side phylink ops for all three switch ports.
For the two external user ports, phylink_get_caps fills
supported_interfaces with RGMII variants, SGMII, and GMII, and sets
mac_capabilities to MAC_100FD and MAC_1000FD. The speed-config field
only encodes those two speeds, so 10 Mbps and half-duplex are not
reachable in hardware. The CPU port has no external PHY. Its link is
internal and fixed at MAC_1000FD, so phylink_get_caps sets
PHY_INTERFACE_MODE_INTERNAL and returns early for it.
mac_link_up programs the resolved speed into the speed-config field
and sets TX_EN and RX_EN to allow frames to flow. mac_link_down clears
them. mac_config is intentionally empty. The interface mode is fixed
at IP synthesis time and there is no runtime register to program.
Co-developed-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
drivers/net/dsa/xilinx/xilinx_tsn.c | 102 ++++++++++++++++++++++++++++
drivers/net/dsa/xilinx/xilinx_tsn.h | 12 ++++
2 files changed, 114 insertions(+)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 542b74514ed3..86aba2e8b6e9 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -14,6 +14,7 @@
#include <linux/of.h>
#include <linux/of_mdio.h>
#include <linux/phy.h>
+#include <linux/phylink.h>
#include <linux/platform_device.h>
#include <net/dsa.h>
@@ -324,6 +325,105 @@ static void xlnx_tsn_port_stp_state_set(struct dsa_switch *ds, int port,
xlnx_tsn_set_port_state(sw, port, hw_state);
}
+static void xlnx_tsn_phylink_get_caps(struct dsa_switch *ds, int port,
+ struct phylink_config *config)
+{
+ if (port == XLNX_TSN_CPU_PORT) {
+ config->mac_capabilities = MAC_1000FD;
+ __set_bit(PHY_INTERFACE_MODE_INTERNAL,
+ config->supported_interfaces);
+ return;
+ }
+
+ /* The MAC's speed-config field only encodes 100 / 1000.
+ * Half-duplex and 10 Mbps are not supported.
+ */
+ config->mac_capabilities = MAC_100FD | MAC_1000FD;
+ phy_interface_set_rgmii(config->supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_SGMII, config->supported_interfaces);
+ __set_bit(PHY_INTERFACE_MODE_GMII, config->supported_interfaces);
+}
+
+static void xlnx_tsn_mac_config(struct phylink_config *config,
+ unsigned int mode,
+ const struct phylink_link_state *state)
+{
+ /* Interface mode (RGMII / SGMII / GMII) is fixed at IP synthesis
+ * time. There is no runtime register to program it here.
+ */
+}
+
+static void xlnx_tsn_mac_link_down(struct phylink_config *config,
+ unsigned int mode,
+ phy_interface_t interface)
+{
+ struct dsa_port *dp = dsa_phylink_to_port(config);
+ struct xlnx_tsn *sw = dp->ds->priv;
+ struct xlnx_tsn_mac *m;
+ u32 rcw1, tc;
+
+ m = &sw->mac[dp->index];
+
+ /* The CPU port has no switch-side MAC registers. Nothing to
+ * tear down here (see xlnx_tsn_mac_link_up).
+ */
+ if (dp->index == XLNX_TSN_CPU_PORT)
+ return;
+
+ tc = mac_ior(m, TSN_TC_OFFSET) & ~TSN_TC_TX_EN;
+ mac_iow(m, TSN_TC_OFFSET, tc);
+
+ rcw1 = mac_ior(m, TSN_RCW1_OFFSET) & ~TSN_RCW1_RX_EN;
+ mac_iow(m, TSN_RCW1_OFFSET, rcw1);
+}
+
+static void xlnx_tsn_mac_link_up(struct phylink_config *config,
+ struct phy_device *phy, unsigned int mode,
+ phy_interface_t interface, int speed,
+ int duplex, bool tx_pause, bool rx_pause)
+{
+ struct dsa_port *dp = dsa_phylink_to_port(config);
+ struct xlnx_tsn *sw = dp->ds->priv;
+ u32 speed_cfg, rcw1, tc;
+ struct xlnx_tsn_mac *m;
+
+ m = &sw->mac[dp->index];
+
+ /* The CPU port is the internal endpoint MAC. It has no switch-side
+ * MAC registers and is managed by the endpoint driver, not here.
+ */
+ if (dp->index == XLNX_TSN_CPU_PORT)
+ return;
+
+ speed_cfg = mac_ior(m, TSN_SPEED_CFG_OFFSET) & ~TSN_SPEED_CFG_MASK;
+ switch (speed) {
+ case SPEED_1000:
+ speed_cfg |= TSN_SPEED_CFG_1000;
+ break;
+ case SPEED_100:
+ speed_cfg |= TSN_SPEED_CFG_100;
+ break;
+ default:
+ dev_warn(sw->dev,
+ "port %d: unsupported link speed %d Mbps\n",
+ dp->index, speed);
+ return;
+ }
+ mac_iow(m, TSN_SPEED_CFG_OFFSET, speed_cfg);
+
+ rcw1 = mac_ior(m, TSN_RCW1_OFFSET) | TSN_RCW1_RX_EN;
+ mac_iow(m, TSN_RCW1_OFFSET, rcw1);
+
+ tc = mac_ior(m, TSN_TC_OFFSET) | TSN_TC_TX_EN;
+ mac_iow(m, TSN_TC_OFFSET, tc);
+}
+
+static const struct phylink_mac_ops xlnx_tsn_phylink_mac_ops = {
+ .mac_config = xlnx_tsn_mac_config,
+ .mac_link_up = xlnx_tsn_mac_link_up,
+ .mac_link_down = xlnx_tsn_mac_link_down,
+};
+
static int xlnx_tsn_setup(struct dsa_switch *ds)
{
struct xlnx_tsn *sw = ds->priv;
@@ -372,6 +472,7 @@ static const struct dsa_switch_ops xlnx_tsn_switch_ops = {
.setup = xlnx_tsn_setup,
.teardown = xlnx_tsn_teardown,
.port_stp_state_set = xlnx_tsn_port_stp_state_set,
+ .phylink_get_caps = xlnx_tsn_phylink_get_caps,
};
static int xlnx_tsn_map_reg(struct platform_device *pdev, const char *name,
@@ -421,6 +522,7 @@ static int xlnx_tsn_probe(struct platform_device *pdev)
ds->dev = dev;
ds->num_ports = XLNX_TSN_NUM_PORTS;
ds->ops = &xlnx_tsn_switch_ops;
+ ds->phylink_mac_ops = &xlnx_tsn_phylink_mac_ops;
ds->priv = sw;
platform_set_drvdata(pdev, sw);
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index 025645a83992..6286caff7a1c 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -66,6 +66,18 @@ enum tsn_port_state {
#define TSN_MDIO_MRD_MASK GENMASK(15, 0)
+/* Per-MAC receive / transmit / speed configuration registers. */
+#define TSN_RCW1_OFFSET 0x00000404
+#define TSN_RCW1_RX_EN BIT(28)
+
+#define TSN_TC_OFFSET 0x00000408
+#define TSN_TC_TX_EN BIT(28)
+
+#define TSN_SPEED_CFG_OFFSET 0x00000410
+#define TSN_SPEED_CFG_MASK GENMASK(31, 30)
+#define TSN_SPEED_CFG_100 BIT(30)
+#define TSN_SPEED_CFG_1000 BIT(31)
+
struct mii_bus;
struct xlnx_tsn;
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 14/20] net: dsa: xilinx: program MAC frame filter and per-port nibbles
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (12 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 13/20] net: dsa: xilinx: wire up phylink for the switch ports Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 15/20] net: dsa: xilinx: register PHC backed by the RTC timer block Nagadheeraj Rottela
` (6 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
The switch fabric identifies a frame's destination port by matching
its MAC against a 48-bit address in TSN_SW_MAC_LSB/TSN_SW_MAC_MSB,
with a 16-bit mask covering bytes 4 and 5. Until the filter is
programmed, every frame misses the classifier.
Take the DSA conduit's MAC as a 44-bit prefix, wildcarding the low
nibble of byte 5, and program both registers. Each of the three
ports also owns a 4-bit MAC nibble field in the Switch Port State
Control register at +0x004C. The field holds the low nibble of
byte 5 of that port's MAC and covers the wildcarded bits, so the
fabric can still tell the ports apart.
Drive all of this from a netdev notifier. On the conduit's
NETDEV_REGISTER, derive the prefix, program the frame filter, and
set the CPU port nibbles for all ports. Handle NETDEV_REGISTER for
each swpN to set its nibble.
Reject port_set_mac_address whenever the requested address does not
match the conduit's prefix, or its low nibble collides with the CPU
port or another user port. The nibble is the only thing that tells
ports apart once the top 44 bits are shared.
Also route CPU-originated bridge control frames (STP, LLDP) to a
single port by nibble match instead of flooding both, since the
classifier can now tell one port's source MAC from the other's.
Co-developed-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
drivers/net/dsa/xilinx/xilinx_tsn.c | 232 +++++++++++++++++++++++++++-
drivers/net/dsa/xilinx/xilinx_tsn.h | 39 ++++-
2 files changed, 268 insertions(+), 3 deletions(-)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 86aba2e8b6e9..9826f006b078 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -5,12 +5,14 @@
#include <linux/bitfield.h>
#include <linux/clk.h>
+#include <linux/etherdevice.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/netdevice.h>
#include <linux/of.h>
#include <linux/of_mdio.h>
#include <linux/phy.h>
@@ -33,6 +35,74 @@ static u32 sw_ior(struct xlnx_tsn *sw, u32 off)
return ioread32(sw->sw_base + off);
}
+/* Cache the conduit MAC with byte 5's low nibble zeroed out. The
+ * frame-filter mask covers those 4 bits. The per-port MAC-nibble
+ * fields in +0x004C supply the actual values.
+ */
+static void xlnx_tsn_derive_prefix(struct xlnx_tsn *sw)
+{
+ memcpy(sw->mac_prefix, sw->conduit->dev_addr, ETH_ALEN);
+ sw->mac_prefix[5] &= ~TSN_SW_MAC_NIBBLE_WILDCARD;
+}
+
+static void xlnx_tsn_program_frame_filter(struct xlnx_tsn *sw)
+{
+ const u8 *p = sw->mac_prefix;
+ u32 lsb, msb;
+
+ lsb = ((u32)p[2] << 24) | ((u32)p[3] << 16) |
+ ((u32)p[4] << 8) | p[5];
+ msb = FIELD_PREP(TSN_SW_MAC_MSB_MASK_MASK, TSN_SW_MAC_NIBBLE_WILDCARD) |
+ FIELD_PREP(TSN_SW_MAC_MSB_ADDR_MASK,
+ ((u32)p[0] << 8) | p[1]);
+
+ sw_iow(sw, TSN_SW_MAC_LSB_OFFSET, lsb);
+ sw_iow(sw, TSN_SW_MAC_MSB_OFFSET, msb);
+}
+
+/* True when addr's upper 44 bits match the cached prefix. The
+ * prefix has byte 5's low nibble already cleared, so byte 5 of addr
+ * is masked the same way before comparison.
+ */
+static bool xlnx_tsn_prefix_matches(struct xlnx_tsn *sw, const u8 *addr)
+{
+ if (memcmp(addr, sw->mac_prefix, ETH_ALEN - 1) != 0)
+ return false;
+
+ return (addr[5] & ~TSN_SW_MAC_NIBBLE_WILDCARD) == sw->mac_prefix[5];
+}
+
+static int xlnx_tsn_set_port_mac_nibble(struct xlnx_tsn *sw, int port,
+ u8 nibble)
+{
+ u32 mask, new_field, reg;
+
+ nibble &= TSN_SW_MAC_NIBBLE_WILDCARD;
+
+ switch (port) {
+ case XLNX_TSN_CPU_PORT:
+ mask = EP_PORT_MAC_NIBBLE_MASK;
+ new_field = FIELD_PREP(EP_PORT_MAC_NIBBLE_MASK, nibble);
+ break;
+ case XLNX_TSN_PORT_MAC1:
+ mask = MAC1_PORT_MAC_NIBBLE_MASK;
+ new_field = FIELD_PREP(MAC1_PORT_MAC_NIBBLE_MASK, nibble);
+ break;
+ case XLNX_TSN_PORT_MAC2:
+ mask = MAC2_PORT_MAC_NIBBLE_MASK;
+ new_field = FIELD_PREP(MAC2_PORT_MAC_NIBBLE_MASK, nibble);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ reg = sw_ior(sw, TSN_PORT_STATE_CTRL_OFFSET);
+ reg = (reg & ~mask) | new_field;
+ sw_iow(sw, TSN_PORT_STATE_CTRL_OFFSET, reg);
+
+ return 0;
+}
+
static int xlnx_tsn_switch_status_ready(struct xlnx_tsn *sw)
{
u32 reg;
@@ -287,6 +357,112 @@ static int xlnx_tsn_mdio_register_all(struct xlnx_tsn *sw)
return ret;
}
+/* Build a per-port MAC from the shared prefix. */
+static void xlnx_tsn_synth_port_mac(struct xlnx_tsn *sw, int port,
+ u8 *out)
+{
+ u8 ep_nibble = sw->conduit->dev_addr[5] & TSN_SW_MAC_NIBBLE_WILDCARD;
+
+ memcpy(out, sw->mac_prefix, ETH_ALEN);
+ out[5] |= (ep_nibble + port) & TSN_SW_MAC_NIBBLE_WILDCARD;
+}
+
+static int xlnx_tsn_user_port_index(struct xlnx_tsn *sw,
+ const struct net_device *dev)
+{
+ struct dsa_port *dp;
+
+ dsa_switch_for_each_user_port(dp, &sw->ds)
+ if (dp->user == dev)
+ return dp->index;
+
+ return -1;
+}
+
+static int xlnx_tsn_handle_user_register(struct xlnx_tsn *sw,
+ struct net_device *dev, int port)
+{
+ u8 want[ETH_ALEN];
+ u8 nibble;
+
+ if (!xlnx_tsn_prefix_matches(sw, dev->dev_addr)) {
+ xlnx_tsn_synth_port_mac(sw, port, want);
+ dev_warn(sw->dev,
+ "port %d: MAC %pM does not match conduit prefix; overriding to %pM\n",
+ port, dev->dev_addr, want);
+ dev_addr_mod(dev, 0, want, ETH_ALEN);
+ nibble = want[5] & TSN_SW_MAC_NIBBLE_WILDCARD;
+ } else if (ether_addr_equal(dev->dev_addr, sw->conduit->dev_addr)) {
+ /* Either DSA inherited the conduit MAC, or DT gave port@N
+ * the same address explicitly. Either way, assign a unique
+ * per-port nibble.
+ */
+ xlnx_tsn_synth_port_mac(sw, port, want);
+ dev_addr_mod(dev, 0, want, ETH_ALEN);
+ nibble = want[5] & TSN_SW_MAC_NIBBLE_WILDCARD;
+ } else {
+ nibble = dev->dev_addr[5] & TSN_SW_MAC_NIBBLE_WILDCARD;
+ }
+
+ return xlnx_tsn_set_port_mac_nibble(sw, port, nibble);
+}
+
+static void xlnx_tsn_handle_conduit_changeaddr(struct xlnx_tsn *sw)
+{
+ struct dsa_port *dp;
+
+ xlnx_tsn_derive_prefix(sw);
+ xlnx_tsn_program_frame_filter(sw);
+ xlnx_tsn_set_port_mac_nibble(sw, XLNX_TSN_CPU_PORT,
+ sw->conduit->dev_addr[5]);
+
+ dsa_switch_for_each_user_port(dp, &sw->ds) {
+ u8 want[ETH_ALEN];
+
+ if (!dp->user)
+ continue;
+
+ xlnx_tsn_synth_port_mac(sw, dp->index, want);
+ dev_addr_mod(dp->user, 0, want, ETH_ALEN);
+ call_netdevice_notifiers(NETDEV_CHANGEADDR, dp->user);
+ }
+}
+
+static int xlnx_tsn_netdev_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct xlnx_tsn *sw = container_of(nb, struct xlnx_tsn, nb);
+ struct net_device *dev = netdev_notifier_info_to_dev(ptr);
+ int port;
+
+ switch (event) {
+ case NETDEV_REGISTER:
+ if (dev == sw->conduit) {
+ xlnx_tsn_handle_conduit_changeaddr(sw);
+ } else {
+ port = xlnx_tsn_user_port_index(sw, dev);
+ if (port < 0)
+ return NOTIFY_DONE;
+
+ xlnx_tsn_handle_user_register(sw, dev, port);
+ }
+ break;
+ case NETDEV_CHANGEADDR:
+ if (dev == sw->conduit) {
+ xlnx_tsn_handle_conduit_changeaddr(sw);
+ } else {
+ port = xlnx_tsn_user_port_index(sw, dev);
+ if (port < 0)
+ return NOTIFY_DONE;
+
+ xlnx_tsn_set_port_mac_nibble(sw, port, dev->dev_addr[5]);
+ }
+ break;
+ }
+
+ return NOTIFY_DONE;
+}
+
static enum dsa_tag_protocol xlnx_tsn_get_tag_protocol(struct dsa_switch *ds,
int port,
enum dsa_tag_protocol mp)
@@ -294,6 +470,30 @@ static enum dsa_tag_protocol xlnx_tsn_get_tag_protocol(struct dsa_switch *ds,
return DSA_TAG_PROTO_XLNX_TSN;
}
+static int xlnx_tsn_port_set_mac_address(struct dsa_switch *ds, int port,
+ const unsigned char *addr)
+{
+ u8 nibble = addr[5] & TSN_SW_MAC_NIBBLE_WILDCARD;
+ struct xlnx_tsn *sw = ds->priv;
+ struct dsa_port *dp;
+
+ if (!xlnx_tsn_prefix_matches(sw, addr))
+ return -EINVAL;
+
+ if (nibble == (sw->conduit->dev_addr[5] & TSN_SW_MAC_NIBBLE_WILDCARD))
+ return -EADDRINUSE;
+
+ dsa_switch_for_each_user_port(dp, ds) {
+ if (dp->index == port || !dp->user)
+ continue;
+
+ if ((dp->user->dev_addr[5] & TSN_SW_MAC_NIBBLE_WILDCARD) == nibble)
+ return -EADDRINUSE;
+ }
+
+ return 0;
+}
+
static void xlnx_tsn_port_stp_state_set(struct dsa_switch *ds, int port,
u8 state)
{
@@ -426,8 +626,10 @@ static const struct phylink_mac_ops xlnx_tsn_phylink_mac_ops = {
static int xlnx_tsn_setup(struct dsa_switch *ds)
{
+ struct dsa_port *cpu_dp = dsa_to_port(ds, XLNX_TSN_CPU_PORT);
struct xlnx_tsn *sw = ds->priv;
struct dsa_port *dp;
+ u32 mgmt;
int ret;
if (!dsa_is_user_port(ds, XLNX_TSN_PORT_MAC1) ||
@@ -435,6 +637,19 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
return dev_err_probe(sw->dev, -EINVAL,
"both MAC1 and MAC2 must be enabled as switch ports\n");
+ if (!cpu_dp || !cpu_dp->conduit)
+ return -ENODEV;
+
+ sw->conduit = cpu_dp->conduit;
+
+ /* Route CPU-originated bridge-group control frames (STP, LLDP) to
+ * the single wire port whose MAC-nibble field matches the frame's
+ * source-MAC low nibble, instead of flooding to both.
+ */
+ mgmt = sw_ior(sw, TSN_SW_MGMT_QUEUING_OFFSET);
+ mgmt |= TSN_SW_MGMT_QUEUING_EP_SA_EGRESS;
+ sw_iow(sw, TSN_SW_MGMT_QUEUING_OFFSET, mgmt);
+
/* CPU port stays in FORWARDING so host traffic always flows.
* User ports start in DISABLED and transition from there under
* bridge STP control.
@@ -451,7 +666,20 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
return ret;
}
- return xlnx_tsn_mdio_register_all(sw);
+ ret = xlnx_tsn_mdio_register_all(sw);
+ if (ret)
+ return ret;
+
+ sw->nb.notifier_call = xlnx_tsn_netdev_event;
+ ret = register_netdevice_notifier(&sw->nb);
+ if (ret)
+ goto err_mdio;
+
+ return 0;
+
+err_mdio:
+ xlnx_tsn_mdio_unregister_all(sw);
+ return ret;
}
static void xlnx_tsn_teardown(struct dsa_switch *ds)
@@ -459,6 +687,7 @@ static void xlnx_tsn_teardown(struct dsa_switch *ds)
struct xlnx_tsn *sw = ds->priv;
struct dsa_port *dp;
+ unregister_netdevice_notifier(&sw->nb);
xlnx_tsn_mdio_unregister_all(sw);
dsa_switch_for_each_user_port(dp, ds)
@@ -471,6 +700,7 @@ static const struct dsa_switch_ops xlnx_tsn_switch_ops = {
.get_tag_protocol = xlnx_tsn_get_tag_protocol,
.setup = xlnx_tsn_setup,
.teardown = xlnx_tsn_teardown,
+ .port_set_mac_address = xlnx_tsn_port_set_mac_address,
.port_stp_state_set = xlnx_tsn_port_stp_state_set,
.phylink_get_caps = xlnx_tsn_phylink_get_caps,
};
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index 6286caff7a1c..a228a7bebd53 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -7,7 +7,9 @@
#include <linux/bitfield.h>
#include <linux/bits.h>
+#include <linux/if_ether.h>
#include <linux/io.h>
+#include <linux/notifier.h>
#include <linux/types.h>
#include <net/dsa.h>
@@ -16,12 +18,29 @@
#define XLNX_TSN_PORT_MAC1 1
#define XLNX_TSN_PORT_MAC2 2
+/* Unicast Frame Filter: the switch accepts an incoming frame when
+ * its destination MAC matches this 48-bit address under the 16-bit
+ * mask in the MSB register's upper half. A set mask bit acts as a
+ * wildcard for the corresponding bit of bytes 4..5.
+ */
+#define TSN_SW_MAC_LSB_OFFSET 0x0000c
+#define TSN_SW_MAC_MSB_OFFSET 0x00010
+#define TSN_SW_MAC_MSB_ADDR_MASK GENMASK(15, 0)
+#define TSN_SW_MAC_MSB_MASK_MASK GENMASK(31, 16)
+
+/* Mask value that covers the low nibble of byte 5, leaving a
+ * 44-bit prefix common to all switch-port MACs. Those 4 bits are
+ * filled in by the per-port MAC-nibble fields in the Switch Port
+ * State Control register below.
+ */
+#define TSN_SW_MAC_NIBBLE_WILDCARD 0x000f
+
#define TSN_SW_STATUS_OFFSET 0x00000
/* Poll this before changing port state. */
#define TSN_SW_STATUS_READY BIT(0)
-/* Switch Port State Control register: packs per-port STP state and
- * change-commit bits into one 32-bit word.
+/* Switch Port State Control register: packs per-port STP state,
+ * change-commit bits, and MAC-nibble fields into one 32-bit word.
*/
#define TSN_PORT_STATE_CTRL_OFFSET 0x0004c
@@ -31,6 +50,12 @@
#define MAC1_PORT_STATUS_MASK GENMASK(11, 9)
#define MAC2_PORT_STATUS_CHG_BIT BIT(16)
#define MAC2_PORT_STATUS_MASK GENMASK(19, 17)
+#define EP_PORT_MAC_NIBBLE_MASK GENMASK(7, 4)
+#define MAC1_PORT_MAC_NIBBLE_MASK GENMASK(15, 12)
+#define MAC2_PORT_MAC_NIBBLE_MASK GENMASK(23, 20)
+
+#define TSN_SW_MGMT_QUEUING_OFFSET 0x00054
+#define TSN_SW_MGMT_QUEUING_EP_SA_EGRESS BIT(4)
/* readl_poll_timeout() parameters (in microseconds): poll until
* the port-state change-commit bit self-clears.
@@ -100,6 +125,13 @@ struct xlnx_tsn_mac {
* @ds: DSA switch
* @dev: backing device
* @sw_base: switch fabric register window
+ * @conduit: DSA conduit netdev (EP MAC), used as the source of the
+ * shared 44-bit frame-filter prefix
+ * @mac_prefix: conduit MAC with byte 5's low nibble cleared to zero,
+ * forming the 44-bit prefix common to all switch-port MACs
+ * @nb: netdev notifier that handles NETDEV_REGISTER on each swpN
+ * to set its final MAC, and NETDEV_CHANGEADDR on the conduit
+ * to refresh the shared prefix
* @mac: per-MAC state, indexed by user-port number (index 0 unused;
* MAC1 at [1], MAC2 at [2])
*/
@@ -107,6 +139,9 @@ struct xlnx_tsn {
struct dsa_switch ds;
struct device *dev;
void __iomem *sw_base;
+ struct net_device *conduit;
+ u8 mac_prefix[ETH_ALEN];
+ struct notifier_block nb;
struct xlnx_tsn_mac mac[XLNX_TSN_NUM_PORTS];
};
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 15/20] net: dsa: xilinx: register PHC backed by the RTC timer block
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (13 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 14/20] net: dsa: xilinx: program MAC frame filter and per-port nibbles Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 16/20] net: dsa: xilinx: drive per-MAC PTP TX/RX hardware paths Nagadheeraj Rottela
` (5 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
Bring the IP-wide RTC timer block up under the switch driver and
expose it as a PTP hardware clock. The timer block sits at a fixed
offset inside MAC1's per-MAC register window, which the switch driver
already owns via the "mac1" reg-name. Map it in probe() and register
the PHC in setup(). A per-switch spinlock serializes concurrent PHC
operations, since gettime/settime/adjtime can run concurrently from
different file descriptors.
Reading the nanoseconds register latches a full RTC snapshot, so
gettime reads nanoseconds before seconds. The timer block fires 128
ticks per second. The ISR delivers PTP_CLOCK_PPS on every 128th tick.
Co-developed-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
drivers/net/dsa/xilinx/Kconfig | 1 +
drivers/net/dsa/xilinx/Makefile | 2 +-
drivers/net/dsa/xilinx/xilinx_tsn.c | 7 +
drivers/net/dsa/xilinx/xilinx_tsn.h | 43 +++++
drivers/net/dsa/xilinx/xilinx_tsn_ptp.c | 235 ++++++++++++++++++++++++
5 files changed, 287 insertions(+), 1 deletion(-)
create mode 100644 drivers/net/dsa/xilinx/xilinx_tsn_ptp.c
diff --git a/drivers/net/dsa/xilinx/Kconfig b/drivers/net/dsa/xilinx/Kconfig
index 0bd5efe8490b..0e675295914b 100644
--- a/drivers/net/dsa/xilinx/Kconfig
+++ b/drivers/net/dsa/xilinx/Kconfig
@@ -3,6 +3,7 @@ config NET_DSA_XILINX_TSN
tristate "AMD/Xilinx TSN Endpoint Ethernet MAC switch support"
depends on OF && HAS_IOMEM
depends on NET_DSA
+ depends on PTP_1588_CLOCK
select NET_DSA_TAG_XLNX_TSN
help
This enables DSA switch support for the three-port switch
diff --git a/drivers/net/dsa/xilinx/Makefile b/drivers/net/dsa/xilinx/Makefile
index 334f0f979276..8d63749104a4 100644
--- a/drivers/net/dsa/xilinx/Makefile
+++ b/drivers/net/dsa/xilinx/Makefile
@@ -1,3 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-or-later
obj-$(CONFIG_NET_DSA_XILINX_TSN) += xlnx_tsn_dsa.o
-xlnx_tsn_dsa-y := xilinx_tsn.o
+xlnx_tsn_dsa-y := xilinx_tsn.o xilinx_tsn_ptp.o
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 9826f006b078..318d8b332208 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -675,8 +675,14 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
if (ret)
goto err_mdio;
+ ret = xlnx_tsn_ptp_init(sw);
+ if (ret)
+ goto err_nb;
+
return 0;
+err_nb:
+ unregister_netdevice_notifier(&sw->nb);
err_mdio:
xlnx_tsn_mdio_unregister_all(sw);
return ret;
@@ -687,6 +693,7 @@ static void xlnx_tsn_teardown(struct dsa_switch *ds)
struct xlnx_tsn *sw = ds->priv;
struct dsa_port *dp;
+ xlnx_tsn_ptp_exit(sw);
unregister_netdevice_notifier(&sw->nb);
xlnx_tsn_mdio_unregister_all(sw);
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index a228a7bebd53..6063763b5c17 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -10,6 +10,8 @@
#include <linux/if_ether.h>
#include <linux/io.h>
#include <linux/notifier.h>
+#include <linux/ptp_clock_kernel.h>
+#include <linux/spinlock.h>
#include <linux/types.h>
#include <net/dsa.h>
@@ -103,6 +105,28 @@ enum tsn_port_state {
#define TSN_SPEED_CFG_100 BIT(30)
#define TSN_SPEED_CFG_1000 BIT(31)
+/* PTP RTC timer block: a single block per IP, physically housed
+ * inside MAC1's per-MAC reg window. Owned by the switch driver
+ * because the PHC it backs is IP-wide, not per-MAC.
+ */
+#define TSN_TIMER_RTC_OFFSET_NS 0x00012800
+#define TSN_TIMER_RTC_OFFSET_SEC_L 0x00012808
+#define TSN_TIMER_RTC_OFFSET_SEC_H 0x0001280c
+#define TSN_TIMER_RTC_INCREMENT 0x00012810
+#define TSN_TIMER_CURRENT_RTC_NS 0x00012814
+#define TSN_TIMER_CURRENT_RTC_SEC_L 0x00012818
+#define TSN_TIMER_CURRENT_RTC_SEC_H 0x0001281c
+#define TSN_TIMER_INTERRUPT 0x00012820
+
+#define TSN_TIMER_MAX_NSEC_SIZE 30
+#define TSN_TIMER_MAX_NSEC_MASK GENMASK_ULL(TSN_TIMER_MAX_NSEC_SIZE - 1, 0)
+#define TSN_TIMER_MAX_SEC_SIZE 48
+#define TSN_TIMER_MAX_SEC_MASK GENMASK_ULL(TSN_TIMER_MAX_SEC_SIZE - 1, 0)
+#define TSN_TIMER_INT_CLEAR BIT(0)
+#define TSN_TIMER_RTC_NS_SHIFT 20
+#define TSN_TIMER_PULSES_PER_PPS 128
+#define TSN_TIMER_GTX_CLK_FREQ 125000000U
+
struct mii_bus;
struct xlnx_tsn;
@@ -134,6 +158,15 @@ struct xlnx_tsn_mac {
* to refresh the shared prefix
* @mac: per-MAC state, indexed by user-port number (index 0 unused;
* MAC1 at [1], MAC2 at [2])
+ * @ptp_timer_irq: 1 PPS / RTC-overflow interrupt
+ * @ptp_clock: registered PHC; NULL until setup() succeeds
+ * @ptp_clock_info: PHC capability + ops descriptor
+ * @reg_lock: serialises RTC offset / increment register accesses
+ * from process context and the PHC ops
+ * @rtc_value: base RTC increment word for the GTX clock frequency,
+ * used as the starting point for adjust_by_scaled_ppm()
+ * @pps_enable: user requested PPS event delivery
+ * @countpulse: timer-tick counter, reset to zero every TSN_TIMER_PULSES_PER_PPS ticks
*/
struct xlnx_tsn {
struct dsa_switch ds;
@@ -143,6 +176,13 @@ struct xlnx_tsn {
u8 mac_prefix[ETH_ALEN];
struct notifier_block nb;
struct xlnx_tsn_mac mac[XLNX_TSN_NUM_PORTS];
+ int ptp_timer_irq;
+ struct ptp_clock *ptp_clock;
+ struct ptp_clock_info ptp_clock_info;
+ spinlock_t reg_lock; /* serialises RTC offset/increment registers */
+ u64 rtc_value;
+ int pps_enable;
+ int countpulse;
};
static inline void mac_iow(struct xlnx_tsn_mac *m, u32 off, u32 val)
@@ -155,4 +195,7 @@ static inline u32 mac_ior(struct xlnx_tsn_mac *m, u32 off)
return ioread32(m->regs + off);
}
+int xlnx_tsn_ptp_init(struct xlnx_tsn *sw);
+void xlnx_tsn_ptp_exit(struct xlnx_tsn *sw);
+
#endif /* _XILINX_TSN_H */
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn_ptp.c b/drivers/net/dsa/xilinx/xilinx_tsn_ptp.c
new file mode 100644
index 000000000000..75c177f752f1
--- /dev/null
+++ b/drivers/net/dsa/xilinx/xilinx_tsn_ptp.c
@@ -0,0 +1,235 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * AMD/Xilinx TSN Endpoint Ethernet MAC DSA switch driver:
+ * PTP hardware clock.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/ktime.h>
+#include <linux/math64.h>
+#include <linux/of_irq.h>
+#include <linux/ptp_clock_kernel.h>
+#include <linux/spinlock.h>
+#include <linux/time64.h>
+#include <net/dsa.h>
+
+#include "xilinx_tsn.h"
+
+/* Reading the nanoseconds register latches a full RTC snapshot
+ * (ns + sec_low + sec_high). The subsequent seconds reads return
+ * those latched values, so nanoseconds must be read first.
+ */
+static void xlnx_tsn_tod_read(struct xlnx_tsn *sw, struct timespec64 *ts)
+{
+ struct xlnx_tsn_mac *m = &sw->mac[XLNX_TSN_PORT_MAC1];
+ u32 secl, sech, nsec;
+
+ nsec = mac_ior(m, TSN_TIMER_CURRENT_RTC_NS);
+ secl = mac_ior(m, TSN_TIMER_CURRENT_RTC_SEC_L);
+ sech = mac_ior(m, TSN_TIMER_CURRENT_RTC_SEC_H);
+
+ ts->tv_sec = (((u64)sech << 32) | secl) & TSN_TIMER_MAX_SEC_MASK;
+ ts->tv_nsec = nsec & TSN_TIMER_MAX_NSEC_MASK;
+}
+
+static void xlnx_tsn_rtc_offset_write(struct xlnx_tsn *sw,
+ const struct timespec64 *ts)
+{
+ struct xlnx_tsn_mac *m = &sw->mac[XLNX_TSN_PORT_MAC1];
+
+ mac_iow(m, TSN_TIMER_RTC_OFFSET_SEC_H, upper_32_bits(ts->tv_sec));
+ mac_iow(m, TSN_TIMER_RTC_OFFSET_SEC_L, lower_32_bits(ts->tv_sec));
+ mac_iow(m, TSN_TIMER_RTC_OFFSET_NS, ts->tv_nsec);
+}
+
+static void xlnx_tsn_rtc_offset_read(struct xlnx_tsn *sw,
+ struct timespec64 *ts)
+{
+ struct xlnx_tsn_mac *m = &sw->mac[XLNX_TSN_PORT_MAC1];
+ u32 secl, sech, nsec;
+
+ secl = mac_ior(m, TSN_TIMER_RTC_OFFSET_SEC_L);
+ sech = mac_ior(m, TSN_TIMER_RTC_OFFSET_SEC_H);
+ nsec = mac_ior(m, TSN_TIMER_RTC_OFFSET_NS);
+
+ ts->tv_sec = (((u64)sech << 32) | secl) & TSN_TIMER_MAX_SEC_MASK;
+ ts->tv_nsec = nsec & TSN_TIMER_MAX_NSEC_MASK;
+}
+
+static int xlnx_tsn_ptp_adjfine(struct ptp_clock_info *ptp, long scaled_ppm)
+{
+ struct xlnx_tsn *sw = container_of(ptp, struct xlnx_tsn, ptp_clock_info);
+ u64 incval;
+
+ /* adjust_by_scaled_ppm() returns u64 but the increment register is
+ * 32 bits, so clamp to U32_MAX to avoid overflow.
+ */
+ incval = adjust_by_scaled_ppm(sw->rtc_value, scaled_ppm);
+ if (incval > U32_MAX)
+ incval = U32_MAX;
+
+ guard(spinlock_irqsave)(&sw->reg_lock);
+ mac_iow(&sw->mac[XLNX_TSN_PORT_MAC1], TSN_TIMER_RTC_INCREMENT,
+ (u32)incval);
+
+ return 0;
+}
+
+static int xlnx_tsn_ptp_adjtime(struct ptp_clock_info *ptp, s64 delta)
+{
+ struct xlnx_tsn *sw = container_of(ptp, struct xlnx_tsn, ptp_clock_info);
+ struct timespec64 now, then = ns_to_timespec64(delta);
+
+ guard(spinlock_irqsave)(&sw->reg_lock);
+
+ xlnx_tsn_rtc_offset_read(sw, &now);
+ now = timespec64_add(now, then);
+
+ /* Stepping time backwards is fine and just lowers the offset. In
+ * practice the offset never goes negative. Reject it only as a
+ * safety net, since the offset register cannot store a negative value.
+ */
+ if (now.tv_sec < 0)
+ return -ERANGE;
+
+ xlnx_tsn_rtc_offset_write(sw, &now);
+
+ return 0;
+}
+
+static int xlnx_tsn_ptp_gettime(struct ptp_clock_info *ptp,
+ struct timespec64 *ts)
+{
+ struct xlnx_tsn *sw = container_of(ptp, struct xlnx_tsn, ptp_clock_info);
+
+ guard(spinlock_irqsave)(&sw->reg_lock);
+ xlnx_tsn_tod_read(sw, ts);
+
+ return 0;
+}
+
+static int xlnx_tsn_ptp_settime(struct ptp_clock_info *ptp,
+ const struct timespec64 *ts)
+{
+ struct xlnx_tsn *sw = container_of(ptp, struct xlnx_tsn, ptp_clock_info);
+ struct timespec64 delta, tod, offset, counter;
+
+ guard(spinlock_irqsave)(&sw->reg_lock);
+
+ xlnx_tsn_tod_read(sw, &tod);
+ xlnx_tsn_rtc_offset_read(sw, &offset);
+ counter = timespec64_sub(tod, offset);
+
+ delta = timespec64_sub(*ts, counter);
+
+ /* A real wall-clock time is always far above the free-running counter,
+ * so this never triggers in practice. Reject it only as a safety net,
+ * since the offset register cannot store a negative value.
+ */
+ if (delta.tv_sec < 0)
+ return -ERANGE;
+
+ xlnx_tsn_rtc_offset_write(sw, &delta);
+
+ return 0;
+}
+
+static int xlnx_tsn_ptp_enable(struct ptp_clock_info *ptp,
+ struct ptp_clock_request *rq, int on)
+{
+ struct xlnx_tsn *sw = container_of(ptp, struct xlnx_tsn, ptp_clock_info);
+
+ switch (rq->type) {
+ case PTP_CLK_REQ_PPS:
+ WRITE_ONCE(sw->pps_enable, on ? 1 : 0);
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static irqreturn_t xlnx_tsn_ptp_timer_isr(int irq, void *priv)
+{
+ struct ptp_clock_event event = { .type = PTP_CLOCK_PPS };
+ struct xlnx_tsn *sw = priv;
+
+ sw->countpulse++;
+ if (sw->countpulse >= TSN_TIMER_PULSES_PER_PPS) {
+ sw->countpulse = 0;
+ if (sw->ptp_clock && READ_ONCE(sw->pps_enable))
+ ptp_clock_event(sw->ptp_clock, &event);
+ }
+
+ mac_iow(&sw->mac[XLNX_TSN_PORT_MAC1], TSN_TIMER_INTERRUPT,
+ TSN_TIMER_INT_CLEAR);
+
+ return IRQ_HANDLED;
+}
+
+int xlnx_tsn_ptp_init(struct xlnx_tsn *sw)
+{
+ struct timespec64 ts;
+ int ret;
+
+ spin_lock_init(&sw->reg_lock);
+
+ sw->ptp_timer_irq = of_irq_get_byname(sw->dev->of_node, "ptp_timer");
+ if (sw->ptp_timer_irq <= 0)
+ return dev_err_probe(sw->dev, sw->ptp_timer_irq ? : -ENXIO,
+ "failed to get ptp_timer IRQ\n");
+
+ sw->ptp_clock_info.owner = THIS_MODULE;
+ snprintf(sw->ptp_clock_info.name, sizeof(sw->ptp_clock_info.name),
+ "TSN PHC");
+ sw->ptp_clock_info.max_adj = 999999999;
+ sw->ptp_clock_info.pps = 1;
+ sw->ptp_clock_info.adjfine = xlnx_tsn_ptp_adjfine;
+ sw->ptp_clock_info.adjtime = xlnx_tsn_ptp_adjtime;
+ sw->ptp_clock_info.gettime64 = xlnx_tsn_ptp_gettime;
+ sw->ptp_clock_info.settime64 = xlnx_tsn_ptp_settime;
+ sw->ptp_clock_info.enable = xlnx_tsn_ptp_enable;
+
+ sw->ptp_clock = ptp_clock_register(&sw->ptp_clock_info, sw->dev);
+ if (IS_ERR_OR_NULL(sw->ptp_clock)) {
+ ret = sw->ptp_clock ? PTR_ERR(sw->ptp_clock) : -ENODEV;
+ sw->ptp_clock = NULL;
+ return dev_err_probe(sw->dev, ret,
+ "failed to register PTP clock\n");
+ }
+
+ sw->rtc_value = div_u64(NSEC_PER_SEC, TSN_TIMER_GTX_CLK_FREQ) <<
+ TSN_TIMER_RTC_NS_SHIFT;
+ mac_iow(&sw->mac[XLNX_TSN_PORT_MAC1], TSN_TIMER_RTC_INCREMENT,
+ (u32)sw->rtc_value);
+
+ ts = ktime_to_timespec64(ktime_get_real());
+ xlnx_tsn_ptp_settime(&sw->ptp_clock_info, &ts);
+
+ ret = request_irq(sw->ptp_timer_irq, xlnx_tsn_ptp_timer_isr, 0,
+ "xlnx-tsn-ptp-timer", sw);
+ if (ret) {
+ dev_err_probe(sw->dev, ret,
+ "failed to request ptp_timer IRQ %d\n",
+ sw->ptp_timer_irq);
+ goto err_unregister_clock;
+ }
+
+ return 0;
+
+err_unregister_clock:
+ ptp_clock_unregister(sw->ptp_clock);
+ sw->ptp_clock = NULL;
+ return ret;
+}
+
+void xlnx_tsn_ptp_exit(struct xlnx_tsn *sw)
+{
+ if (!sw->ptp_clock)
+ return;
+
+ free_irq(sw->ptp_timer_irq, sw);
+ ptp_clock_unregister(sw->ptp_clock);
+ sw->ptp_clock = NULL;
+}
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 16/20] net: dsa: xilinx: drive per-MAC PTP TX/RX hardware paths
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (14 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 15/20] net: dsa: xilinx: register PHC backed by the RTC timer block Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 17/20] net: dsa: xilinx: opt into TX forwarding offload on bridge join Nagadheeraj Rottela
` (4 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
PTP event frames bypass the switch fabric. Each MAC has its own PTP
TX and PTP RX register windows in the per-MAC register space, plus
two dedicated interrupts. A frame captured in a MAC's PTP RX buffer
goes directly to the matching user port. A frame pushed to a MAC's
PTP TX buffer transmits out the same port without ever entering the
conduit or switch.
Publish the TX path to the tag protocol through ds->tagger_data
in setup(). Using a function pointer through tagger_data rather
than an exported symbol avoids a link-time dependency from the tag
module to the switch module.
TX timestamping accepts HWTSTAMP_TX_OFF and HWTSTAMP_TX_ON. Accept
any PTP v2 event RX filter and promote it to
HWTSTAMP_FILTER_PTP_V2_L2_EVENT. The per-MAC frame-filter block cannot
narrow the filter by message type.
Co-developed-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Srinivas Neeli <srinivas.neeli@amd.com>
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
MAINTAINERS | 1 +
drivers/net/dsa/xilinx/xilinx_tsn.c | 24 ++
drivers/net/dsa/xilinx/xilinx_tsn.h | 76 +++++
drivers/net/dsa/xilinx/xilinx_tsn_ptp.c | 357 +++++++++++++++++++++++-
include/linux/dsa/xlnx_tsn.h | 25 ++
net/dsa/tag_xlnx_tsn.c | 29 +-
6 files changed, 510 insertions(+), 2 deletions(-)
create mode 100644 include/linux/dsa/xlnx_tsn.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 0e91cb2a307d..c44b6fdd5b4a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -29615,6 +29615,7 @@ S: Maintained
F: Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
F: drivers/net/dsa/xilinx/
F: drivers/net/ethernet/xilinx/tsn/
+F: include/linux/dsa/xlnx_tsn.h
F: net/dsa/tag_xlnx_tsn.c
XILINX UARTLITE SERIAL DRIVER
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 318d8b332208..e48c81f18d6e 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -18,6 +18,7 @@
#include <linux/phy.h>
#include <linux/phylink.h>
#include <linux/platform_device.h>
+#include <linux/dsa/xlnx_tsn.h>
#include <net/dsa.h>
#include "xilinx_tsn.h"
@@ -679,8 +680,25 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
if (ret)
goto err_nb;
+ ret = xlnx_tsn_port_ptp_init(sw, XLNX_TSN_PORT_MAC1,
+ "ptp_rx_mac1", "ptp_tx_mac1");
+ if (ret)
+ goto err_ptp_exit;
+
+ ret = xlnx_tsn_port_ptp_init(sw, XLNX_TSN_PORT_MAC2,
+ "ptp_rx_mac2", "ptp_tx_mac2");
+ if (ret)
+ goto err_ptp_mac1;
+
+ sw->tagger_data.ptp_tx = xlnx_tsn_ptp_tx;
+ ds->tagger_data = &sw->tagger_data;
+
return 0;
+err_ptp_mac1:
+ xlnx_tsn_port_ptp_exit(sw, XLNX_TSN_PORT_MAC1);
+err_ptp_exit:
+ xlnx_tsn_ptp_exit(sw);
err_nb:
unregister_netdevice_notifier(&sw->nb);
err_mdio:
@@ -692,7 +710,10 @@ static void xlnx_tsn_teardown(struct dsa_switch *ds)
{
struct xlnx_tsn *sw = ds->priv;
struct dsa_port *dp;
+ int port;
+ for (port = XLNX_TSN_PORT_MAC1; port <= XLNX_TSN_PORT_MAC2; port++)
+ xlnx_tsn_port_ptp_exit(sw, port);
xlnx_tsn_ptp_exit(sw);
unregister_netdevice_notifier(&sw->nb);
xlnx_tsn_mdio_unregister_all(sw);
@@ -709,6 +730,9 @@ static const struct dsa_switch_ops xlnx_tsn_switch_ops = {
.teardown = xlnx_tsn_teardown,
.port_set_mac_address = xlnx_tsn_port_set_mac_address,
.port_stp_state_set = xlnx_tsn_port_stp_state_set,
+ .port_hwtstamp_get = xlnx_tsn_port_hwtstamp_get,
+ .port_hwtstamp_set = xlnx_tsn_port_hwtstamp_set,
+ .get_ts_info = xlnx_tsn_get_ts_info,
.phylink_get_caps = xlnx_tsn_phylink_get_caps,
};
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index 6063763b5c17..d46150535775 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -9,10 +9,14 @@
#include <linux/bits.h>
#include <linux/if_ether.h>
#include <linux/io.h>
+#include <linux/net_tstamp.h>
#include <linux/notifier.h>
#include <linux/ptp_clock_kernel.h>
+#include <linux/skbuff.h>
#include <linux/spinlock.h>
#include <linux/types.h>
+#include <linux/workqueue.h>
+#include <linux/dsa/xlnx_tsn.h>
#include <net/dsa.h>
#define XLNX_TSN_NUM_PORTS 3
@@ -127,7 +131,44 @@ enum tsn_port_state {
#define TSN_TIMER_PULSES_PER_PPS 128
#define TSN_TIMER_GTX_CLK_FREQ 125000000U
+/* Per-MAC PTP TX / RX register windows, sitting inside each per-MAC
+ * reg space. Each PTP TX slot is 256 B wide; the first 8 B hold the
+ * cmd1/cmd2 header, leaving 248 B for frame data. HW provides 8 slots.
+ * The PTP RX buffer mirrors the layout with a 252 B usable area and
+ * an 8 B HW timestamp footer.
+ */
+#define TSN_PTP_TX_CONTROL_OFFSET 0x00012000
+#define TSN_PTP_RX_CONTROL_OFFSET 0x00012004
+
+#define TSN_PTP_RX_BASE_OFFSET 0x00010000
+#define TSN_PTP_RX_PACKET_FIELD_MASK GENMASK(11, 8)
+#define TSN_PTP_RX_PACKET_CLEAR BIT(0)
+
+#define TSN_PTP_TX_BASE_OFFSET 0x00011000
+#define TSN_PTP_TX_HWBUF_SIZE 0x100
+#define TSN_PTP_TX_BUFFERS 8
+#define TSN_PTP_TX_BUFFER_OFFSET(i) (TSN_PTP_TX_BASE_OFFSET + \
+ (i) * TSN_PTP_TX_HWBUF_SIZE)
+#define TSN_PTP_TX_CMD_FIELD_LEN 8
+#define TSN_PTP_TX_MAX_FRAME_SIZE (TSN_PTP_TX_HWBUF_SIZE - \
+ TSN_PTP_TX_CMD_FIELD_LEN)
+#define TSN_PTP_TX_BUFFER_CMD2_FIELD 0x4
+
+#define TSN_PTP_TX_FRAME_WAITING_MASK GENMASK(15, 8)
+#define TSN_PTP_TX_BUFFERS_FULL_MASK BIT(TSN_PTP_TX_BUFFERS - 1)
+#define TSN_PTP_TX_PACKET_FIELD_MASK GENMASK(18, 16)
+
+#define TSN_PTP_HW_TSTAMP_SIZE 8
+#define TSN_PTP_RX_HWBUF_SIZE 256
+#define TSN_PTP_RX_FRAME_SIZE 252
+#define TSN_PTP_HW_TSTAMP_OFFSET (TSN_PTP_RX_HWBUF_SIZE - \
+ TSN_PTP_HW_TSTAMP_SIZE)
+
+#define TSN_PTP_MSG_TYPE_MASK BIT(3)
+
+struct kernel_ethtool_ts_info;
struct mii_bus;
+struct netlink_ext_ack;
struct xlnx_tsn;
/**
@@ -137,11 +178,32 @@ struct xlnx_tsn;
* @regs: per-MAC register window, from reg-name "macN"
* @mii_bus: MDIO bus registered under the "mdio-macN" DT child,
* or NULL if absent
+ * @ptp_tx_irq: per-MAC PTP TX-completion interrupt
+ * @ptp_rx_irq: per-MAC PTP RX interrupt
+ * @ptp_tx_lock: serialises the PTP TX slot allocator and the TX
+ * completion path
+ * @ptp_txq: in-flight PTP TX frames awaiting timestamp completion.
+ * Each frame's slot index is kept in skb->cb[0].
+ * @tx_tstamp_work: work item queued by the PTP TX IRQ to drain
+ * ptp_txq and deliver timestamps through skb_tstamp_tx()
+ * @ptp_rx_hw_pointer: HW write pointer snapshot read in the RX ISR
+ * @ptp_rx_sw_pointer: SW read pointer; drained until it catches up
+ * @hwtstamp_tx_type: current SO_TIMESTAMPING TX type for this port
+ * @hwtstamp_rx_filter: current SO_TIMESTAMPING RX filter for this port
*/
struct xlnx_tsn_mac {
struct xlnx_tsn *sw;
void __iomem *regs;
struct mii_bus *mii_bus;
+ int ptp_tx_irq;
+ int ptp_rx_irq;
+ spinlock_t ptp_tx_lock; /* serialises PTP TX slot alloc + completion */
+ struct sk_buff_head ptp_txq;
+ struct work_struct tx_tstamp_work;
+ u32 ptp_rx_hw_pointer;
+ u32 ptp_rx_sw_pointer;
+ int hwtstamp_tx_type;
+ int hwtstamp_rx_filter;
};
/**
@@ -167,6 +229,8 @@ struct xlnx_tsn_mac {
* used as the starting point for adjust_by_scaled_ppm()
* @pps_enable: user requested PPS event delivery
* @countpulse: timer-tick counter, reset to zero every TSN_TIMER_PULSES_PER_PPS ticks
+ * @tagger_data: PTP TX callback descriptor handed to the tag protocol
+ * via @dsa_switch.tagger_data
*/
struct xlnx_tsn {
struct dsa_switch ds;
@@ -183,6 +247,7 @@ struct xlnx_tsn {
u64 rtc_value;
int pps_enable;
int countpulse;
+ struct xlnx_tsn_tagger_data tagger_data;
};
static inline void mac_iow(struct xlnx_tsn_mac *m, u32 off, u32 val)
@@ -197,5 +262,16 @@ static inline u32 mac_ior(struct xlnx_tsn_mac *m, u32 off)
int xlnx_tsn_ptp_init(struct xlnx_tsn *sw);
void xlnx_tsn_ptp_exit(struct xlnx_tsn *sw);
+int xlnx_tsn_port_ptp_init(struct xlnx_tsn *sw, int port,
+ const char *rx_name, const char *tx_name);
+void xlnx_tsn_port_ptp_exit(struct xlnx_tsn *sw, int port);
+void xlnx_tsn_ptp_tx(struct dsa_port *dp, struct sk_buff *skb);
+int xlnx_tsn_port_hwtstamp_get(struct dsa_switch *ds, int port,
+ struct kernel_hwtstamp_config *config);
+int xlnx_tsn_port_hwtstamp_set(struct dsa_switch *ds, int port,
+ struct kernel_hwtstamp_config *config,
+ struct netlink_ext_ack *extack);
+int xlnx_tsn_get_ts_info(struct dsa_switch *ds, int port,
+ struct kernel_ethtool_ts_info *info);
#endif /* _XILINX_TSN_H */
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn_ptp.c b/drivers/net/dsa/xilinx/xilinx_tsn_ptp.c
index 75c177f752f1..740d92e3e80d 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn_ptp.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn_ptp.c
@@ -1,18 +1,26 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* AMD/Xilinx TSN Endpoint Ethernet MAC DSA switch driver:
- * PTP hardware clock.
+ * PTP hardware clock and per-MAC PTP TX/RX paths.
*/
+#include <linux/bitfield.h>
#include <linux/cleanup.h>
+#include <linux/etherdevice.h>
+#include <linux/ethtool.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/ktime.h>
#include <linux/math64.h>
+#include <linux/net_tstamp.h>
+#include <linux/netdevice.h>
#include <linux/of_irq.h>
#include <linux/ptp_clock_kernel.h>
+#include <linux/skbuff.h>
#include <linux/spinlock.h>
#include <linux/time64.h>
+#include <linux/unaligned.h>
+#include <linux/workqueue.h>
#include <net/dsa.h>
#include "xilinx_tsn.h"
@@ -233,3 +241,350 @@ void xlnx_tsn_ptp_exit(struct xlnx_tsn *sw)
ptp_clock_unregister(sw->ptp_clock);
sw->ptp_clock = NULL;
}
+
+static void memcpy_toio_32(struct xlnx_tsn_mac *m, unsigned long off,
+ const u8 *data, size_t len)
+{
+ while (len >= 4) {
+ mac_iow(m, off, get_unaligned((const u32 *)data));
+ off += 4;
+ data += 4;
+ len -= 4;
+ }
+
+ if (len) {
+ u32 leftover = 0;
+ u8 *dst = (u8 *)&leftover;
+
+ while (len--)
+ *dst++ = *data++;
+ mac_iow(m, off, leftover);
+ }
+}
+
+static void memcpy_fromio_32(struct xlnx_tsn_mac *m, unsigned long off,
+ u8 *data, size_t len)
+{
+ while (len >= 4) {
+ put_unaligned(mac_ior(m, off), (u32 *)data);
+ off += 4;
+ data += 4;
+ len -= 4;
+ }
+
+ if (len) {
+ u32 leftover = mac_ior(m, off);
+ u8 *src = (u8 *)&leftover;
+
+ while (len--)
+ *data++ = *src++;
+ }
+}
+
+static void xlnx_tsn_read_tstamp(struct xlnx_tsn_mac *m,
+ struct skb_shared_hwtstamps *hwtstamps,
+ unsigned int off)
+{
+ u32 captured_ns, captured_sec;
+
+ memset(hwtstamps, 0, sizeof(*hwtstamps));
+
+ captured_ns = mac_ior(m, off + 4);
+ captured_sec = mac_ior(m, off);
+
+ hwtstamps->hwtstamp = ktime_set(captured_sec, captured_ns);
+}
+
+void xlnx_tsn_ptp_tx(struct dsa_port *dp, struct sk_buff *skb)
+{
+ struct xlnx_tsn *sw = dp->ds->priv;
+ u32 frame_waiting, cmd1, cmd2 = 0;
+ struct xlnx_tsn_mac *m;
+ u8 free_index;
+
+ m = &sw->mac[dp->index];
+
+ if (unlikely(skb->len > TSN_PTP_TX_MAX_FRAME_SIZE)) {
+ dev_kfree_skb_any(skb);
+ return;
+ }
+
+ scoped_guard(spinlock_irqsave, &m->ptp_tx_lock) {
+ frame_waiting = FIELD_GET(TSN_PTP_TX_FRAME_WAITING_MASK,
+ mac_ior(m, TSN_PTP_TX_CONTROL_OFFSET));
+ if (frame_waiting & TSN_PTP_TX_BUFFERS_FULL_MASK) {
+ dev_kfree_skb_any(skb);
+ return;
+ }
+
+ free_index = fls(frame_waiting);
+ cmd1 = skb->len;
+
+ mac_iow(m, TSN_PTP_TX_BUFFER_OFFSET(free_index), cmd1);
+ mac_iow(m, TSN_PTP_TX_BUFFER_OFFSET(free_index) +
+ TSN_PTP_TX_BUFFER_CMD2_FIELD, cmd2);
+ memcpy_toio_32(m,
+ TSN_PTP_TX_BUFFER_OFFSET(free_index) +
+ TSN_PTP_TX_CMD_FIELD_LEN,
+ skb->data, skb->len);
+
+ skb->cb[0] = free_index;
+ __skb_queue_tail(&m->ptp_txq, skb);
+
+ if (skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP)
+ skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;
+
+ skb_tx_timestamp(skb);
+ mac_iow(m, TSN_PTP_TX_CONTROL_OFFSET, BIT(free_index));
+ }
+}
+
+static void xlnx_tsn_ptp_recv(struct xlnx_tsn *sw, int port)
+{
+ struct net_device *user = dsa_to_port(&sw->ds, port)->user;
+ struct xlnx_tsn_mac *m = &sw->mac[port];
+ unsigned long frame_base;
+ struct sk_buff *skb;
+ u16 msg_len;
+ u8 msg_type;
+
+ if (!user || !netif_running(user))
+ return;
+
+ while ((m->ptp_rx_hw_pointer & 0xf) != (m->ptp_rx_sw_pointer & 0xf)) {
+ m->ptp_rx_sw_pointer++;
+
+ frame_base = TSN_PTP_RX_BASE_OFFSET +
+ (m->ptp_rx_sw_pointer & 0xf) *
+ TSN_PTP_RX_HWBUF_SIZE;
+
+ skb = netdev_alloc_skb(user, TSN_PTP_RX_FRAME_SIZE);
+ if (!skb) {
+ DEV_STATS_INC(user, rx_dropped);
+ continue;
+ }
+
+ memcpy_fromio_32(m, frame_base, skb->data,
+ TSN_PTP_RX_FRAME_SIZE);
+
+ msg_type = *(u8 *)(skb->data + ETH_HLEN) & 0xf;
+ msg_len = get_unaligned_be16(skb->data + ETH_HLEN + 2);
+
+ if (msg_len + ETH_HLEN > TSN_PTP_RX_FRAME_SIZE) {
+ dev_kfree_skb_any(skb);
+ DEV_STATS_INC(user, rx_length_errors);
+ continue;
+ }
+
+ skb_put(skb, msg_len + ETH_HLEN);
+ skb->protocol = eth_type_trans(skb, user);
+ skb->ip_summed = CHECKSUM_UNNECESSARY;
+
+ if (READ_ONCE(m->hwtstamp_rx_filter) != HWTSTAMP_FILTER_NONE &&
+ !(msg_type & TSN_PTP_MSG_TYPE_MASK))
+ xlnx_tsn_read_tstamp(m, skb_hwtstamps(skb),
+ frame_base +
+ TSN_PTP_HW_TSTAMP_OFFSET);
+
+ dev_sw_netstats_rx_add(user, skb->len);
+ netif_rx(skb);
+ }
+}
+
+static irqreturn_t xlnx_tsn_ptp_rx_isr(int irq, void *data)
+{
+ struct xlnx_tsn_mac *m = data;
+ struct xlnx_tsn *sw = m->sw;
+ int port = m - sw->mac;
+
+ m->ptp_rx_hw_pointer = FIELD_GET(TSN_PTP_RX_PACKET_FIELD_MASK,
+ mac_ior(m, TSN_PTP_RX_CONTROL_OFFSET));
+ xlnx_tsn_ptp_recv(sw, port);
+
+ return IRQ_HANDLED;
+}
+
+static void xlnx_tsn_tx_tstamp_work(struct work_struct *work)
+{
+ struct xlnx_tsn_mac *m = container_of(work, struct xlnx_tsn_mac,
+ tx_tstamp_work);
+ struct skb_shared_hwtstamps hwtstamps;
+ unsigned long ts_off;
+ struct sk_buff *skb;
+ u8 tx_packet, index;
+
+ guard(spinlock_irqsave)(&m->ptp_tx_lock);
+
+ tx_packet = FIELD_GET(TSN_PTP_TX_PACKET_FIELD_MASK,
+ mac_ior(m, TSN_PTP_TX_CONTROL_OFFSET));
+
+ while ((skb = __skb_dequeue(&m->ptp_txq)) != NULL) {
+ index = skb->cb[0];
+
+ /* HW writes ascending slot indices into the TX status field
+ * as frames depart. Any queued skb with index > tx_packet
+ * has not been timestamped yet, so requeue it and stop.
+ */
+ if (index > tx_packet) {
+ __skb_queue_head(&m->ptp_txq, skb);
+ break;
+ }
+
+ ts_off = TSN_PTP_TX_BUFFER_OFFSET(index) +
+ TSN_PTP_HW_TSTAMP_OFFSET;
+
+ if (skb_shinfo(skb)->tx_flags & SKBTX_IN_PROGRESS) {
+ xlnx_tsn_read_tstamp(m, &hwtstamps, ts_off);
+ skb_tstamp_tx(skb, &hwtstamps);
+ }
+ consume_skb(skb);
+ }
+}
+
+static irqreturn_t xlnx_tsn_ptp_tx_isr(int irq, void *data)
+{
+ struct xlnx_tsn_mac *m = data;
+
+ mac_ior(m, TSN_PTP_TX_CONTROL_OFFSET);
+ schedule_work(&m->tx_tstamp_work);
+
+ return IRQ_HANDLED;
+}
+
+int xlnx_tsn_port_ptp_init(struct xlnx_tsn *sw, int port,
+ const char *rx_name, const char *tx_name)
+{
+ struct xlnx_tsn_mac *m = &sw->mac[port];
+ int ret;
+
+ skb_queue_head_init(&m->ptp_txq);
+ spin_lock_init(&m->ptp_tx_lock);
+ INIT_WORK(&m->tx_tstamp_work, xlnx_tsn_tx_tstamp_work);
+ m->ptp_rx_hw_pointer = 0;
+ m->ptp_rx_sw_pointer = 0xff;
+ m->hwtstamp_tx_type = HWTSTAMP_TX_OFF;
+ m->hwtstamp_rx_filter = HWTSTAMP_FILTER_NONE;
+
+ m->ptp_rx_irq = of_irq_get_byname(sw->dev->of_node, rx_name);
+ if (m->ptp_rx_irq <= 0)
+ return dev_err_probe(sw->dev, m->ptp_rx_irq ? : -ENXIO,
+ "failed to get %s IRQ\n", rx_name);
+
+ m->ptp_tx_irq = of_irq_get_byname(sw->dev->of_node, tx_name);
+ if (m->ptp_tx_irq <= 0)
+ return dev_err_probe(sw->dev, m->ptp_tx_irq ? : -ENXIO,
+ "failed to get %s IRQ\n", tx_name);
+
+ mac_iow(m, TSN_PTP_RX_CONTROL_OFFSET, TSN_PTP_RX_PACKET_CLEAR);
+
+ ret = request_irq(m->ptp_rx_irq, xlnx_tsn_ptp_rx_isr, 0, rx_name, m);
+ if (ret)
+ return dev_err_probe(sw->dev, ret,
+ "failed to request %s IRQ %d\n",
+ rx_name, m->ptp_rx_irq);
+
+ ret = request_irq(m->ptp_tx_irq, xlnx_tsn_ptp_tx_isr, 0, tx_name, m);
+ if (ret) {
+ free_irq(m->ptp_rx_irq, m);
+ return dev_err_probe(sw->dev, ret,
+ "failed to request %s IRQ %d\n",
+ tx_name, m->ptp_tx_irq);
+ }
+
+ return 0;
+}
+
+void xlnx_tsn_port_ptp_exit(struct xlnx_tsn *sw, int port)
+{
+ struct xlnx_tsn_mac *m = &sw->mac[port];
+ struct sk_buff *skb;
+
+ if (m->ptp_tx_irq > 0)
+ free_irq(m->ptp_tx_irq, m);
+
+ if (m->ptp_rx_irq > 0)
+ free_irq(m->ptp_rx_irq, m);
+
+ cancel_work_sync(&m->tx_tstamp_work);
+
+ scoped_guard(spinlock_irqsave, &m->ptp_tx_lock)
+ while ((skb = __skb_dequeue(&m->ptp_txq)) != NULL)
+ dev_kfree_skb_any(skb);
+}
+
+int xlnx_tsn_port_hwtstamp_get(struct dsa_switch *ds, int port,
+ struct kernel_hwtstamp_config *config)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ struct xlnx_tsn_mac *m;
+
+ m = &sw->mac[port];
+
+ if (port == XLNX_TSN_CPU_PORT)
+ return -EOPNOTSUPP;
+
+ config->tx_type = m->hwtstamp_tx_type;
+ config->rx_filter = m->hwtstamp_rx_filter;
+
+ return 0;
+}
+
+int xlnx_tsn_port_hwtstamp_set(struct dsa_switch *ds, int port,
+ struct kernel_hwtstamp_config *config,
+ struct netlink_ext_ack *extack)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ struct xlnx_tsn_mac *m;
+
+ m = &sw->mac[port];
+
+ if (port == XLNX_TSN_CPU_PORT)
+ return -EOPNOTSUPP;
+
+ switch (config->tx_type) {
+ case HWTSTAMP_TX_OFF:
+ case HWTSTAMP_TX_ON:
+ break;
+ default:
+ return -ERANGE;
+ }
+
+ /* The per-MAC RX filter is left at its power-on default, which
+ * captures L2 gPTP event frames (ethertype 0x88f7) only. PTP over
+ * UDP is not supported. The filter cannot narrow by message type,
+ * so any L2 PTPv2 event request is promoted to
+ * HWTSTAMP_FILTER_PTP_V2_L2_EVENT.
+ */
+ switch (config->rx_filter) {
+ case HWTSTAMP_FILTER_NONE:
+ break;
+ case HWTSTAMP_FILTER_PTP_V2_L2_EVENT:
+ case HWTSTAMP_FILTER_PTP_V2_L2_SYNC:
+ case HWTSTAMP_FILTER_PTP_V2_L2_DELAY_REQ:
+ config->rx_filter = HWTSTAMP_FILTER_PTP_V2_L2_EVENT;
+ break;
+ default:
+ return -ERANGE;
+ }
+
+ m->hwtstamp_tx_type = config->tx_type;
+ WRITE_ONCE(m->hwtstamp_rx_filter, config->rx_filter);
+
+ return 0;
+}
+
+int xlnx_tsn_get_ts_info(struct dsa_switch *ds, int port,
+ struct kernel_ethtool_ts_info *info)
+{
+ struct xlnx_tsn *sw = ds->priv;
+
+ info->phc_index = sw->ptp_clock ? ptp_clock_index(sw->ptp_clock) : -1;
+ info->so_timestamping = SOF_TIMESTAMPING_TX_HARDWARE |
+ SOF_TIMESTAMPING_RX_HARDWARE |
+ SOF_TIMESTAMPING_RAW_HARDWARE;
+ info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON);
+ info->rx_filters = BIT(HWTSTAMP_FILTER_NONE) |
+ BIT(HWTSTAMP_FILTER_PTP_V2_L2_EVENT);
+
+ return 0;
+}
diff --git a/include/linux/dsa/xlnx_tsn.h b/include/linux/dsa/xlnx_tsn.h
new file mode 100644
index 000000000000..34a451f4568a
--- /dev/null
+++ b/include/linux/dsa/xlnx_tsn.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * AMD/Xilinx TSN Endpoint Ethernet MAC: shared tagger / switch
+ * private interface.
+ */
+#ifndef _NET_DSA_XLNX_TSN_H
+#define _NET_DSA_XLNX_TSN_H
+
+#include <linux/skbuff.h>
+#include <net/dsa.h>
+
+/**
+ * struct xlnx_tsn_tagger_data - per-switch context for the tag protocol
+ * @ptp_tx: callback that copies a PTP event frame into the per-port
+ * hardware TX buffer and holds the skb until the TX IRQ
+ * delivers the timestamp. Consumes one reference to @skb.
+ *
+ * Set on @dsa_switch.tagger_data by the switch driver in setup()
+ * and read by the tag protocol's xmit hook.
+ */
+struct xlnx_tsn_tagger_data {
+ void (*ptp_tx)(struct dsa_port *dp, struct sk_buff *skb);
+};
+
+#endif /* _NET_DSA_XLNX_TSN_H */
diff --git a/net/dsa/tag_xlnx_tsn.c b/net/dsa/tag_xlnx_tsn.c
index de352aa3d9a8..6f487ad7bcf9 100644
--- a/net/dsa/tag_xlnx_tsn.c
+++ b/net/dsa/tag_xlnx_tsn.c
@@ -3,14 +3,41 @@
* AMD/Xilinx TSN Endpoint Ethernet MAC tag protocol.
*/
+#include <linux/dsa/xlnx_tsn.h>
+#include <linux/if_vlan.h>
+
#include "tag.h"
#define XLNX_TSN_NAME "xlnx_tsn"
+/* PTP frames must go directly into the egress MAC's hardware TX buffer,
+ * not through the switch fabric or conduit DMA. Intercept here before
+ * dsa_enqueue_skb() takes the frame and hand off our reference: ptp_tx()
+ * consumes it, freeing the skb once the HW timestamp is read (or on
+ * error).
+ *
+ * VLAN-tagged PTP is not supported. Match on the L2 ethertype and
+ * skip any VLAN-tagged frame.
+ */
static struct sk_buff *xlnx_tsn_xmit(struct sk_buff *skb,
struct net_device *dev)
{
- return skb;
+ struct xlnx_tsn_tagger_data *tagger_data;
+ struct dsa_port *dp;
+
+ dp = dsa_user_to_port(dev);
+ tagger_data = dp->ds->tagger_data;
+
+ if (!tagger_data || !tagger_data->ptp_tx)
+ return skb;
+
+ if (eth_hdr(skb)->h_proto != htons(ETH_P_1588) ||
+ skb_vlan_tag_present(skb))
+ return skb;
+
+ tagger_data->ptp_tx(dp, skb);
+
+ return NULL;
}
static struct sk_buff *xlnx_tsn_rcv(struct sk_buff *skb,
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 17/20] net: dsa: xilinx: opt into TX forwarding offload on bridge join
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (15 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 16/20] net: dsa: xilinx: drive per-MAC PTP TX/RX hardware paths Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 18/20] net: dsa: xilinx: offload the bridge FDB to the switch CAM Nagadheeraj Rottela
` (3 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
Set *tx_fwd_offload in port_bridge_join. The bridge then sends one
copy via the offloaded TX path and the fabric handles per-port
replication.
The fabric offloads a single bridge across the user ports. Set
ds->max_num_bridges to 1 so the DSA core rejects a second bridge
instead of silently merging the ports.
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
drivers/net/dsa/xilinx/xilinx_tsn.c | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index e48c81f18d6e..9a4d4a2fb892 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -495,6 +495,19 @@ static int xlnx_tsn_port_set_mac_address(struct dsa_switch *ds, int port,
return 0;
}
+static int xlnx_tsn_port_bridge_join(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge,
+ bool *tx_fwd_offload,
+ struct netlink_ext_ack *extack)
+{
+ /* The switch fabric replicates flooded frames per egress port
+ * on its own, so the bridge does not need to clone-and-send.
+ */
+ *tx_fwd_offload = true;
+
+ return 0;
+}
+
static void xlnx_tsn_port_stp_state_set(struct dsa_switch *ds, int port,
u8 state)
{
@@ -729,6 +742,7 @@ static const struct dsa_switch_ops xlnx_tsn_switch_ops = {
.setup = xlnx_tsn_setup,
.teardown = xlnx_tsn_teardown,
.port_set_mac_address = xlnx_tsn_port_set_mac_address,
+ .port_bridge_join = xlnx_tsn_port_bridge_join,
.port_stp_state_set = xlnx_tsn_port_stp_state_set,
.port_hwtstamp_get = xlnx_tsn_port_hwtstamp_get,
.port_hwtstamp_set = xlnx_tsn_port_hwtstamp_set,
@@ -786,6 +800,9 @@ static int xlnx_tsn_probe(struct platform_device *pdev)
ds->phylink_mac_ops = &xlnx_tsn_phylink_mac_ops;
ds->priv = sw;
+ /* The fabric offloads a single bridge across the user ports. */
+ ds->max_num_bridges = 1;
+
platform_set_drvdata(pdev, sw);
return dsa_register_switch(ds);
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 18/20] net: dsa: xilinx: offload the bridge FDB to the switch CAM
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (16 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 17/20] net: dsa: xilinx: opt into TX forwarding offload on bridge join Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 19/20] net: dsa: xilinx: offload bridge VLAN filtering to the switch Nagadheeraj Rottela
` (2 subsequent siblings)
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
The switch resolves forwarding through a stream-destination lookup
CAM that maps a (destination MAC, VLAN ID) key to an egress port
list. Back the bridge FDB with it.
The CAM is an indirect register block. Load the key and port-list
registers, write the opcode (add, delete, read, or read-key) to the
control register, and wait for the operation to finish. Add a mutex
to serialise these sequences.
The fabric also learns source MACs into the same CAM. Enable the
learning engine in port_bridge_join, and add port_bridge_leave to
disable it once the last user port leaves, so entries accumulate
only while a bridge spans the ports.
port_fdb_add and port_fdb_del read the entry, set or clear the
calling port's bit in the port list, and write it back. Delete the
entry once no port references it. A (MAC, VID) reachable on several
ports then carries a correct multi-port list. The DSA "no VLAN" key
(vid 0) maps to the default native VID that an untagged frame uses.
port_fdb_dump reports a port's learnt entries. The hardware scatters
them across the port's read-key region. Scan all of it and report
every slot that is present. The CPU port has no such region.
port_fast_age flushes a port's learnt addresses from the CAM.
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
drivers/net/dsa/xilinx/xilinx_tsn.c | 301 +++++++++++++++++++++++++++-
drivers/net/dsa/xilinx/xilinx_tsn.h | 93 ++++++++-
2 files changed, 391 insertions(+), 3 deletions(-)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 9a4d4a2fb892..57558b0e2613 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -174,6 +174,31 @@ static int xlnx_tsn_set_port_state(struct xlnx_tsn *sw, int port,
return 0;
}
+static enum tsn_port_state xlnx_tsn_get_port_state(struct xlnx_tsn *sw,
+ int port)
+{
+ u32 mask, chg_bit, reg;
+
+ if (xlnx_tsn_port_state_bits(port, &mask, &chg_bit))
+ return TSN_PORT_STATE_DISABLED;
+
+ reg = sw_ior(sw, TSN_PORT_STATE_CTRL_OFFSET);
+ return (reg & mask) >> __ffs(mask);
+}
+
+static int xlnx_tsn_port_state_cycle(struct xlnx_tsn *sw, int port,
+ enum tsn_port_state state)
+{
+ enum tsn_port_state saved = xlnx_tsn_get_port_state(sw, port);
+ int err;
+
+ err = xlnx_tsn_set_port_state(sw, port, state);
+ if (err)
+ return err;
+
+ return xlnx_tsn_set_port_state(sw, port, saved);
+}
+
static int xlnx_tsn_mdio_wait_ready(struct xlnx_tsn_mac *m)
{
u32 val;
@@ -495,11 +520,32 @@ static int xlnx_tsn_port_set_mac_address(struct dsa_switch *ds, int port,
return 0;
}
+/* Enable or disable the hardware address learning engine globally.
+ * Standalone ports do not learn. Enable when a port joins a bridge,
+ * disable when the last one leaves. The sub-qualifier bits below are
+ * inert while learning is disabled. Caller holds indirect_lock.
+ */
+static void xlnx_tsn_set_global_learning(struct xlnx_tsn *sw, bool on)
+{
+ u32 reg = sw_ior(sw, TSN_SW_ADDR_LEARN_OFFSET);
+
+ if (on)
+ reg &= ~TSN_SW_ADDR_LEARN_DISABLE;
+ else
+ reg |= TSN_SW_ADDR_LEARN_DISABLE;
+ sw_iow(sw, TSN_SW_ADDR_LEARN_OFFSET, reg);
+}
+
static int xlnx_tsn_port_bridge_join(struct dsa_switch *ds, int port,
struct dsa_bridge bridge,
bool *tx_fwd_offload,
struct netlink_ext_ack *extack)
{
+ struct xlnx_tsn *sw = ds->priv;
+
+ scoped_guard(mutex, &sw->indirect_lock)
+ xlnx_tsn_set_global_learning(sw, true);
+
/* The switch fabric replicates flooded frames per egress port
* on its own, so the bridge does not need to clone-and-send.
*/
@@ -508,6 +554,24 @@ static int xlnx_tsn_port_bridge_join(struct dsa_switch *ds, int port,
return 0;
}
+static void xlnx_tsn_port_bridge_leave(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ struct dsa_port *dp;
+
+ dsa_switch_for_each_user_port(dp, ds) {
+ if (dp->index == port)
+ continue;
+
+ if (dp->bridge)
+ return;
+ }
+
+ scoped_guard(mutex, &sw->indirect_lock)
+ xlnx_tsn_set_global_learning(sw, false);
+}
+
static void xlnx_tsn_port_stp_state_set(struct dsa_switch *ds, int port,
u8 state)
{
@@ -539,6 +603,217 @@ static void xlnx_tsn_port_stp_state_set(struct dsa_switch *ds, int port,
xlnx_tsn_set_port_state(sw, port, hw_state);
}
+/* The CAM status enable bit reads 1 when the block is ready to accept the
+ * next operation.
+ */
+static int xlnx_tsn_cam_wait_ready(struct xlnx_tsn *sw)
+{
+ u32 reg;
+
+ return readl_poll_timeout(sw->sw_base + TSN_CAM_STATUS_OFFSET, reg,
+ reg & TSN_CAM_STATUS_READY, TSN_SW_POLL_DELAY_US,
+ TSN_SW_POLL_TIMEOUT_US);
+}
+
+/* The CAM control enable bit self-clears when the operation completes. */
+static int xlnx_tsn_cam_wait_done(struct xlnx_tsn *sw)
+{
+ u32 reg;
+
+ return readl_poll_timeout(sw->sw_base + TSN_CAM_CTRL_OFFSET, reg,
+ !(reg & TSN_CAM_OP_ENABLE),
+ TSN_SW_POLL_DELAY_US, TSN_SW_POLL_TIMEOUT_US);
+}
+
+static void xlnx_tsn_cam_load_key(struct xlnx_tsn *sw,
+ const unsigned char *addr, u16 vid)
+{
+ sw_iow(sw, TSN_CAM_KEY1_OFFSET,
+ ((u32)addr[0] << 24) | ((u32)addr[1] << 16) |
+ ((u32)addr[2] << 8) | addr[3]);
+ sw_iow(sw, TSN_CAM_KEY2_OFFSET,
+ ((u32)addr[4] << 8) | addr[5] |
+ FIELD_PREP(TSN_CAM_VLAN, vid));
+}
+
+/* Look up a (MAC, VID) key and return its current egress port list, or 0
+ * if the entry is absent. Caller holds indirect_lock.
+ */
+static int xlnx_tsn_cam_read_portlist(struct xlnx_tsn *sw,
+ const unsigned char *addr, u16 vid,
+ u8 *portlist)
+{
+ u32 ctrl;
+ int ret;
+
+ ret = xlnx_tsn_cam_wait_ready(sw);
+ if (ret)
+ return ret;
+
+ xlnx_tsn_cam_load_key(sw, addr, vid);
+ sw_iow(sw, TSN_CAM_CTRL_OFFSET,
+ FIELD_PREP(TSN_CAM_OP_MASK, TSN_CAM_OP_READ) | TSN_CAM_OP_ENABLE);
+
+ ret = xlnx_tsn_cam_wait_done(sw);
+ if (ret)
+ return ret;
+
+ ctrl = sw_ior(sw, TSN_CAM_CTRL_OFFSET);
+ if (!(ctrl & TSN_CAM_FOUND)) {
+ *portlist = 0;
+ return 0;
+ }
+
+ *portlist = FIELD_GET(TSN_CAM_PORT_LIST,
+ sw_ior(sw, TSN_CAM_PORT_ACT_OFFSET));
+ return 0;
+}
+
+/* Add (add=true) or delete (add=false) the (MAC, VID) entry carrying the
+ * given port list. Caller holds indirect_lock.
+ */
+static int xlnx_tsn_cam_write(struct xlnx_tsn *sw, const unsigned char *addr,
+ u16 vid, u8 portlist, bool add)
+{
+ int ret;
+
+ ret = xlnx_tsn_cam_wait_ready(sw);
+ if (ret)
+ return ret;
+
+ xlnx_tsn_cam_load_key(sw, addr, vid);
+ sw_iow(sw, TSN_CAM_TV1_OFFSET, 0);
+ sw_iow(sw, TSN_CAM_TV2_OFFSET, 0);
+ sw_iow(sw, TSN_CAM_PORT_ACT_OFFSET,
+ FIELD_PREP(TSN_CAM_PORT_LIST, portlist));
+ sw_iow(sw, TSN_CAM_CTRL_OFFSET,
+ FIELD_PREP(TSN_CAM_OP_MASK, add ? TSN_CAM_OP_ADD : TSN_CAM_OP_DELETE) |
+ TSN_CAM_OP_ENABLE);
+
+ return xlnx_tsn_cam_wait_done(sw);
+}
+
+static void xlnx_tsn_port_fast_age(struct dsa_switch *ds, int port)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ int err;
+
+ err = xlnx_tsn_port_state_cycle(sw, port, TSN_PORT_STATE_FLUSH);
+ if (err)
+ dev_err(sw->dev, "port %d: fast age failed (%d)\n", port, err);
+}
+
+static int xlnx_tsn_port_fdb_add(struct dsa_switch *ds, int port,
+ const unsigned char *addr, u16 vid,
+ struct dsa_db db)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ u8 portlist;
+ int ret;
+
+ if (!vid)
+ vid = TSN_SW_DEFAULT_VID;
+
+ guard(mutex)(&sw->indirect_lock);
+ ret = xlnx_tsn_cam_read_portlist(sw, addr, vid, &portlist);
+ if (!ret) {
+ portlist |= TSN_PORT_BIT(port);
+ ret = xlnx_tsn_cam_write(sw, addr, vid, portlist, true);
+ }
+
+ return ret;
+}
+
+static int xlnx_tsn_port_fdb_del(struct dsa_switch *ds, int port,
+ const unsigned char *addr, u16 vid,
+ struct dsa_db db)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ u8 portlist;
+ int ret;
+
+ if (!vid)
+ vid = TSN_SW_DEFAULT_VID;
+
+ guard(mutex)(&sw->indirect_lock);
+ ret = xlnx_tsn_cam_read_portlist(sw, addr, vid, &portlist);
+ if (!ret) {
+ if (!portlist)
+ return 0;
+
+ /* Drop the entry once no port references it. Otherwise
+ * rewrite it with the updated port list.
+ */
+ portlist &= ~TSN_PORT_BIT(port);
+ ret = xlnx_tsn_cam_write(sw, addr, vid, portlist, portlist != 0);
+ }
+
+ return ret;
+}
+
+static int xlnx_tsn_port_fdb_dump(struct dsa_switch *ds, int port,
+ dsa_fdb_dump_cb_t *cb, void *data)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ unsigned char addr[ETH_ALEN];
+ u32 base, ctrl, key1, key2;
+ int ret = 0;
+ u16 vid;
+ u32 i;
+
+ /* Learnt entries live in a per-MAC-port read-key region. The CPU
+ * port has no such region.
+ */
+ if (port == XLNX_TSN_CPU_PORT)
+ return 0;
+
+ guard(mutex)(&sw->indirect_lock);
+
+ if (port == XLNX_TSN_PORT_MAC2)
+ base = TSN_CAM_MAC2_READ_KEY_BASE;
+ else
+ base = 0;
+
+ /* Learnt entries occupy non-consecutive slots, so scan the whole
+ * region and report each slot marked found.
+ */
+ for (i = 0; i < TSN_CAM_READ_KEY_COUNT; i++) {
+ ret = xlnx_tsn_cam_wait_ready(sw);
+ if (ret)
+ return ret;
+
+ sw_iow(sw, TSN_CAM_CTRL_OFFSET,
+ FIELD_PREP(TSN_CAM_READ_KEY_ADDR, base + i) |
+ FIELD_PREP(TSN_CAM_OP_MASK, TSN_CAM_OP_READ_KEY) |
+ TSN_CAM_OP_ENABLE);
+
+ ret = xlnx_tsn_cam_wait_done(sw);
+ if (ret)
+ return ret;
+
+ ctrl = sw_ior(sw, TSN_CAM_CTRL_OFFSET);
+ if (!(ctrl & TSN_CAM_FOUND))
+ continue;
+
+ key1 = sw_ior(sw, TSN_CAM_KEY1_OFFSET);
+ key2 = sw_ior(sw, TSN_CAM_KEY2_OFFSET);
+ addr[0] = key1 >> 24;
+ addr[1] = key1 >> 16;
+ addr[2] = key1 >> 8;
+ addr[3] = key1;
+ addr[4] = key2 >> 8;
+ addr[5] = key2;
+ vid = FIELD_GET(TSN_CAM_VLAN, key2);
+ if (vid == TSN_SW_DEFAULT_VID)
+ vid = 0;
+
+ ret = cb(addr, vid, false, data);
+ if (ret)
+ return ret;
+ }
+ return ret;
+}
+
static void xlnx_tsn_phylink_get_caps(struct dsa_switch *ds, int port,
struct phylink_config *config)
{
@@ -643,7 +918,7 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
struct dsa_port *cpu_dp = dsa_to_port(ds, XLNX_TSN_CPU_PORT);
struct xlnx_tsn *sw = ds->priv;
struct dsa_port *dp;
- u32 mgmt;
+ u32 mgmt, reg;
int ret;
if (!dsa_is_user_port(ds, XLNX_TSN_PORT_MAC1) ||
@@ -656,6 +931,22 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
sw->conduit = cpu_dp->conduit;
+ /* Pre-arm the learning sub-qualifiers for when a port joins a bridge:
+ * learn untagged frames under their ingress native VID, and allow
+ * learning on VIDs with no membership entry while the bridge is
+ * VLAN-unaware. Both bits are inert while global learning is disabled.
+ */
+ reg = sw_ior(sw, TSN_SW_ADDR_LEARN_OFFSET);
+ reg |= TSN_SW_ADDR_LEARN_DISABLE | TSN_SW_ADDR_LEARN_UNTAGGED_EN |
+ TSN_SW_ADDR_LEARN_NO_VLAN_EN;
+ sw_iow(sw, TSN_SW_ADDR_LEARN_OFFSET, reg);
+
+ /* On a CAM miss flood unknown tagged unicast frames to all ports. */
+ reg = sw_ior(sw, TSN_SW_CTRL_OFFSET);
+ reg &= ~TSN_SW_CTRL_UCAST_MISS_MASK;
+ reg |= FIELD_PREP(TSN_SW_CTRL_UCAST_MISS_MASK, TSN_SW_CTRL_UCAST_MISS_FLOOD);
+ sw_iow(sw, TSN_SW_CTRL_OFFSET, reg);
+
/* Route CPU-originated bridge-group control frames (STP, LLDP) to
* the single wire port whose MAC-nibble field matches the frame's
* source-MAC low nibble, instead of flooding to both.
@@ -743,7 +1034,12 @@ static const struct dsa_switch_ops xlnx_tsn_switch_ops = {
.teardown = xlnx_tsn_teardown,
.port_set_mac_address = xlnx_tsn_port_set_mac_address,
.port_bridge_join = xlnx_tsn_port_bridge_join,
+ .port_bridge_leave = xlnx_tsn_port_bridge_leave,
.port_stp_state_set = xlnx_tsn_port_stp_state_set,
+ .port_fdb_add = xlnx_tsn_port_fdb_add,
+ .port_fdb_del = xlnx_tsn_port_fdb_del,
+ .port_fdb_dump = xlnx_tsn_port_fdb_dump,
+ .port_fast_age = xlnx_tsn_port_fast_age,
.port_hwtstamp_get = xlnx_tsn_port_hwtstamp_get,
.port_hwtstamp_set = xlnx_tsn_port_hwtstamp_set,
.get_ts_info = xlnx_tsn_get_ts_info,
@@ -778,6 +1074,9 @@ static int xlnx_tsn_probe(struct platform_device *pdev)
sw->dev = dev;
sw->mac[XLNX_TSN_PORT_MAC1].sw = sw;
sw->mac[XLNX_TSN_PORT_MAC2].sw = sw;
+ ret = devm_mutex_init(dev, &sw->indirect_lock);
+ if (ret)
+ return ret;
ret = xlnx_tsn_map_reg(pdev, "switch", &sw->sw_base);
if (ret)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index d46150535775..a94f5124a33d 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -63,18 +63,104 @@
#define TSN_SW_MGMT_QUEUING_OFFSET 0x00054
#define TSN_SW_MGMT_QUEUING_EP_SA_EGRESS BIT(4)
-/* readl_poll_timeout() parameters (in microseconds): poll until
- * the port-state change-commit bit self-clears.
+/* Shared readl_poll_timeout() parameters (in microseconds) used at
+ * two call sites: the port-state change-commit bit and the CAM
+ * operation enable bit. Both self-clear when the operation completes.
*/
#define TSN_SW_POLL_DELAY_US 10
#define TSN_SW_POLL_TIMEOUT_US 5000
+/* Stream-destination-lookup CAM: maps a (destination MAC, VLAN ID) key
+ * to an egress port list, backing the bridge FDB. Access is indirect:
+ * load the key, value, and port-list registers, write the opcode, then
+ * set the Enable Operation bit in the control register. Poll CAM Init
+ * Done in the status register to 1 before each operation to confirm the
+ * block is ready. Enable Operation self-clears when the operation
+ * finishes.
+ */
+#define TSN_CAM_CTRL_OFFSET 0x01000
+#define TSN_CAM_STATUS_OFFSET 0x01004
+#define TSN_CAM_KEY1_OFFSET 0x01008
+#define TSN_CAM_KEY2_OFFSET 0x0100c
+#define TSN_CAM_TV1_OFFSET 0x01010
+#define TSN_CAM_TV2_OFFSET 0x01014
+#define TSN_CAM_PORT_ACT_OFFSET 0x01018
+
+/* Control register: set to start an operation, self-clears when done. */
+#define TSN_CAM_OP_ENABLE BIT(0)
+/* Status register: reads 1 when the CAM is ready for the next operation. */
+#define TSN_CAM_STATUS_READY BIT(0)
+#define TSN_CAM_OP_MASK GENMASK(2, 1)
+#define TSN_CAM_OP_READ_KEY 0x0
+#define TSN_CAM_OP_ADD 0x1
+#define TSN_CAM_OP_DELETE 0x2
+#define TSN_CAM_OP_READ 0x3
+#define TSN_CAM_FOUND BIT(7)
+#define TSN_CAM_VLAN GENMASK(27, 16)
+#define TSN_CAM_PORT_LIST GENMASK(10, 8)
+#define TSN_CAM_READ_KEY_ADDR GENMASK(19, 8)
+#define TSN_CAM_MAC2_READ_KEY_BASE 0x800
+#define TSN_CAM_READ_KEY_COUNT 2048
+
+/* HW reset-default native VID; DSA's "no VLAN" (vid 0) maps onto it. */
+#define TSN_SW_DEFAULT_VID 1
+
+/* Switch Control Register: switch-wide control fields. */
+#define TSN_SW_CTRL_OFFSET 0x00004
+/* Per-port native-VLAN untag enables: strip the tag on egress when the
+ * frame VID equals the egress port's native VID. Only the wire ports are
+ * untagged; the endpoint port is left tagged so the host keeps the VID for
+ * software classification.
+ */
+#define TSN_SW_CTRL_MAC1_NATIVE_UNTAG_EN BIT(18)
+#define TSN_SW_CTRL_MAC2_NATIVE_UNTAG_EN BIT(19)
+/* Action for an ingress frame whose port is not in its VLAN's member
+ * list: forward to the processor or discard. Pinned to discard. This only
+ * matters once a VID's Port-List-Valid bit is set, which happens only
+ * while VLAN filtering is on.
+ */
+#define TSN_SW_CTRL_MEMBER_VIOL_MASK GENMASK(9, 8)
+#define TSN_SW_CTRL_MEMBER_VIOL_DISCARD 0x1
+
+/* Forwarding action for a tagged unicast or multicast frame that misses
+ * both the CAM and the VLAN membership list: flood, to processor, to MAC,
+ * or discard.
+ */
+#define TSN_SW_CTRL_UCAST_MISS_MASK GENMASK(1, 0)
+#define TSN_SW_CTRL_MCAST_MISS_MASK GENMASK(3, 2)
+#define TSN_SW_CTRL_UCAST_MISS_FLOOD 0x1
+#define TSN_SW_CTRL_MCAST_MISS_FLOOD 0x1
+#define TSN_SW_CTRL_MISS_DISCARD 0x3
+
+/* Hardware Address Learning Control: switch-wide learning behaviour. */
+#define TSN_SW_ADDR_LEARN_OFFSET 0x00048
+/* Disable the global learning engine; set to 1 while every user port is
+ * standalone, cleared to 0 when at least one port joins a bridge. The
+ * sub-qualifier bits below are inert while this bit is set.
+ */
+#define TSN_SW_ADDR_LEARN_DISABLE BIT(0)
+/* Learn the source MAC of untagged and priority-tagged frames against the
+ * ingress port's native VID; the reset default excludes them from learning.
+ */
+#define TSN_SW_ADDR_LEARN_UNTAGGED_EN BIT(1)
+/* Permit learning on a VID that has no member entry programmed in the VLAN
+ * membership memory; the reset default only learns already-configured VIDs.
+ */
+#define TSN_SW_ADDR_LEARN_NO_VLAN_EN BIT(3)
+
+/* Map a DSA port index to its bit in a switch port list. */
+#define TSN_PORT_BIT(port) BIT(port)
+
enum tsn_port_state {
TSN_PORT_STATE_DISABLED = 0,
TSN_PORT_STATE_BLOCKING,
TSN_PORT_STATE_LISTENING,
TSN_PORT_STATE_LEARNING,
TSN_PORT_STATE_FORWARDING,
+ /* Not an STP state. Writing it flushes the port's dynamic learnt
+ * entries and leaves static FDB entries in place.
+ */
+ TSN_PORT_STATE_FLUSH,
};
/* Per-MAC MDIO controller register window, sitting at +0x500 inside
@@ -218,6 +304,8 @@ struct xlnx_tsn_mac {
* @nb: netdev notifier that handles NETDEV_REGISTER on each swpN
* to set its final MAC, and NETDEV_CHANGEADDR on the conduit
* to refresh the shared prefix
+ * @indirect_lock: serialises the CAM and VLAN-membership indirect
+ * register sequences
* @mac: per-MAC state, indexed by user-port number (index 0 unused;
* MAC1 at [1], MAC2 at [2])
* @ptp_timer_irq: 1 PPS / RTC-overflow interrupt
@@ -239,6 +327,7 @@ struct xlnx_tsn {
struct net_device *conduit;
u8 mac_prefix[ETH_ALEN];
struct notifier_block nb;
+ struct mutex indirect_lock; /* serialises CAM indirect access */
struct xlnx_tsn_mac mac[XLNX_TSN_NUM_PORTS];
int ptp_timer_irq;
struct ptp_clock *ptp_clock;
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 19/20] net: dsa: xilinx: offload bridge VLAN filtering to the switch
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (17 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 18/20] net: dsa: xilinx: offload the bridge FDB to the switch CAM Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 20/20] net: dsa: xilinx: trap link-local control frames to the CPU port Nagadheeraj Rottela
2026-08-07 22:28 ` [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Jakub Kicinski
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
The switch always keys the L2 forwarding lookup on {DMAC, VID}. A
per-VID port-membership table controls which ports each VLAN spans.
The ingress parser assigns untagged frames the native VID of the
receiving port before lookup, so tagged and untagged traffic share
the same {DMAC, VID} key throughout.
Wire bridge VLAN filtering to the port-membership table and
native-VID registers so bridge VLAN commands take effect in
hardware. Program the native-VID register on PVID changes so
untagged ingress lands in the right VLAN.
Filtering is switch-wide, so declare it global. The per-MAC
VLAN-enable bit adjusts the MAC's maximum frame length to
account for the 4-byte tag.
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
drivers/net/dsa/xilinx/xilinx_tsn.c | 329 +++++++++++++++++++++++++++-
drivers/net/dsa/xilinx/xilinx_tsn.h | 50 ++++-
2 files changed, 367 insertions(+), 12 deletions(-)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 57558b0e2613..7e5cf77cfa66 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -20,6 +20,7 @@
#include <linux/platform_device.h>
#include <linux/dsa/xlnx_tsn.h>
#include <net/dsa.h>
+#include <net/switchdev.h>
#include "xilinx_tsn.h"
@@ -804,7 +805,7 @@ static int xlnx_tsn_port_fdb_dump(struct dsa_switch *ds, int port,
addr[4] = key2 >> 8;
addr[5] = key2;
vid = FIELD_GET(TSN_CAM_VLAN, key2);
- if (vid == TSN_SW_DEFAULT_VID)
+ if (!sw->vlan_aware && vid == TSN_SW_DEFAULT_VID)
vid = 0;
ret = cb(addr, vid, false, data);
@@ -814,6 +815,297 @@ static int xlnx_tsn_port_fdb_dump(struct dsa_switch *ds, int port,
return ret;
}
+/* The VLAN-membership control enable bit self-clears once the indirect
+ * read or write completes.
+ */
+static int xlnx_tsn_vlan_wait_done(struct xlnx_tsn *sw)
+{
+ u32 reg;
+
+ return readl_poll_timeout(sw->sw_base + TSN_VLAN_CTRL_OFFSET, reg,
+ !(reg & TSN_VLAN_EN), TSN_SW_POLL_DELAY_US,
+ TSN_SW_POLL_TIMEOUT_US);
+}
+
+/* Caller holds indirect_lock. */
+static int xlnx_tsn_vlan_read(struct xlnx_tsn *sw, u16 vid, u32 *data)
+{
+ int ret;
+
+ sw_iow(sw, TSN_VLAN_CTRL_OFFSET,
+ FIELD_PREP(TSN_VLAN_CTRL_VID_MASK, vid) | TSN_VLAN_ACCESS_READ | TSN_VLAN_EN);
+ ret = xlnx_tsn_vlan_wait_done(sw);
+ if (ret)
+ return ret;
+
+ *data = sw_ior(sw, TSN_VLAN_DATA_OFFSET);
+ return 0;
+}
+
+/* Caller holds indirect_lock. */
+static int xlnx_tsn_vlan_write(struct xlnx_tsn *sw, u16 vid, u32 data)
+{
+ sw_iow(sw, TSN_VLAN_DATA_OFFSET, data);
+ sw_iow(sw, TSN_VLAN_CTRL_OFFSET,
+ FIELD_PREP(TSN_VLAN_CTRL_VID_MASK, vid) | TSN_VLAN_ACCESS_WRITE | TSN_VLAN_EN);
+
+ return xlnx_tsn_vlan_wait_done(sw);
+}
+
+static void xlnx_tsn_set_native_vid(struct xlnx_tsn *sw, int port, u16 vid)
+{
+ u32 reg;
+
+ switch (port) {
+ case XLNX_TSN_CPU_PORT:
+ reg = sw_ior(sw, TSN_EP_NATIVE_VLAN_OFFSET);
+ reg = (reg & ~TSN_NATIVE_EP_VLAN_MASK) |
+ FIELD_PREP(TSN_NATIVE_EP_VLAN_MASK, vid);
+ sw_iow(sw, TSN_EP_NATIVE_VLAN_OFFSET, reg);
+ break;
+ case XLNX_TSN_PORT_MAC1:
+ reg = sw_ior(sw, TSN_MAC_NATIVE_VLAN_OFFSET);
+ reg = (reg & ~TSN_NATIVE_MAC1_VLAN_MASK) |
+ FIELD_PREP(TSN_NATIVE_MAC1_VLAN_MASK, vid);
+ sw_iow(sw, TSN_MAC_NATIVE_VLAN_OFFSET, reg);
+ break;
+ case XLNX_TSN_PORT_MAC2:
+ reg = sw_ior(sw, TSN_MAC_NATIVE_VLAN_OFFSET);
+ reg = (reg & ~TSN_NATIVE_MAC2_VLAN_MASK) |
+ FIELD_PREP(TSN_NATIVE_MAC2_VLAN_MASK, vid);
+ sw_iow(sw, TSN_MAC_NATIVE_VLAN_OFFSET, reg);
+ break;
+ }
+}
+
+/* Enable or disable native-VLAN egress untagging for a wire port.
+ * The endpoint port always sends frames tagged toward the host, so
+ * it is not handled here.
+ */
+static void xlnx_tsn_set_native_untag(struct xlnx_tsn *sw, int port, bool en)
+{
+ u32 bit, reg;
+
+ switch (port) {
+ case XLNX_TSN_PORT_MAC1:
+ bit = TSN_SW_CTRL_MAC1_NATIVE_UNTAG_EN;
+ break;
+ case XLNX_TSN_PORT_MAC2:
+ bit = TSN_SW_CTRL_MAC2_NATIVE_UNTAG_EN;
+ break;
+ default:
+ return;
+ }
+
+ reg = sw_ior(sw, TSN_SW_CTRL_OFFSET);
+ if (en)
+ reg |= bit;
+ else
+ reg &= ~bit;
+ sw_iow(sw, TSN_SW_CTRL_OFFSET, reg);
+}
+
+/* Write a port's native VID and egress-untag setting to hardware.
+ * While VLAN filtering is off, every port uses the reset-default
+ * native VID and stays tagged, so committed VLANs have no effect
+ * until the bridge goes VLAN-aware. Caller holds indirect_lock.
+ */
+static void xlnx_tsn_apply_pvid(struct xlnx_tsn *sw, int port)
+{
+ u16 vid = sw->vlan_aware ? sw->pvid[port] : TSN_SW_DEFAULT_VID;
+ bool untag = sw->vlan_aware && sw->pvid_untagged[port];
+
+ xlnx_tsn_set_native_vid(sw, port, vid);
+ xlnx_tsn_set_native_untag(sw, port, untag);
+}
+
+/* Restrict learning to VIDs that already have a membership entry.
+ * When on, frames with an unregistered VID are not learned, preventing
+ * a spurious CAM entry that would bypass the membership check on a
+ * later hit. When off, learning is allowed on any VID, as required
+ * while the bridge is VLAN-unaware. Caller holds indirect_lock.
+ */
+static void xlnx_tsn_set_vlan_only_learning(struct xlnx_tsn *sw, bool on)
+{
+ u32 reg = sw_ior(sw, TSN_SW_ADDR_LEARN_OFFSET);
+
+ if (on)
+ reg &= ~TSN_SW_ADDR_LEARN_NO_VLAN_EN;
+ else
+ reg |= TSN_SW_ADDR_LEARN_NO_VLAN_EN;
+ sw_iow(sw, TSN_SW_ADDR_LEARN_OFFSET, reg);
+}
+
+static int xlnx_tsn_port_vlan_filtering(struct dsa_switch *ds, int port,
+ bool vlan_filtering,
+ struct netlink_ext_ack *extack)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ struct dsa_port *dp;
+ unsigned long bit;
+ u32 reg, data;
+ int ret;
+
+ guard(mutex)(&sw->indirect_lock);
+
+ /* Flip Port-List-Valid on every configured VID: set it to enforce
+ * membership, clear it so VLANs added while filtering was off stay
+ * inert.
+ */
+ for_each_set_bit(bit, sw->cfg_vids, VLAN_N_VID) {
+ ret = xlnx_tsn_vlan_read(sw, bit, &data);
+ if (ret)
+ return ret;
+
+ if (vlan_filtering)
+ data |= TSN_VLAN_PORT_LIST_VALID;
+ else
+ data &= ~TSN_VLAN_PORT_LIST_VALID;
+
+ ret = xlnx_tsn_vlan_write(sw, bit, data);
+ if (ret)
+ return ret;
+ }
+
+ sw->vlan_aware = vlan_filtering;
+
+ /* Miss policy for unicast and multicast. It only kicks in when the
+ * CAM misses and the VID has no membership entry either. So flood the
+ * unregistered ones while VLAN-unaware, and drop them once filtering
+ * is on.
+ */
+ reg = sw_ior(sw, TSN_SW_CTRL_OFFSET);
+ reg &= ~(TSN_SW_CTRL_UCAST_MISS_MASK | TSN_SW_CTRL_MCAST_MISS_MASK);
+ if (vlan_filtering) {
+ reg |= FIELD_PREP(TSN_SW_CTRL_UCAST_MISS_MASK, TSN_SW_CTRL_MISS_DISCARD) |
+ FIELD_PREP(TSN_SW_CTRL_MCAST_MISS_MASK, TSN_SW_CTRL_MISS_DISCARD);
+ } else {
+ reg |= FIELD_PREP(TSN_SW_CTRL_UCAST_MISS_MASK, TSN_SW_CTRL_UCAST_MISS_FLOOD) |
+ FIELD_PREP(TSN_SW_CTRL_MCAST_MISS_MASK, TSN_SW_CTRL_MCAST_MISS_FLOOD);
+ }
+ sw_iow(sw, TSN_SW_CTRL_OFFSET, reg);
+
+ xlnx_tsn_set_vlan_only_learning(sw, vlan_filtering);
+
+ /* Apply the native VID and egress-untag settings saved while
+ * filtering was off, or restore defaults when filtering is turned
+ * off. VLANs added while filtering is already on are applied
+ * directly by port_vlan_add().
+ */
+ dsa_switch_for_each_user_port(dp, ds)
+ xlnx_tsn_apply_pvid(sw, dp->index);
+
+ /* The learning-control bits latch only when a port passes through
+ * blocking, so cycle each port to make the new setting take hold.
+ * The transition does not drop learnt entries, so flush them too -
+ * they were keyed on the old native VID and are now stale.
+ */
+ dsa_switch_for_each_user_port(dp, ds) {
+ ret = xlnx_tsn_port_state_cycle(sw, dp->index,
+ TSN_PORT_STATE_BLOCKING);
+ if (ret)
+ return ret;
+
+ ret = xlnx_tsn_port_state_cycle(sw, dp->index,
+ TSN_PORT_STATE_FLUSH);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int xlnx_tsn_port_vlan_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack)
+{
+ bool untagged = vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED;
+ bool pvid = vlan->flags & BRIDGE_VLAN_INFO_PVID;
+ struct xlnx_tsn *sw = ds->priv;
+ u32 data;
+ int ret;
+
+ guard(mutex)(&sw->indirect_lock);
+
+ /* The hardware strips the tag on egress only for a wire port's native
+ * VLAN. Reject an untagged request for any other VID. The CPU port is
+ * exempt as it always trunks tagged toward the host.
+ */
+ if (port != XLNX_TSN_CPU_PORT && untagged && !pvid &&
+ vlan->vid != sw->pvid[port]) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "untagged egress is only supported for the port PVID");
+ return -EINVAL;
+ }
+
+ ret = xlnx_tsn_vlan_read(sw, vlan->vid, &data);
+ if (ret)
+ return ret;
+
+ data |= TSN_PORT_BIT(port);
+ if (sw->vlan_aware)
+ data |= TSN_VLAN_PORT_LIST_VALID;
+ else
+ data &= ~TSN_VLAN_PORT_LIST_VALID;
+ ret = xlnx_tsn_vlan_write(sw, vlan->vid, data);
+ if (ret)
+ return ret;
+
+ set_bit(vlan->vid, sw->cfg_vids);
+
+ /* The CPU port needs membership only. */
+ if (port == XLNX_TSN_CPU_PORT)
+ return 0;
+
+ if (pvid) {
+ sw->pvid[port] = vlan->vid;
+ sw->pvid_untagged[port] = untagged;
+ xlnx_tsn_apply_pvid(sw, port);
+ }
+
+ if (vlan->vid == sw->pvid[port] &&
+ sw->pvid_untagged[port] != untagged) {
+ sw->pvid_untagged[port] = untagged;
+ xlnx_tsn_apply_pvid(sw, port);
+ }
+
+ return 0;
+}
+
+static int xlnx_tsn_port_vlan_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan)
+{
+ struct xlnx_tsn *sw = ds->priv;
+ bool last_member;
+ u32 data;
+ int ret;
+
+ guard(mutex)(&sw->indirect_lock);
+ ret = xlnx_tsn_vlan_read(sw, vlan->vid, &data);
+ if (ret)
+ return ret;
+
+ data &= ~TSN_PORT_BIT(port);
+ last_member = !(data & TSN_VLAN_PORT_LIST_MASK);
+ if (last_member)
+ data &= ~TSN_VLAN_PORT_LIST_VALID;
+
+ ret = xlnx_tsn_vlan_write(sw, vlan->vid, data);
+ if (ret)
+ return ret;
+
+ if (last_member)
+ clear_bit(vlan->vid, sw->cfg_vids);
+
+ if (sw->pvid[port] == vlan->vid) {
+ sw->pvid[port] = TSN_SW_DEFAULT_VID;
+ sw->pvid_untagged[port] = false;
+ xlnx_tsn_apply_pvid(sw, port);
+ }
+
+ return 0;
+}
+
static void xlnx_tsn_phylink_get_caps(struct dsa_switch *ds, int port,
struct phylink_config *config)
{
@@ -900,10 +1192,9 @@ static void xlnx_tsn_mac_link_up(struct phylink_config *config,
}
mac_iow(m, TSN_SPEED_CFG_OFFSET, speed_cfg);
- rcw1 = mac_ior(m, TSN_RCW1_OFFSET) | TSN_RCW1_RX_EN;
+ rcw1 = mac_ior(m, TSN_RCW1_OFFSET) | TSN_RCW1_RX_EN | TSN_RCW1_VLAN_EN;
+ tc = mac_ior(m, TSN_TC_OFFSET) | TSN_TC_TX_EN | TSN_TC_VLAN_EN;
mac_iow(m, TSN_RCW1_OFFSET, rcw1);
-
- tc = mac_ior(m, TSN_TC_OFFSET) | TSN_TC_TX_EN;
mac_iow(m, TSN_TC_OFFSET, tc);
}
@@ -919,7 +1210,7 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
struct xlnx_tsn *sw = ds->priv;
struct dsa_port *dp;
u32 mgmt, reg;
- int ret;
+ int port, ret;
if (!dsa_is_user_port(ds, XLNX_TSN_PORT_MAC1) ||
!dsa_is_user_port(ds, XLNX_TSN_PORT_MAC2))
@@ -941,12 +1232,26 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
TSN_SW_ADDR_LEARN_NO_VLAN_EN;
sw_iow(sw, TSN_SW_ADDR_LEARN_OFFSET, reg);
- /* On a CAM miss flood unknown tagged unicast frames to all ports. */
+ /* On a CAM miss flood unknown tagged unicast frames to all ports.
+ * Also drop tagged frames whose ingress port is not in the VLAN
+ * member list. The membership check only bites once VLANs exist.
+ */
reg = sw_ior(sw, TSN_SW_CTRL_OFFSET);
- reg &= ~TSN_SW_CTRL_UCAST_MISS_MASK;
- reg |= FIELD_PREP(TSN_SW_CTRL_UCAST_MISS_MASK, TSN_SW_CTRL_UCAST_MISS_FLOOD);
+ reg &= ~(TSN_SW_CTRL_UCAST_MISS_MASK | TSN_SW_CTRL_MEMBER_VIOL_MASK);
+ reg |= FIELD_PREP(TSN_SW_CTRL_UCAST_MISS_MASK, TSN_SW_CTRL_UCAST_MISS_FLOOD) |
+ FIELD_PREP(TSN_SW_CTRL_MEMBER_VIOL_MASK, TSN_SW_CTRL_MEMBER_VIOL_DISCARD);
sw_iow(sw, TSN_SW_CTRL_OFFSET, reg);
+ /* Every port uses the default native VID until VLAN filtering is
+ * enabled. Seed the PVID shadow and program the native VID
+ * registers to match.
+ */
+ scoped_guard(mutex, &sw->indirect_lock)
+ for (port = XLNX_TSN_CPU_PORT; port < XLNX_TSN_NUM_PORTS; port++) {
+ sw->pvid[port] = TSN_SW_DEFAULT_VID;
+ xlnx_tsn_apply_pvid(sw, port);
+ }
+
/* Route CPU-originated bridge-group control frames (STP, LLDP) to
* the single wire port whose MAC-nibble field matches the frame's
* source-MAC low nibble, instead of flooding to both.
@@ -1040,6 +1345,9 @@ static const struct dsa_switch_ops xlnx_tsn_switch_ops = {
.port_fdb_del = xlnx_tsn_port_fdb_del,
.port_fdb_dump = xlnx_tsn_port_fdb_dump,
.port_fast_age = xlnx_tsn_port_fast_age,
+ .port_vlan_filtering = xlnx_tsn_port_vlan_filtering,
+ .port_vlan_add = xlnx_tsn_port_vlan_add,
+ .port_vlan_del = xlnx_tsn_port_vlan_del,
.port_hwtstamp_get = xlnx_tsn_port_hwtstamp_get,
.port_hwtstamp_set = xlnx_tsn_port_hwtstamp_set,
.get_ts_info = xlnx_tsn_get_ts_info,
@@ -1102,6 +1410,11 @@ static int xlnx_tsn_probe(struct platform_device *pdev)
/* The fabric offloads a single bridge across the user ports. */
ds->max_num_bridges = 1;
+ /* The switch fabric is a single VLAN domain shared by all ports, so
+ * VLAN filtering is configured switch-wide rather than per port.
+ */
+ ds->vlan_filtering_is_global = true;
+
platform_set_drvdata(pdev, sw);
return dsa_register_switch(ds);
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index a94f5124a33d..955041cde564 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -8,6 +8,7 @@
#include <linux/bitfield.h>
#include <linux/bits.h>
#include <linux/if_ether.h>
+#include <linux/if_vlan.h>
#include <linux/io.h>
#include <linux/net_tstamp.h>
#include <linux/notifier.h>
@@ -64,8 +65,9 @@
#define TSN_SW_MGMT_QUEUING_EP_SA_EGRESS BIT(4)
/* Shared readl_poll_timeout() parameters (in microseconds) used at
- * two call sites: the port-state change-commit bit and the CAM
- * operation enable bit. Both self-clear when the operation completes.
+ * three call sites: the port-state change-commit bit, the CAM
+ * operation enable bit, and the VLAN-memory operation enable bit.
+ * All three self-clear when the hardware completes the operation.
*/
#define TSN_SW_POLL_DELAY_US 10
#define TSN_SW_POLL_TIMEOUT_US 5000
@@ -102,6 +104,28 @@
#define TSN_CAM_MAC2_READ_KEY_BASE 0x800
#define TSN_CAM_READ_KEY_COUNT 2048
+/* Port VLAN membership memory: per-VID egress port list, accessed
+ * indirectly through a control/data register pair. The control
+ * register's enable bit self-clears when the read or write completes.
+ */
+#define TSN_VLAN_CTRL_OFFSET 0x01100
+#define TSN_VLAN_DATA_OFFSET 0x01104
+#define TSN_VLAN_CTRL_VID_MASK GENMASK(27, 16)
+#define TSN_VLAN_ACCESS_READ 0
+#define TSN_VLAN_ACCESS_WRITE BIT(1)
+#define TSN_VLAN_EN BIT(0)
+#define TSN_VLAN_PORT_LIST_VALID BIT(3)
+#define TSN_VLAN_PORT_LIST_MASK GENMASK(2, 0)
+
+/* Per-port native (PVID) registers: the endpoint port at +0x40, the two
+ * MAC ports packed into +0x44 (MAC1 in the low half, MAC2 in the high).
+ */
+#define TSN_EP_NATIVE_VLAN_OFFSET 0x00040
+#define TSN_MAC_NATIVE_VLAN_OFFSET 0x00044
+#define TSN_NATIVE_EP_VLAN_MASK GENMASK(11, 0)
+#define TSN_NATIVE_MAC1_VLAN_MASK GENMASK(11, 0)
+#define TSN_NATIVE_MAC2_VLAN_MASK GENMASK(27, 16)
+
/* HW reset-default native VID; DSA's "no VLAN" (vid 0) maps onto it. */
#define TSN_SW_DEFAULT_VID 1
@@ -183,12 +207,17 @@ enum tsn_port_state {
#define TSN_MDIO_MRD_MASK GENMASK(15, 0)
-/* Per-MAC receive / transmit / speed configuration registers. */
+/* Per-MAC receive / transmit / speed configuration registers. Setting
+ * the VLAN enable bit makes the MAC accept full-size VLAN-tagged frames
+ * without treating the extra 4 bytes as an oversize error.
+ */
#define TSN_RCW1_OFFSET 0x00000404
#define TSN_RCW1_RX_EN BIT(28)
+#define TSN_RCW1_VLAN_EN BIT(27)
#define TSN_TC_OFFSET 0x00000408
#define TSN_TC_TX_EN BIT(28)
+#define TSN_TC_VLAN_EN BIT(27)
#define TSN_SPEED_CFG_OFFSET 0x00000410
#define TSN_SPEED_CFG_MASK GENMASK(31, 30)
@@ -304,6 +333,15 @@ struct xlnx_tsn_mac {
* @nb: netdev notifier that handles NETDEV_REGISTER on each swpN
* to set its final MAC, and NETDEV_CHANGEADDR on the conduit
* to refresh the shared prefix
+ * @vlan_aware: true once bridge VLAN filtering is on. Until then the
+ * native VID, egress untag, and membership enforcement are
+ * all held back.
+ * @pvid: per-port bridge PVID shadow, written to the native VID register
+ * while @vlan_aware; defaults to TSN_SW_DEFAULT_VID
+ * @pvid_untagged: per-port flag tracking whether the PVID egresses
+ * untagged; drives the native-VLAN untag enable
+ * @cfg_vids: VIDs with a membership entry in the VLAN-membership memory,
+ * used to walk and update Port-List-Valid when @vlan_aware changes
* @indirect_lock: serialises the CAM and VLAN-membership indirect
* register sequences
* @mac: per-MAC state, indexed by user-port number (index 0 unused;
@@ -327,7 +365,11 @@ struct xlnx_tsn {
struct net_device *conduit;
u8 mac_prefix[ETH_ALEN];
struct notifier_block nb;
- struct mutex indirect_lock; /* serialises CAM indirect access */
+ bool vlan_aware;
+ u16 pvid[XLNX_TSN_NUM_PORTS];
+ bool pvid_untagged[XLNX_TSN_NUM_PORTS];
+ DECLARE_BITMAP(cfg_vids, VLAN_N_VID);
+ struct mutex indirect_lock; /* serialises CAM + VLAN-memory access */
struct xlnx_tsn_mac mac[XLNX_TSN_NUM_PORTS];
int ptp_timer_irq;
struct ptp_clock *ptp_clock;
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH 20/20] net: dsa: xilinx: trap link-local control frames to the CPU port
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (18 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 19/20] net: dsa: xilinx: offload bridge VLAN filtering to the switch Nagadheeraj Rottela
@ 2026-08-07 10:44 ` Nagadheeraj Rottela
2026-08-07 22:28 ` [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Jakub Kicinski
20 siblings, 0 replies; 24+ messages in thread
From: Nagadheeraj Rottela @ 2026-08-07 10:44 UTC (permalink / raw)
To: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Richard Cochran, Michal Simek, Andrew Lunn,
Vladimir Oltean, Simon Horman, Russell King
Cc: netdev, devicetree, linux-kernel, linux-arm-kernel, git-dev,
nagadheeraj.rottela
The TSN switch fabric forwards frames by CAM lookup. A frame whose
destination MAC misses the CAM is treated as unknown and flooded to all
ports except the ingress port. Link-local control frames (STP BPDUs,
LLDPDUs) use destination MACs in the IEEE 802.1 bridge-group address
range 01:80:c2:00:00:00..0f, which a bridge must consume locally and
never relay between ports.
Install CAM entries for the in-use bridge-group addresses (STP at
01:80:c2:00:00:00 and LLDP at 01:80:c2:00:00:0e). Set the forwarding
port list to the CPU port only and set the endpoint management queuing
bit in the port-action register. The switch then delivers trapped
frames on the management queue.
The CAM matches on (DA, VID) exactly. Untagged control frames are
looked up under the ingress port's native VID, so a trap entry must
exist for every native VID in use. Keep entries in sync with each
wire port's current native VID.
The CAM has no DA-mask capability, so covering all 16 bridge-group
addresses would cost 16 entries per VID. Only the addresses in
active use are trapped.
Signed-off-by: Nagadheeraj Rottela <nagadheeraj.rottela@amd.com>
---
drivers/net/dsa/xilinx/xilinx_tsn.c | 176 ++++++++++++++++++++++++++--
drivers/net/dsa/xilinx/xilinx_tsn.h | 9 ++
2 files changed, 176 insertions(+), 9 deletions(-)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 7e5cf77cfa66..e35dbcdfcbda 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -671,11 +671,14 @@ static int xlnx_tsn_cam_read_portlist(struct xlnx_tsn *sw,
}
/* Add (add=true) or delete (add=false) the (MAC, VID) entry carrying the
- * given port list. Caller holds indirect_lock.
+ * given port list. Set mgmt for entries whose frames must be classified as
+ * endpoint management traffic; clear it for plain forwarding/FDB entries.
+ * Caller holds indirect_lock.
*/
static int xlnx_tsn_cam_write(struct xlnx_tsn *sw, const unsigned char *addr,
- u16 vid, u8 portlist, bool add)
+ u16 vid, u8 portlist, bool mgmt, bool add)
{
+ u32 port_act;
int ret;
ret = xlnx_tsn_cam_wait_ready(sw);
@@ -685,8 +688,13 @@ static int xlnx_tsn_cam_write(struct xlnx_tsn *sw, const unsigned char *addr,
xlnx_tsn_cam_load_key(sw, addr, vid);
sw_iow(sw, TSN_CAM_TV1_OFFSET, 0);
sw_iow(sw, TSN_CAM_TV2_OFFSET, 0);
- sw_iow(sw, TSN_CAM_PORT_ACT_OFFSET,
- FIELD_PREP(TSN_CAM_PORT_LIST, portlist));
+
+ port_act = FIELD_PREP(TSN_CAM_PORT_LIST, portlist);
+ if (mgmt)
+ port_act |= TSN_CAM_EP_MGMTQ_EN;
+
+ sw_iow(sw, TSN_CAM_PORT_ACT_OFFSET, port_act);
+
sw_iow(sw, TSN_CAM_CTRL_OFFSET,
FIELD_PREP(TSN_CAM_OP_MASK, add ? TSN_CAM_OP_ADD : TSN_CAM_OP_DELETE) |
TSN_CAM_OP_ENABLE);
@@ -694,6 +702,28 @@ static int xlnx_tsn_cam_write(struct xlnx_tsn *sw, const unsigned char *addr,
return xlnx_tsn_cam_wait_done(sw);
}
+/* IEEE 802.1 bridge-group destination MACs. A bridge must consume
+ * these locally rather than relay them between ports. Each address
+ * gets a CAM trap entry pointing to the CPU port only. The CAM
+ * matches the destination MAC exactly, so only the addresses in
+ * active use are listed here.
+ */
+static const u8 xlnx_tsn_ctrl_das[][ETH_ALEN] = {
+ { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x00 }, /* STP / RSTP / MSTP */
+ { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e }, /* LLDP */
+};
+
+static bool xlnx_tsn_addr_is_ctrl_trap(const unsigned char *addr)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(xlnx_tsn_ctrl_das); i++)
+ if (ether_addr_equal(addr, xlnx_tsn_ctrl_das[i]))
+ return true;
+
+ return false;
+}
+
static void xlnx_tsn_port_fast_age(struct dsa_switch *ds, int port)
{
struct xlnx_tsn *sw = ds->priv;
@@ -712,6 +742,9 @@ static int xlnx_tsn_port_fdb_add(struct dsa_switch *ds, int port,
u8 portlist;
int ret;
+ if (xlnx_tsn_addr_is_ctrl_trap(addr))
+ return 0;
+
if (!vid)
vid = TSN_SW_DEFAULT_VID;
@@ -719,7 +752,7 @@ static int xlnx_tsn_port_fdb_add(struct dsa_switch *ds, int port,
ret = xlnx_tsn_cam_read_portlist(sw, addr, vid, &portlist);
if (!ret) {
portlist |= TSN_PORT_BIT(port);
- ret = xlnx_tsn_cam_write(sw, addr, vid, portlist, true);
+ ret = xlnx_tsn_cam_write(sw, addr, vid, portlist, false, true);
}
return ret;
@@ -733,6 +766,9 @@ static int xlnx_tsn_port_fdb_del(struct dsa_switch *ds, int port,
u8 portlist;
int ret;
+ if (xlnx_tsn_addr_is_ctrl_trap(addr))
+ return 0;
+
if (!vid)
vid = TSN_SW_DEFAULT_VID;
@@ -746,7 +782,8 @@ static int xlnx_tsn_port_fdb_del(struct dsa_switch *ds, int port,
* rewrite it with the updated port list.
*/
portlist &= ~TSN_PORT_BIT(port);
- ret = xlnx_tsn_cam_write(sw, addr, vid, portlist, portlist != 0);
+ ret = xlnx_tsn_cam_write(sw, addr, vid, portlist, false,
+ portlist != 0);
}
return ret;
@@ -936,12 +973,93 @@ static void xlnx_tsn_set_vlan_only_learning(struct xlnx_tsn *sw, bool on)
sw_iow(sw, TSN_SW_ADDR_LEARN_OFFSET, reg);
}
+/* Install or remove link-local control-frame traps for one VID.
+ * Each entry points to the CPU port and marks frames as management
+ * traffic for delivery on the management queue. Caller holds
+ * indirect_lock.
+ */
+static int xlnx_tsn_set_ctrl_traps(struct xlnx_tsn *sw, u16 vid, bool add)
+{
+ int i, ret;
+
+ for (i = 0; i < ARRAY_SIZE(xlnx_tsn_ctrl_das); i++) {
+ ret = xlnx_tsn_cam_write(sw, xlnx_tsn_ctrl_das[i], vid,
+ TSN_PORT_BIT(XLNX_TSN_CPU_PORT),
+ true, add);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static bool xlnx_tsn_vid_in(const u16 *vids, int count, u16 vid)
+{
+ int i;
+
+ for (i = 0; i < count; i++)
+ if (vids[i] == vid)
+ return true;
+
+ return false;
+}
+
+/* Keep CAM traps in sync with the native VIDs the wire ports use. An
+ * untagged BPDU or LLDP frame is looked up under the ingress port's
+ * native VID, so a trap must exist at each native VID in use. While
+ * the bridge is not VLAN-aware every port uses the default VID. Call
+ * again whenever a native VID changes. Caller holds indirect_lock.
+ */
+static int xlnx_tsn_sync_ctrl_traps(struct xlnx_tsn *sw)
+{
+ u16 want[XLNX_TSN_NUM_PORTS - 1], vid;
+ struct dsa_port *dp;
+ int i, n, ret;
+
+ n = 0;
+ dsa_switch_for_each_user_port(dp, &sw->ds) {
+ vid = sw->vlan_aware ? sw->pvid[dp->index] : TSN_SW_DEFAULT_VID;
+ if (!xlnx_tsn_vid_in(want, n, vid))
+ want[n++] = vid;
+ }
+
+ /* Remove traps at VIDs no longer used as any wire port's native VID. */
+ i = 0;
+ while (i < sw->ctrl_trap_count) {
+ vid = sw->ctrl_trap_vid[i];
+ if (xlnx_tsn_vid_in(want, n, vid)) {
+ i++;
+ continue;
+ }
+ ret = xlnx_tsn_set_ctrl_traps(sw, vid, false);
+ if (ret)
+ return ret;
+
+ sw->ctrl_trap_vid[i] = sw->ctrl_trap_vid[--sw->ctrl_trap_count];
+ }
+
+ /* Add traps at native VIDs not yet installed. */
+ for (i = 0; i < n; i++) {
+ if (xlnx_tsn_vid_in(sw->ctrl_trap_vid, sw->ctrl_trap_count,
+ want[i]))
+ continue;
+ ret = xlnx_tsn_set_ctrl_traps(sw, want[i], true);
+ if (ret)
+ return ret;
+
+ sw->ctrl_trap_vid[sw->ctrl_trap_count++] = want[i];
+ }
+
+ return 0;
+}
+
static int xlnx_tsn_port_vlan_filtering(struct dsa_switch *ds, int port,
bool vlan_filtering,
struct netlink_ext_ack *extack)
{
struct xlnx_tsn *sw = ds->priv;
struct dsa_port *dp;
+ bool old_vlan_aware;
unsigned long bit;
u32 reg, data;
int ret;
@@ -967,6 +1085,7 @@ static int xlnx_tsn_port_vlan_filtering(struct dsa_switch *ds, int port,
return ret;
}
+ old_vlan_aware = sw->vlan_aware;
sw->vlan_aware = vlan_filtering;
/* Miss policy for unicast and multicast. It only kicks in when the
@@ -1004,15 +1123,23 @@ static int xlnx_tsn_port_vlan_filtering(struct dsa_switch *ds, int port,
ret = xlnx_tsn_port_state_cycle(sw, dp->index,
TSN_PORT_STATE_BLOCKING);
if (ret)
- return ret;
+ goto restore;
ret = xlnx_tsn_port_state_cycle(sw, dp->index,
TSN_PORT_STATE_FLUSH);
if (ret)
- return ret;
+ goto restore;
}
+ ret = xlnx_tsn_sync_ctrl_traps(sw);
+ if (ret)
+ goto restore;
+
return 0;
+
+restore:
+ sw->vlan_aware = old_vlan_aware;
+ return ret;
}
static int xlnx_tsn_port_vlan_add(struct dsa_switch *ds, int port,
@@ -1061,6 +1188,7 @@ static int xlnx_tsn_port_vlan_add(struct dsa_switch *ds, int port,
sw->pvid[port] = vlan->vid;
sw->pvid_untagged[port] = untagged;
xlnx_tsn_apply_pvid(sw, port);
+ return xlnx_tsn_sync_ctrl_traps(sw);
}
if (vlan->vid == sw->pvid[port] &&
@@ -1101,6 +1229,7 @@ static int xlnx_tsn_port_vlan_del(struct dsa_switch *ds, int port,
sw->pvid[port] = TSN_SW_DEFAULT_VID;
sw->pvid_untagged[port] = false;
xlnx_tsn_apply_pvid(sw, port);
+ return xlnx_tsn_sync_ctrl_traps(sw);
}
return 0;
@@ -1204,6 +1333,21 @@ static const struct phylink_mac_ops xlnx_tsn_phylink_mac_ops = {
.mac_link_down = xlnx_tsn_mac_link_down,
};
+static void xlnx_tsn_remove_ctrl_traps(struct xlnx_tsn *sw)
+{
+ int i;
+
+ guard(mutex)(&sw->indirect_lock);
+
+ for (i = 0; i < sw->ctrl_trap_count; i++)
+ if (xlnx_tsn_set_ctrl_traps(sw, sw->ctrl_trap_vid[i], false))
+ dev_warn(sw->dev,
+ "failed to remove control trap vid %u\n",
+ sw->ctrl_trap_vid[i]);
+
+ sw->ctrl_trap_count = 0;
+}
+
static int xlnx_tsn_setup(struct dsa_switch *ds)
{
struct dsa_port *cpu_dp = dsa_to_port(ds, XLNX_TSN_CPU_PORT);
@@ -1276,9 +1420,19 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
return ret;
}
+ /* Trap link-local control frames (STP, LLDP) to the CPU port.
+ * Without this, a frame arriving on one wire port would be
+ * flooded out the other instead of reaching the host bridge.
+ */
+ scoped_guard(mutex, &sw->indirect_lock) {
+ ret = xlnx_tsn_sync_ctrl_traps(sw);
+ if (ret)
+ return ret;
+ }
+
ret = xlnx_tsn_mdio_register_all(sw);
if (ret)
- return ret;
+ goto err_traps;
sw->nb.notifier_call = xlnx_tsn_netdev_event;
ret = register_netdevice_notifier(&sw->nb);
@@ -1312,6 +1466,8 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
unregister_netdevice_notifier(&sw->nb);
err_mdio:
xlnx_tsn_mdio_unregister_all(sw);
+err_traps:
+ xlnx_tsn_remove_ctrl_traps(sw);
return ret;
}
@@ -1327,6 +1483,8 @@ static void xlnx_tsn_teardown(struct dsa_switch *ds)
unregister_netdevice_notifier(&sw->nb);
xlnx_tsn_mdio_unregister_all(sw);
+ xlnx_tsn_remove_ctrl_traps(sw);
+
dsa_switch_for_each_user_port(dp, ds)
xlnx_tsn_set_port_state(sw, dp->index, TSN_PORT_STATE_DISABLED);
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index 955041cde564..dbf330d3d615 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -100,6 +100,10 @@
#define TSN_CAM_FOUND BIT(7)
#define TSN_CAM_VLAN GENMASK(27, 16)
#define TSN_CAM_PORT_LIST GENMASK(10, 8)
+/* Classify a CAM-matched frame as endpoint management traffic so the
+ * switch delivers it on the management queue.
+ */
+#define TSN_CAM_EP_MGMTQ_EN BIT(15)
#define TSN_CAM_READ_KEY_ADDR GENMASK(19, 8)
#define TSN_CAM_MAC2_READ_KEY_BASE 0x800
#define TSN_CAM_READ_KEY_COUNT 2048
@@ -342,6 +346,9 @@ struct xlnx_tsn_mac {
* untagged; drives the native-VLAN untag enable
* @cfg_vids: VIDs with a membership entry in the VLAN-membership memory,
* used to walk and update Port-List-Valid when @vlan_aware changes
+ * @ctrl_trap_vid: native VIDs that currently carry link-local control-frame
+ * traps, kept in sync with the wire ports' native VIDs
+ * @ctrl_trap_count: number of valid entries in @ctrl_trap_vid
* @indirect_lock: serialises the CAM and VLAN-membership indirect
* register sequences
* @mac: per-MAC state, indexed by user-port number (index 0 unused;
@@ -369,6 +376,8 @@ struct xlnx_tsn {
u16 pvid[XLNX_TSN_NUM_PORTS];
bool pvid_untagged[XLNX_TSN_NUM_PORTS];
DECLARE_BITMAP(cfg_vids, VLAN_N_VID);
+ u16 ctrl_trap_vid[XLNX_TSN_NUM_PORTS - 1];
+ u8 ctrl_trap_count;
struct mutex indirect_lock; /* serialises CAM + VLAN-memory access */
struct xlnx_tsn_mac mac[XLNX_TSN_NUM_PORTS];
int ptp_timer_irq;
--
2.34.1
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support
2026-08-07 10:44 [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Nagadheeraj Rottela
` (19 preceding siblings ...)
2026-08-07 10:44 ` [PATCH 20/20] net: dsa: xilinx: trap link-local control frames to the CPU port Nagadheeraj Rottela
@ 2026-08-07 22:28 ` Jakub Kicinski
20 siblings, 0 replies; 24+ messages in thread
From: Jakub Kicinski @ 2026-08-07 22:28 UTC (permalink / raw)
To: Nagadheeraj Rottela
Cc: Srinivas Neeli, Andrew Lunn, David S . Miller, Eric Dumazet,
Paolo Abeni, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Richard Cochran, Michal Simek, Andrew Lunn, Vladimir Oltean,
Simon Horman, Russell King, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
On Fri, 7 Aug 2026 16:14:11 +0530 Nagadheeraj Rottela wrote:
> Nagadheeraj Rottela (13):
> net: xilinx: tsn: deliver endpoint RX frames to DSA user ports
> net: dsa: tag_xlnx_tsn: add skeleton tag protocol
> net: dsa: xilinx: add skeleton driver for TSN switch
> net: dsa: xilinx: implement port_stp_state_set
> net: dsa: xilinx: register per-MAC MDIO buses
> net: dsa: xilinx: wire up phylink for the switch ports
> net: dsa: xilinx: program MAC frame filter and per-port nibbles
> net: dsa: xilinx: register PHC backed by the RTC timer block
> net: dsa: xilinx: drive per-MAC PTP TX/RX hardware paths
> net: dsa: xilinx: opt into TX forwarding offload on bridge join
> net: dsa: xilinx: offload the bridge FDB to the switch CAM
> net: dsa: xilinx: offload bridge VLAN filtering to the switch
> net: dsa: xilinx: trap link-local control frames to the CPU port
>
> Srinivas Neeli (7):
> dt-bindings: net: add Xilinx TSN Endpoint Ethernet MAC
> net: xilinx: tsn: add TSN endpoint wrapper driver
> net: xilinx: tsn: add endpoint MAC driver skeleton
> net: xilinx: tsn: parse endpoint DMA channel configuration
> net: xilinx: tsn: bring up the endpoint MCDMA channels
> net: xilinx: tsn: add the endpoint RX data path
> net: xilinx: tsn: add the endpoint TX data path
This is so clearly two series squished into one.
We have a requirement to keep series under 15 patches.
The smaller the better, easier for people to review and merge.
A 20 patch series will take forever to be completely clean
Please read:
https://www.kernel.org/doc/html/latest/process/maintainer-netdev.html
^ permalink raw reply [flat|nested] 24+ messages in thread