* [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support
@ 2026-08-07 10:44 Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 01/20] dt-bindings: net: add Xilinx TSN Endpoint Ethernet MAC Nagadheeraj Rottela
` (20 more replies)
0 siblings, 21 replies; 45+ 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
Introduce a new network driver for the AMD LogiCORE 100M/1G TSN
Subsystem IP, also known as the TSN Endpoint Ethernet MAC IP, which
implements IEEE 802.1 Time-Sensitive Networking (TSN) features for
deterministic and low-latency Ethernet communication in real-time and
industrial automation use cases.
IP Core Overview:
The AMD LogiCORE 100M/1G TSN Subsystem IP solution (named as TSN Endpoint
Ethernet MAC IP in the IP catalog) implements IEEE 802.1 Time Sensitive
Networking (TSN) Standards and provides a low latency Bridged Endpoint or
Endpoint only solutions. The bridged endpoint solution consists of a 3-port
switch that connects to an endpoint including Linux software drivers. For
Bridged Endpoint (Switch Endpoint), two ports connects to the network and
one port connects to an internal Endpoint. It supports the use of
GMII/RGMII interfaces connecting to a physical-side interface (PHY) chip
with full duplex 100 Mb/s and 1 Gb/s operations.
Features:
- IP Customizable to generate TSN Endpoint or Bridged Endpoint solution
- Supports interface to network software stack
- Designed to comply with the following IEEE standards
- 802.1AS : Precision Time Protocol (PTP) synchronization
- 802.1Qav : Credit-Based Shaper for traffic shaping
- 802.1Qbv : Time-Aware Shaper for scheduled traffic
- 802.1Qbu : Frame Preemption for ultra-low latency
- 802.1CB : Frame Replication and Elimination for redundancy
- 802.1Qcc : Stream Reservation Protocol (SRP)
- IP is customizable to enable various TSN feature-sets
- Supports 2 to 8 priority queues for scheduled, reserved (IEEE 802.1 Qav
Credit Based Shaper) and best effort traffic classes
- Provides feature rich Ethernet Switch that caters to various network
needs
* 3-port Switch (2-external, 1-internal)
* Programmable cut-through and store-forward operations
* 4-port Switch (2-external, 2-internal) extension through
'Endpoint Extension' and 'Endpoint Packet Switching' features
- Solution validated on ZCU102 & ZC702 AMD Evaluation boards.
Sample hardware architecture diagram for Bidge End Point like below:
+------------------+
| MCDMA |
+---------+--------+
Q0---Q7
|
+------------------------------------------------------------ +
| | TSN sub system(Bridge End Point) |
| | |
| +------+----+ Port 0 +-----------------------+ |
| | EndPoint |<--------->| TSN Switch | |
| | (EP) | +----+-------------+----+ |
| +-----------+ | | |
| | | |
| Port 1 Port 2 |
| | | |
| +-----------+ +-----------+ |
| | MAC-1 | | MAC-2 | |
| | (ETH1) | | (ETH2) | |
| +-----+-----+ +-----+-----+ |
| | | |
| | | |
+-------------------------------------------------------------+
| |
RGMII RGMII
| |
+-----------+ +-----------+
| PHY1 | | PHY2 |
| (Port 0) | | (Port 2) |
+-----------+ +-----------+
Software Driver Overview:
The switch-facing part of the IP is exposed through the Distributed
Switch Architecture (DSA) framework, so MAC1 and MAC2 show up to Linux
as proper switch ports (swp1, swp2) that can be enslaved to a Linux
bridge, with the endpoint acting as the CPU/conduit port. The driver
is split into a thin wrapper, an endpoint driver, a DSA switch driver,
and a DSA tag protocol, mirroring the hardware blocks:
Wrapper driver:
- Binds the top-level IP node, acquires and enables the IP-block
clocks.
- Spawns the endpoint and switch blocks as child platform devices via
devm_of_platform_populate() and carries no netdev or register access
of its own.
Endpoint driver:
- Implements the conduit net_device that Linux DMA traffic flows
through on its way to and from the switch fabric.
- Manages multi-queue TX and RX descriptor rings using the dmaengine
framework for high-performance packet transfer.
- Classifies each received frame by the source-port id carried in the
RX descriptor and attaches DSA port metadata so the DSA core can
demux it to the right user port netdev.
DSA switch driver:
- Registers a struct dsa_switch with three ports (CPU + MAC1 + MAC2)
and owns the switch fabric registers, the per-MAC register windows,
and all switch IRQs.
- Implements port_stp_state_set to drive the hardware's per-port
Disabled/Blocking/Forwarding state machine from the bridge's STP
state.
- Offloads the bridge's FDB and VLAN membership into the switch's CAM
and per-port VLAN tables (port_fdb_add/del, port_vlan_add/del), and
installs CAM trap entries so link-local STP/LLDP frames reach the
CPU port.
- Enables the bridge's TX forwarding offload so data frames take the
switch's own CAM/VLAN lookup, the same path a frame ingressing on
MAC1/MAC2 takes, instead of being flooded by software.
- Registers a per-MAC MDIO bus and wires up phylink for each port,
including PHY connection management on link state changes.
- Registers a PTP Hardware Clock (PHC) backed by the IP's shared RTC
timer block and drives the per-MAC PTP TX/RX hardware paths so
event-frame timestamping bypasses the switch fabric and DMA
entirely.
DSA tag protocol:
- Adds DSA_TAG_PROTO_XLNX_TSN. The xmit hook intercepts PTP frames for
the per-MAC PTP TX path, other frames pass through unchanged. The rcv
hook is a defensive no-op since RX demux is handled by the endpoint
driver's port metadata rather than by an in-band tag.
Changes in v1:
- Reworked the switch side of the driver onto the DSA framework
instead of exposing MAC1/MAC2 as independent, framework-unaware
netdevs.
- Added the DSA tag protocol and moved RX classification from an
skb->dev based scheme into DSA port metadata attached by the
endpoint driver.
- Added STP state offload (port_stp_state_set), bridge FDB/VLAN
offload backed by the switch CAM and VLAN tables, bridge TX
forwarding offload, and CAM trap entries for link-local control
frames.
- Carried phylink, per-MAC MDIO, and PTP support over from the
previous posting, now wired up through DSA port ops instead of
the platform driver.
- Split the driver into a thin wrapper, endpoint driver, and DSA
switch driver along the DSA compatible/binding boundaries instead
of a single flat driver.
Test information:
- Validated these changes on the net-next branch with the MCDMA patches
cherry-picked from linux-next, along with the series listed below
https://lore.kernel.org/all/20260713072146.45269-1-srinivas.neeli@amd.com/
- Performed ping, iperf3, and 2-step PTP tests on the ZynqMP platform,
including bridging swp1/swp2 through a Linux bridge with hardware
FDB/VLAN offload enabled.
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
.../net/xlnx,tsn-endpoint-ethernet-mac.yaml | 343 ++++
MAINTAINERS | 11 +
drivers/net/dsa/Kconfig | 2 +
drivers/net/dsa/Makefile | 1 +
drivers/net/dsa/xilinx/Kconfig | 12 +
drivers/net/dsa/xilinx/Makefile | 3 +
drivers/net/dsa/xilinx/xilinx_tsn.c | 1621 +++++++++++++++++
drivers/net/dsa/xilinx/xilinx_tsn.h | 417 +++++
drivers/net/dsa/xilinx/xilinx_tsn_ptp.c | 590 ++++++
drivers/net/ethernet/xilinx/Kconfig | 1 +
drivers/net/ethernet/xilinx/Makefile | 1 +
drivers/net/ethernet/xilinx/tsn/Kconfig | 16 +
drivers/net/ethernet/xilinx/tsn/Makefile | 2 +
drivers/net/ethernet/xilinx/tsn/xilinx_tsn.h | 15 +
.../net/ethernet/xilinx/tsn/xilinx_tsn_ep.c | 926 ++++++++++
.../net/ethernet/xilinx/tsn/xilinx_tsn_main.c | 108 ++
include/linux/dsa/xlnx_tsn.h | 25 +
include/net/dsa.h | 2 +
net/dsa/Kconfig | 9 +
net/dsa/Makefile | 1 +
net/dsa/tag_xlnx_tsn.c | 60 +
21 files changed, 4166 insertions(+)
create mode 100644 Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
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
create mode 100644 drivers/net/dsa/xilinx/xilinx_tsn_ptp.c
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.h
create mode 100644 drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c
create mode 100644 drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c
create mode 100644 include/linux/dsa/xlnx_tsn.h
create mode 100644 net/dsa/tag_xlnx_tsn.c
--
2.34.1
^ permalink raw reply [flat|nested] 45+ messages in thread
* [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-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 02/20] net: xilinx: tsn: add TSN endpoint wrapper driver Nagadheeraj Rottela
` (19 subsequent siblings)
20 siblings, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 03/20] net: xilinx: tsn: add endpoint MAC driver skeleton Nagadheeraj Rottela
` (18 subsequent siblings)
20 siblings, 2 replies; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 04/20] net: xilinx: tsn: parse endpoint DMA channel configuration Nagadheeraj Rottela
` (17 subsequent siblings)
20 siblings, 2 replies; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 05/20] net: xilinx: tsn: bring up the endpoint MCDMA channels Nagadheeraj Rottela
` (16 subsequent siblings)
20 siblings, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 06/20] net: xilinx: tsn: add the endpoint RX data path Nagadheeraj Rottela
` (15 subsequent siblings)
20 siblings, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 07/20] net: xilinx: tsn: add the endpoint TX " Nagadheeraj Rottela
` (14 subsequent siblings)
20 siblings, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
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, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 09/20] net: dsa: tag_xlnx_tsn: add skeleton tag protocol Nagadheeraj Rottela
` (12 subsequent siblings)
20 siblings, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 10/20] net: dsa: xilinx: add skeleton driver for TSN switch Nagadheeraj Rottela
` (11 subsequent siblings)
20 siblings, 1 reply; 45+ 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] 45+ 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; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 12/20] net: dsa: xilinx: register per-MAC MDIO buses Nagadheeraj Rottela
` (9 subsequent siblings)
20 siblings, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
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, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
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, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
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, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
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, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
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, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
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, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
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, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
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, 1 reply; 45+ 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] 45+ 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-08 19:48 ` Jakub Kicinski
2026-08-07 22:28 ` [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Jakub Kicinski
20 siblings, 1 reply; 45+ 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] 45+ 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
2026-08-08 12:27 ` Neeli, Srinivas
2026-08-08 19:48 ` Jakub Kicinski
1 sibling, 1 reply; 45+ 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] 45+ 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
2026-08-08 12:28 ` Neeli, Srinivas
2026-08-08 19:48 ` Jakub Kicinski
1 sibling, 1 reply; 45+ 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] 45+ 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; 45+ 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] 45+ messages in thread
* Re: [PATCH 02/20] net: xilinx: tsn: add TSN endpoint wrapper driver
2026-08-07 20:58 ` Uwe Kleine-König
@ 2026-08-08 12:27 ` Neeli, Srinivas
0 siblings, 0 replies; 45+ messages in thread
From: Neeli, Srinivas @ 2026-08-08 12:27 UTC (permalink / raw)
To: Uwe Kleine-König, 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
Hi Uwe,
On 8/8/2026 2:28 AM, Uwe Kleine-König wrote:
> 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.
Thanks for review, Will update in V2 series.
>> [...]
>> +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.
Yes, patch 3 adds &xlnx_tsn_ep_driver to the array, so I will keep
platform_register_drivers().
Thanks
Srinivas Neeli
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 03/20] net: xilinx: tsn: add endpoint MAC driver skeleton
2026-08-07 21:00 ` Uwe Kleine-König
@ 2026-08-08 12:28 ` Neeli, Srinivas
0 siblings, 0 replies; 45+ messages in thread
From: Neeli, Srinivas @ 2026-08-08 12:28 UTC (permalink / raw)
To: Uwe Kleine-König, 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
Hi,
On 8/8/2026 2:30 AM, Uwe Kleine-König wrote:
> 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
Sure, Will update in V2 series.
Thanks
Srinivas Neeli
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 01/20] dt-bindings: net: add Xilinx TSN Endpoint Ethernet MAC
2026-08-07 10:44 ` [PATCH 01/20] dt-bindings: net: add Xilinx TSN Endpoint Ethernet MAC Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
dt-bindings: net: add Xilinx TSN Endpoint Ethernet MAC
This adds a new binding for the Xilinx TSN Endpoint Ethernet MAC IP,
describing a wrapper node that owns the six IP clocks and
xlnx,num-priorities, plus an endpoint MAC child and a three-port switch
child. A MAINTAINERS entry is added for the new file.
A few questions about the schema below.
> 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 @@
[ ... ]
> +properties:
> + compatible:
> + const: xlnx,tsn-endpoint-ethernet-mac-3.0
> +
> + reg:
> + maxItems: 1
[Severity: Low]
Should the wrapper node have a reg at all? It is listed in the top-level
required list alongside ranges, and in the example the two describe the
same window:
reg = <0x80040000 0x40000>;
ranges = <0x0 0x80040000 0x40000>;
Every child window (mac1 0x0+0x14000, ep-mac 0x16000+0xa000, mac2
0x20000+0x14000, switch 0x38000+0x8000) falls inside the parent's own
reg.
The commit message says the wrapper only owns the six shared clocks and
xlnx,num-priorities, and the wrapper driver in this series never maps
that region:
drivers/net/ethernet/xilinx/tsn/xilinx_tsn_main.c:tsn_ip_probe() {
...
ret = devm_clk_bulk_get(dev, TSN_NUM_CLOCKS, w->clks);
...
return devm_of_platform_populate(dev);
}
Would it be cleaner for a bus node that translates its children through
ranges to either drop reg or describe only a wrapper-private register
block that no child window overlaps?
> +
> + 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
[Severity: Low]
Are all six clocks present in every IP configuration?
A fixed items list implies minItems == maxItems == 6, and both clocks and
clock-names are in the top-level required list, so each DT must supply all
six phandles. Two of them are documented as RGMII specific, gtx90 ("GTX
90-degree phase shifted clock for RGMII") and ref ("Reference clock for
IDELAY control"), while the switch ports below also allow:
phy-mode:
enum:
- gmii
- rgmii
- rgmii-id
For a GMII-only build with no IDELAY primitives, would a DT author have to
invent phandles for clocks the IP does not instantiate? If those two are
optional in some configurations, could they be made conditional rather
than unconditionally required?
Note the wrapper driver in this series is consistent with what is written
here, it requests exactly these six names non-optionally via
devm_clk_bulk_get(dev, TSN_NUM_CLOCKS, w->clks), so the question is only
about the hardware.
[ ... ]
> +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
[ ... ]
> + required:
> + - compatible
> + - reg
> + - dmas
> + - dma-names
> + - tx-queues-config
> +
> + unevaluatedProperties: false
[Severity: Low]
How is an Endpoint-only instance described here? The top-level
description says the IP:
"provides low-latency network connectivity in either Endpoint-only or
Bridged Endpoint configurations"
and
"It also supports GMII/RGMII interfaces for connection to an external
PHY"
The ep-mac node declares no interrupts/interrupt-names, and
ethernet-controller.yaml contains no interrupt properties either, so with
unevaluatedProperties: false any interrupt or MDIO child on the endpoint
MAC is rejected. All interrupts and both MDIO buses live only on the
optional switch@ child, whose interrupt-names list is fixed to MAC1/MAC2
plus one ptp_timer.
An Endpoint-only DT (wrapper plus ep-mac, no switch child) is allowed by
the top-level required list, but ends up with no interrupt and no MDIO
description at all, and there is one compatible string with no if/then to
tell the two configurations apart.
Should ep-mac gain its own interrupts/interrupt-names and a way to reach
its PHY, or should the description be narrowed to the Bridged Endpoint
configuration the drivers in this series implement? Nothing appears
broken today, the endpoint driver requests no interrupts and drives the
data path entirely through dmaengine.
> +
> + "^switch@":
> + type: object
> + $ref: /schemas/net/dsa/dsa.yaml#/$defs/ethernet-ports
[ ... ]
> + 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
[Severity: Low]
This isn't a bug, but the child node names contradict the $nodename
patterns of the generic schemas this binding references.
mdio.yaml documents:
pattern: '^mdio(-(bus|external))?(@.+|-([0-9]+))?$'
and -mac1 / -mac2 matches none of the allowed suffixes (-bus, -external,
@<addr>, -<digits>). Similarly the patternProperties key "^ep-mac@"
forces names like the example's ep-mac@16000, while
ethernet-controller.yaml documents:
pattern: "^ethernet(@.*)?$"
Other in-tree bindings that $ref mdio.yaml for a child use a conforming
name (qca8k.yaml, realtek.yaml, renesas,rzn1-a5psw.yaml,
renesas,etheravb.yaml, faraday,ftgmac100.yaml, microchip,lan7800.yaml).
Would mdio@<offset> (or mdio-0 / mdio-1) and ethernet@<offset> work here
instead?
Related: the mdio-mac<N> nodes carry no reg, so nothing in DT locates the
MDIO block inside the parent mac1/mac2 window. The offset is instead
hard-coded in the driver added later in this series:
drivers/net/dsa/xilinx/xilinx_tsn.h:
#define TSN_MDIO_MC_OFFSET 0x00000500
Could the buses be described as mdio@500 with a reg so the offset comes
from DT?
> +
> + 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
[Severity: Medium]
Can a valid device tree bypass all of these port constraints?
The switch node uses $ref: /schemas/net/dsa/dsa.yaml#/$defs/ethernet-ports,
and the referenced schemas accept both container and port spellings:
Documentation/devicetree/bindings/net/ethernet-switch.yaml:
patternProperties:
'^(ethernet-)?ports$':
...
patternProperties:
'^(ethernet-)?port@[0-9a-f]+$':
...
oneOf:
- required:
- ports
- required:
- ethernet-ports
The local constraints are attached only under properties: ethernet-ports:
and only to the keys port@0 and "^port@[1-2]$", and that container is
declared additionalProperties: true.
So a DT spelling the container ports { port@0 ... } (the spelling most
existing DSA device trees use), or spelling the ports
ethernet-port@0/1/2, passes dt_binding_check while none of the hardware
rules apply: the CPU port's reg: const: 0 and phy-mode: const: internal,
the gmii/rgmii/rgmii-id enum on external ports, and the port@1/port@2
only topology of this three-port IP. A fourth port or phy-mode =
"sgmii" then validates cleanly and is only caught at probe:
drivers/net/dsa/xilinx/xilinx_tsn.c:xlnx_tsn_setup() {
...
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");
...
}
Would matching both container spellings ('^(ethernet-)?ports$') and both
port spellings, and dropping additionalProperties: true inside the
container, close this?
[ ... ]
^ permalink raw reply [flat|nested] 45+ 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
@ 2026-08-08 19:48 ` Jakub Kicinski
1 sibling, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: xilinx: tsn: add TSN endpoint wrapper driver
Adds a thin wrapper platform driver for the Xilinx TSN Endpoint Ethernet
MAC IP node, along with a new Kconfig symbol and Makefile under
drivers/net/ethernet/xilinx/tsn/. The wrapper enables the six IP-wide
clocks and then populates the sub-nodes so each can bind to its own driver.
A few questions below, all fairly minor.
> 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
[Severity: Low]
Should this depend on OF_ADDRESS rather than OF && HAS_IOMEM?
The only functional statement in tsn_ip_probe() is:
return devm_of_platform_populate(dev);
and in include/linux/of_platform.h the real implementation is compiled
only under CONFIG_OF_ADDRESS; otherwise the inline stub is:
static inline int devm_of_platform_populate(struct device *dev)
{
return -ENODEV;
}
OF && HAS_IOMEM does not imply OF_ADDRESS, per drivers/of/Kconfig:
config OF_ADDRESS
def_bool y
depends on !SPARC && (HAS_IOMEM || UML)
sparc selects OF and provides HAS_IOMEM, and NET_VENDOR_XILINX has no
architecture dependency, so XILINX_TSN is selectable there with
OF_ADDRESS=n.
In that configuration tsn_ip_probe() still runs devm_kzalloc(),
devm_clk_bulk_get() and clk_bulk_prepare_enable() for all six clocks and
registers the disable action, and then returns -ENODEV, so the driver can
never bind and no sub-node device is created. The clocks are unwound by
devres, so nothing leaks, but the whole driver is dead code in that
configuration.
This also stays as is through the end of the series, where the symbol is
still "depends on OF && HAS_IOMEM" plus "depends on XILINX_DMA", and
XILINX_DMA only has "depends on 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.
[Severity: Low]
The help text describes "the endpoint MAC that connects the IP to the host
CPU" and refers the reader to NET_DSA_XILINX_TSN, but at this commit the
symbol only builds xilinx_tsn_main.o (clock enable plus
devm_of_platform_populate()) and no NET_DSA_XILINX_TSN symbol exists in the
tree yet.
Both do appear later in the same series: the end-of-series Makefile has
xilinx_tsn-y := xilinx_tsn_main.o xilinx_tsn_ep.o
with &xlnx_tsn_ep_driver registered from xilinx_tsn_main.c, and
NET_DSA_XILINX_TSN is added in drivers/net/dsa/xilinx/Kconfig.
Since the text only appears in prose and not in a select or depends on,
there is no build effect; would it be worth wording the help text so it
matches what this commit actually builds, or is the forward reference
intentional for the series?
> +
> + If unsure, say N.
[ ... ]
> 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 @@
[ ... ]
> +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);
> +}
[Severity: Low]
Is there a consumer for xlnx,num-priorities anywhere?
Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
lists it in the top-level required block for this same node:
required:
- compatible
- reg
- clocks
- clock-names
- xlnx,num-priorities
- ranges
tsn_ip_probe() is the only driver binding
"xlnx,tsn-endpoint-ethernet-mac-3.0", and it only fills clks[].id and
calls devm_clk_bulk_get(), clk_bulk_prepare_enable() and
devm_of_platform_populate(). Grepping for num-priorities or
num_priorities at the end of the series finds hits only inside the YAML
itself plus unrelated fsl-mc and xe files.
So device tree authors must supply the property to pass dt_binding_check
while nothing reads it. Would it make sense to drop it from required
until a driver actually uses it?
[ ... ]
^ permalink raw reply [flat|nested] 45+ 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
@ 2026-08-08 19:48 ` Jakub Kicinski
1 sibling, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: xilinx: tsn: add endpoint MAC driver skeleton
This adds a platform driver for the AMD/Xilinx TSN endpoint MAC
("xlnx,tsn-ep-mac"), registering a netdev named "ep" with minimal netdev
and ethtool ops. There is no data path yet: ndo_open only starts the
queues and ndo_start_xmit drops frames.
> 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
[ ... ]
> +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 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;
[Severity: Medium]
Can a second TSN IP instance ever probe successfully with this fixed name?
"ep" carries no "%d", so the name is not auto-uniquified and
register_netdev() ends up returning -EEXIST:
net/core/dev.c:dev_prep_valid_name() {
...
if (netdev_name_in_use(net, want_name))
return -dup_errno;
...
}
reached from register_netdev() -> dev_get_valid_name(), which passes
dup_errno = EEXIST.
The binding added earlier in the series places the ep-mac@ child under the
soft-IP wrapper node and puts no singleton restriction on the wrapper:
Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml:
patternProperties:
"^ep-mac@":
type: object
So an FPGA design with two "xlnx,tsn-endpoint-ethernet-mac-3.0" instances is
a valid device tree, and the second xlnx_tsn_ep_probe() would fail at
register_netdev(), free the netdev, and leave that IP's DSA switch without
its conduit. The same collision happens if any unrelated interface already
holds the very generic global name "ep", for example after a udev rename.
Would something like "ep%d" be preferable here?
Also, since the name is chosen deterministically by the driver rather than
being of unknown origin, should this be NET_NAME_PREDICTABLE instead of
NET_NAME_UNKNOWN? include/uapi/linux/netdevice.h describes
NET_NAME_UNKNOWN as "unknown origin (not exposed to userspace)", so as
written sysfs name_assign_type hides the provenance from udev.
This still reads the same way at the end of the series, where the call
becomes alloc_netdev_mqs(sizeof(*ep), "ep", NET_NAME_UNKNOWN, ether_setup,
num_tx, num_rx), so no later patch changes it.
> +
> + 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);
> + }
[Severity: Low]
Is ep->regs meant to be used by this patch? Grepping
drivers/net/ethernet/xilinx/tsn/ at the end of the series, ep->regs is only
ever assigned and error-checked in xlnx_tsn_ep_probe() and never
dereferenced by any read or write, so the mapped window is dead state.
Would it be better to map it in the patch that first programs it?
Related question about the address handling: the station address is only
ever set in software here (eth_hw_addr_set(), or eth_hw_addr_random() when
the DT has no valid address), and ndo_set_mac_address is the generic
eth_mac_addr(), which just updates ndev->dev_addr. If the endpoint MAC
holds its own unicast address or filter words in that window, does the
address advertised to userspace end up permanently diverging from what the
bitstream or bootloader left in the hardware, and does a later
ip link set ep address ...
ever reach the device?
There is some counter-evidence that no hardware filtering is intended at
all: the driver implements neither ndo_set_rx_mode nor any promiscuity
handling, while DSA adds user-port addresses to the conduit via
dev_uc_add() in net/dsa/user.c and net/dsa/port.c. If that is the design,
could the commit message say so, and could the unused register mapping be
dropped for now?
Cross-instance finding from sashiko-gemini (e8e82d52a83540aba64fbedc360b0969d77529c63fbeb8f2b749e3809191d7cb):
[Severity: Medium]
Dropped packets are freed using `dev_kfree_skb()`, which aliases to `consume_skb()` and incorrectly signals successful transmission to tracepoints.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 04/20] net: xilinx: tsn: parse endpoint DMA channel configuration
2026-08-07 10:44 ` [PATCH 04/20] net: xilinx: tsn: parse endpoint DMA channel configuration Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: xilinx: tsn: parse endpoint DMA channel configuration
This patch counts the TX and RX MCDMA channels listed in "dma-names",
allocates the conduit netdev with that many hardware queues, and parses the
"tx-queues-config" child node to build a logical TX queue to physical DMA
channel map.
> 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
[ ... ]
> @@ -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
> +
[ ... ]
> @@ -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);
[Severity: Medium]
Is xlnx,dma-channel-num being validated against the wrong thing here?
The binding in
Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
describes it as the physical TX channel number, with range 0..15:
xlnx,dma-channel-num:
$ref: /schemas/types.yaml#/definitions/uint32
description:
DMA TX channel number connected to this priority queue.
minimum: 0
maximum: 15
and the tx-queues-config description in the same file spells out an
example:
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.
But ep->num_tx_queues is only a count: ep_count_dma_queues() does
str_has_prefix(name, "tx_chan") and increments a counter, so the numeric
suffix of each tx_chan<N> name is never recorded.
So for an endpoint with two TX channels using the binding's own documented
mapping (queue0 with xlnx,dma-channel-num = <5>), does this test reject a
schema-valid device tree with -EINVAL?
Likewise, since the dma-names pattern ^(tx|rx)_chan(1[0-5]|[0-9])$ does not
require channel 0 to exist, would dma-names = "tx_chan4", "tx_chan5", ...
have every legitimate channel number rejected here?
The error string says the channel "has no matching tx_chan", but no name
match is attempted - would it be clearer to record the channel indices
actually listed in dma-names and validate against that set, or else state
in the binding that the value is a 0-based index bounded by the number of
tx_chan entries and that TX channels must be named densely from tx_chan0?
Related: the follow-on patch "net: xilinx: tsn: bring up the endpoint MCDMA
channels" requests channels by a synthesized name:
snprintf(name, sizeof(name), "tx_chan%d", i);
ep->tx_chans[i] = ep_alloc_dma_chan(ep, name, true, TX_BD_NUM_DEFAULT);
for i in 0..num_tx_queues-1, so a sparse or non-zero-based naming that the
schema accepts would also make dma_request_chan() fail there.
> +
> + 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);
[Severity: Low]
Does the binding require exactly one queue node per tx_chan entry?
The tx-queues-config node in xlnx,tsn-endpoint-ethernet-mac.yaml only
constrains the shape of its ^queue[0-7]$ children; it has no required list
and no minimum child count, so a node with fewer queue children than
tx_chan entries (even none) validates.
A device tree with 8 tx_chan entries and 4 queue nodes would then fail
probe with "tx-config: described 4 queues but expected 8".
Separately, xlnx,num-priorities (minimum 2, maximum 8) is in the parent's
required list and is documented as the property that "determines the number
of priority queues available for QoS scheduling", but no driver under
drivers/net/ethernet/xilinx/ reads it - the priority queue count is instead
taken from the dma-names TX count via alloc_netdev_mqs() below.
Should the binding require one queue node per tx_chan entry (with the
driver cross-checking xlnx,num-priorities), or should the driver drop the
exact-equality requirement?
> +
> + 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);
[Severity: Low]
Is the TX maximum here inconsistent with the binding?
The dma-names schema allows tx_chan0..tx_chan15:
items:
pattern: "^(tx|rx)_chan(1[0-5]|[0-9])$"
with maxItems: 32, so 9 to 16 TX channel names validate, while this test
refuses anything above TSN_MAX_TX_QUEUE (8).
The RX side matches (binding maximum 16, TSN_MAX_RX_QUEUE 16), which makes
the TX side the outlier. Since the TX queue nodes are limited to
^queue[0-7]$ and xlnx,num-priorities maxes at 8, more than 8 TX channels
can never be used - should the TX name pattern be narrowed to
tx_chan[0-7], or should the driver accept 16?
> +
> + 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;
>
[ ... ]
For all three points above, the binding file and both of these functions
are unchanged at the end of the series, so none of them appear to be
resolved by a later patch.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 05/20] net: xilinx: tsn: bring up the endpoint MCDMA channels
2026-08-07 10:44 ` [PATCH 05/20] net: xilinx: tsn: bring up the endpoint MCDMA channels Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: xilinx: tsn: bring up the endpoint MCDMA channels
This patch requests the TX and RX MCDMA channels named in the device tree
when the endpoint interface is opened, gives each channel a ring of
preallocated SKB descriptor slots, resets the controller through tx_chan0,
and releases the channels on stop. It also adds a XILINX_DMA dependency to
the Kconfig entry.
No Link: tags are present in the commit header.
> 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
[ ... ]
> @@ -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;
[ ... ]
> @@ -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);
> + }
> +
[ ... ]
> +static int ep_init_dmaengine(struct xlnx_tsn_ep *ep)
> +{
> + int tx_allocated = 0, rx_allocated = 0;
> + char name[16];
> + int i, ret;
> +
[ ... ]
> + 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);
[Severity: Medium]
The channel names are re-invented here as a dense 0..N-1 sequence, but is
that what the binding in this series promises?
The dma-names schema in
Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
allows any subset of tx_chan0..15 / rx_chan0..15:
dma-names:
minItems: 2
maxItems: 32
items:
pattern: "^(tx|rx)_chan(1[0-5]|[0-9])$"
and ep_count_dma_queues() only counts the prefixes, discarding the indices:
if (str_has_prefix(name, "tx_chan"))
tx++;
else if (str_has_prefix(name, "rx_chan"))
rx++;
So for a device tree that validates against the binding, for example
dma-names = "tx_chan2", "tx_chan3", "rx_chan4", "rx_chan5";
probe succeeds, but ep_open()->ep_reset_dma_controller() asks for a channel
literally named "tx_chan0" and ep_init_dmaengine() asks for "tx_chan0" and
"tx_chan1". Does dma_request_chan() not return -ENODEV in that case,
leaving the interface impossible to bring up?
There is a second mismatch in the other direction: the binding permits up to
16 tx_chan entries, while ep_count_dma_queues() rejects anything above
TSN_MAX_TX_QUEUE (8) with -EINVAL at probe.
Would it be better to use the actual strings from dma-names, or to narrow the
binding to the dense, at-most-8-TX form the driver implements? Both the
binding pattern and these two functions are unchanged through the end of the
series.
[ ... ]
> +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");
[Severity: Medium]
Can -EPROBE_DEFER reach userspace through ndo_open here?
dma_request_chan() forwards that code verbatim:
drivers/dma/dmaengine.c:dma_request_chan() {
...
if (PTR_ERR(chan) == -EPROBE_DEFER)
return chan;
...
}
and of_dma_request_slave_channel() returns -EPROBE_DEFER when no of_dma
controller has registered for the phandle yet. Since probe() requests no DMA
channel, the netdev exists before the MCDMA provider is bound, and
XILINX_DMA is a tristate that can be loaded later:
ip link set ep up -> __dev_open() -> ep_open() -> ep_reset_dma_controller()
__dev_open() returns ops->ndo_open()'s value unchanged, so would
SIOCSIFFLAGS report errno 517 to userspace?
ep_alloc_dma_chan() has the same path: return ERR_CAST(err_chan) is turned
into ret = PTR_ERR(...) by ep_init_dmaengine() and returned from ep_open().
Separately, dev_err_probe() is being used outside a probe path. For
-EPROBE_DEFER it logs only at dev_dbg() level and overwrites the device's
deferred-probe reason string, so this open failure would be silent for the
operator. Would dev_err()/netdev_err() be more appropriate in ndo_open?
Both ep_reset_dma_controller() and ep_open() are unchanged at the end of the
series.
> +
> + 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;
> +}
[ ... ]
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 06/20] net: xilinx: tsn: add the endpoint RX data path
2026-08-07 10:44 ` [PATCH 06/20] net: xilinx: tsn: add the endpoint RX data path Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: xilinx: tsn: add the endpoint RX data path
This adds the RX side of the TSN endpoint: RX buffers are posted to every
RX channel at open time and reaped from the dmaengine completion callback,
which decodes a TUSER port id out of the descriptor metadata and passes
MAC1/MAC2 frames up the stack. The completion callback also refills the
slot it just consumed, and the channel teardown path grows an extra
terminate/synchronize/terminate sequence.
> 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
[ ... ]
> @@ -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));
[Severity: Low]
This isn't a bug today, but should this use xchan->ring_size instead of
RX_BD_NUM_DEFAULT?
struct xlnx_tsn_ep_dma_chan.ring_size is documented as the number of slots
in skb_ring, is set from the caller-supplied size in ep_alloc_dma_chan(),
and ep_free_dma_chan() masks with it:
skbuf_dma = chan->skb_ring[chan->ring_tail & (chan->ring_size - 1)];
The new RX code hardcodes RX_BD_NUM_DEFAULT for the ring mask here, for the
mask and CIRC_SPACE() in ep_dma_rx_cb(), and for the prefill count in
ep_init_dmaengine(). The two sides of the same ring now disagree about
where the size comes from, so a per-channel or ethtool-configurable RX ring
size would index out of range on one of them.
> + 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)
> +{
[ ... ]
> + 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;
> + }
[Severity: High]
Does this path drop every received frame on the configuration described by
the binding?
The binding added by this series wires the endpoint to an AXI MCDMA:
dmas = <&axi_mcdma_0 0>, ... <&axi_mcdma_0 23>;
In drivers/dma/xilinx/xilinx_dma.c, the "xlnx,axistream-connected"
property is only evaluated for XDMA_TYPE_AXIDMA, desc_metadata_modes is
only set to DESC_METADATA_ENGINE under that flag, and async_tx.metadata_ops
is only attached in the AXIDMA prep paths. xilinx_mcdma_prep_slave_sg(),
which is installed as device_prep_slave_sg for XDMA_TYPE_AXIMCDMA, never
sets metadata_ops.
So for an MCDMA instance:
ep_dma_rx_cb()
dmaengine_desc_get_metadata_ptr()
desc_check_and_set_metadata_mode() /* desc_metadata_modes == 0 */
-> -ENOTSUPP
IS_ERR_OR_NULL(metadata) -> dev_kfree_skb_any() + rx_dropped/rx_errors
Every frame would be freed and counted as an RX error, with only a
net_ratelimit() warning to explain it. Should ep_init_dmaengine() check
dmaengine_is_metadata_mode_supported() while arming the RX channels and
fail the open, rather than letting ndo_open succeed with a dead RX path?
> +
> + /* MCDMA metadata: [0] = status, [1] = sideband (TID/TDEST/TUSER), [2..] = app */
> + tuser = metadata[1] & TSN_TUSER_MASK;
[Severity: Medium]
Is this metadata layout comment correct, and is metadata[1] the sideband
word?
xilinx_dma_get_metadata_ptr() returns the app array, not the start of the
descriptor status words:
drivers/dma/xilinx/xilinx_dma.c:xilinx_dma_get_metadata_ptr() {
...
*max_len = *payload_len = sizeof(u32) * XILINX_DMA_NUM_APP_WORDS;
seg = list_first_entry(&desc->segments,
struct xilinx_axidma_tx_segment, node);
return seg->hw.app;
}
And in struct xilinx_aximcdma_desc_hw:
u32 status; /* @0x18 */
u32 sideband_status; /* @0x1C */
u32 app[XILINX_DMA_NUM_APP_WORDS]; /* @0x20 */
That makes metadata[0] == app[0] and metadata[1] == app[1]; status and
sideband_status sit before the returned pointer and are not reachable
through this API. The existing consumer of the same API agrees that index
N maps to app[N]:
drivers/net/ethernet/xilinx/xilinx_axienet_main.c:axienet_dma_rx_cb() {
...
rx_len = (app_metadata[LEN_APP] & 0xFFFF); /* #define LEN_APP 4 */
}
With the base off by two words, tuser and port_id are decoded from an
unrelated AXI4-Stream application word, so frames are either all dropped as
"unexpected TUSER port_id" or, once the later patch adds
skb_dst_set_noref(skb, &ep->port_md[port_id]->dst);
attributed to the wrong DSA user port. Could either the index or the
comment be corrected so the claimed layout is verifiable?
> + rx_len = ep->max_frm_size - result->residue;
[ ... ]
> + 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);
[Severity: Low]
This isn't a bug, but could the changelog say why the RX path is not NAPI
based? Frames are pushed with __netif_rx() straight from the dmaengine
completion callback, so there is no poll budget and no GRO, and every frame
goes through the per-CPU backlog where overflow is accounted to the core
drop counters rather than the driver. The per-packet statistics also use
the legacy dev->stats atomics (DEV_STATS_INC/DEV_STATS_ADD) in the hot
path.
> +
> +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);
> +}
[Severity: Medium]
Can RX stall permanently here if the refills fail?
ep_rx_submit_desc() returns silently on dev_alloc_skb() returning NULL, on
dma_map_single() failure and on dmaengine_prep_slave_sg() returning NULL.
None of those advance ring_head, bump a counter, or tell the caller
anything.
ep_dma_rx_cb() has already done xchan->ring_tail++ unconditionally, so the
number of outstanding descriptors (ring_head - ring_tail) drops by one for
every failed refill. The loop above then performs avail no-ops.
The only callers of ep_rx_submit_desc() are ep_init_dmaengine() and this
callback; there is no worker, timer or poll routine, as the commit message
notes:
Refill each slot from the completion callback unless the interface is
closing, which keeps the ring full without a separate refill worker.
So once the outstanding count reaches zero the DMA engine raises no further
completion, this callback is never entered again, and RX is dead until the
interface is downed and re-opened. dev_alloc_skb() here runs in the
dmaengine tasklet with GFP_ATOMIC, and a persistent mapping failure (IOMMU
or swiotlb space exhaustion) makes all attempts fail deterministically.
Would a bounded retry, a work item, or an error return from the arming path
be preferable? This still reads the same way at the end of the series.
> +
> 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);
> + }
[Severity: High]
Does this sequence actually close the window, or does it only clean up
after the damage?
Nothing serializes the re-arm in ep_dma_rx_cb() against the terminate here.
The check-then-act on ep->closing gives no exclusion:
tasklet: xilinx_dma_chan_desc_cleanup()
list_for_each_entry_safe(desc, next, &chan->done_list, node) {
...
spin_unlock_irqrestore(&chan->lock, flags);
dmaengine_desc_get_callback_invoke(&desc->async_tx, &result);
spin_lock_irqsave(&chan->lock, flags);
...
xilinx_dma_free_tx_descriptor(chan, desc);
if (chan->terminating)
break;
}
callback: READ_ONCE(ep->closing) == false
ip link set down: ep_stop() -> ep_exit_dmaengine() -> ep_free_dma_chan()
dmaengine_terminate_sync() -> xilinx_dma_terminate_all()
chan->terminating = true;
xilinx_dma_free_descriptors(); /* kfree()s done_list
entries, including the
iterator's `next` */
callback: ep_rx_submit_desc() -> dmaengine_submit()
xilinx_dma_tx_submit()
chan->terminating = false;
tasklet resumes: `if (chan->terminating) break;` is now false, so the
loop does desc = next and dereferences freed memory.
That looks like a use-after-free plus list_del() on freed memory inside the
DMA engine. The comment above shows the cleared terminating flag is known
about, but the terminate/synchronize/terminate runs after the corrupting
iteration. Would a real quiesce (for example refusing the re-arm under the
same lock that the teardown path takes, or fixing the flag handling in
xilinx_dma_tx_submit()) be the right shape here? This also still reads the
same way at the end of the series.
[Severity: Medium]
The commit message describes only the RX data path and says nothing about
changing teardown. Could it mention that ep_free_dma_chan(), which is
shared by TX and RX channels, now issues terminate/synchronize/terminate
instead of a single dmaengine_terminate_sync(), and that the reason is an
internal behaviour of one dmaengine provider (xilinx_dma_tx_submit()
clearing chan->terminating)? As written, the open-coded triple terminate
becomes silently dead code if that provider is ever fixed, and nobody
touching drivers/dma/xilinx/xilinx_dma.c has a pointer to the dependency.
>
> if (chan->is_tx) {
> while (chan->ring_tail != chan->ring_head) {
[ ... ]
> @@ -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;
[Severity: Medium]
Related to the refill question above: every failure inside
ep_rx_submit_desc() is discarded here and this returns 0, so ep_open() can
report success with an under-filled or completely empty RX ring. Should
this loop count successful submissions and fail the open when none (or too
few) were armed?
>
> err_free_chans:
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 07/20] net: xilinx: tsn: add the endpoint TX data path
2026-08-07 10:44 ` [PATCH 07/20] net: xilinx: tsn: add the endpoint TX " Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: xilinx: tsn: add the endpoint TX data path
This adds the TX path for the Xilinx TSN endpoint conduit netdev: the
logical queue is translated to a physical DMA channel through the map built
at probe, the frame is mapped and submitted to that channel, and the queue
is stopped/woken based on the software ring occupancy. A few questions
about the new ring accounting and the scatterlist handling below.
> 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
[ ... ]
> @@ -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 */
> +
[Severity: Low]
Does the implementation match this documented protection domain? The
kernel-doc says tx_lock protects the "SKB ring slots", but ep_start_xmit()
drops the lock right after ep_get_desc() and then fills the slot unlocked:
skbuf_dma = ep_get_desc(xchan, xchan->ring_head & (TX_BD_NUM_DEFAULT - 1));
...
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);
...
skbuf_dma->skb = skb;
skbuf_dma->sg_len = sg_len;
while ep_dma_tx_cb() reads skbuf_dma->skb/sgl/sg_len under the lock. The
pre-existing teardown loop in ep_free_dma_chan() also walks chan->ring_tail
and clears skbuf_dma->skb with no tx_lock held at all.
As far as I can tell there is no race today: ep_parse_tx_queue_config()
rejects duplicate queue and duplicate channel numbers, so each DMA channel
is fed by exactly one netdev txq and the slot writes have a single writer
serialised by __netif_tx_lock, and the drain path only runs after
netif_tx_disable() plus dmaengine_terminate_sync().
Would it be clearer to narrow the comment to what is actually enforced,
e.g. "protects TX ring head/tail; slot payloads are serialised by the txq
xmit lock", or alternatively extend the locked region to cover the slot
writes?
> 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));
[Severity: Low]
Should the new TX sites use xchan->ring_size instead of the compile-time
constant? struct xlnx_tsn_ep_dma_chan documents ring_size as the "number
of slots in @skb_ring" and ep_free_dma_chan() uses it:
skbuf_dma = chan->skb_ring[chan->ring_tail & (chan->ring_size - 1)];
...
for (i = 0; i < chan->ring_size; i++)
but the index masks in ep_dma_tx_cb() and ep_start_xmit(), and all four
CIRC_SPACE() computations, hardcode TX_BD_NUM_DEFAULT. They agree today
only because ep_init_dmaengine() passes TX_BD_NUM_DEFAULT to
ep_alloc_dma_chan(); if the ring size ever becomes configurable through
ethtool set_ringparam the producer and consumer would mask against the
wrong size while the free path masks correctly.
> + 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);
> +}
[Severity: Low]
Is the READ_ONCE() here paired with anything on the producer side? This
read happens after the scoped_guard() block has already released
ep->tx_lock, while ep_start_xmit() updates the index with a plain
read-modify-write:
spin_lock_bh(&ep->tx_lock);
xchan->ring_head++;
That is a marked read against a plain write, which KCSAN would report, and
the wake decision is taken on a non-atomic snapshot of the (head, tail)
pair. If the completion observes a stale head, CIRC_SPACE() over-estimates
the free space and the queue may be woken with only one slot free, so the
next ep_start_xmit() takes the "TSN TX ring full" path and returns
NETDEV_TX_BUSY.
Would WRITE_ONCE() on the ring_head update be appropriate here?
Note the stop/wake handshake itself looks fine: include/net/netdev_queues.h
documents these macros as single-producer/single-consumer safe and states
they do not prevent false wake ups when the ring is full, which
ep_start_xmit() re-checks on entry.
> static netdev_tx_t ep_start_xmit(struct sk_buff *skb, struct net_device *ndev)
> {
[ ... ]
> + 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);
> +
[ ... ]
> + 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;
[Severity: Medium]
Can dma_map_sg() walk past the end of skbuf_dma->sgl here? The return
value of skb_to_sgvec() is only tested for < 0 and then discarded, and the
assumed count sg_len = nr_frags + 1 is what gets passed as nents.
skb_to_sgvec() only emits an entry for a non-zero-length region and marks
the end at sg[nsg - 1]:
net/core/skbuff.c:__skb_to_sgvec() {
...
if ((copy = end - offset) > 0) {
...
}
net/core/skbuff.c:skb_to_sgvec() {
int nsg = __skb_to_sgvec(skb, sg, offset, len, 0);
...
sg_mark_end(&sg[nsg - 1]);
return nsg;
}
So a zero-length page frag, or skb_headlen() == 0, yields nsg < nr_frags +
1 while the caller still hands nents = sg_len to dma_map_sg().
dma_map_sg_attrs() iterates with for_each_sg(sgl, sg, nents, i), and
sg_next() returns NULL once sg_is_last() is true, so the extra iterations
dereference NULL inside the DMA core.
The same value is stored in skbuf_dma->sg_len, which the struct documents
as "number of valid entries in @sgl", and it is later used for the
dma_unmap_sg() in ep_dma_tx_cb() and on the err_unmap_sg path.
Would using skb_to_sgvec()'s return value as the entry count for
dma_map_sg(), the prep call and skbuf_dma->sg_len be more robust? Note
ndev->features sets NETIF_F_SG, so page frags are passed through as-is and
only frag_list skbs get linearized. For what it is worth, the upstream
axienet dmaengine TX path has the same pattern, which is likely why this
has not been observed in practice.
> +
> + 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;
[Severity: Low]
Any reason to call the ops member directly rather than use
dmaengine_prep_slave_sg(), which is what ep_rx_submit_desc() in this same
file does?
dma_rx_desc = dmaengine_prep_slave_sg(xchan->chan, skbuf_dma->sgl,
1, DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT);
device_prep_slave_sg is optional: xilinx_dma_probe() installs it only for
XDMA_TYPE_AXIDMA and XDMA_TYPE_AXIMCDMA, while for CDMA it installs only
device_prep_dma_memcpy and for VDMA only device_prep_interleaved_dma. If a
device tree pointed the endpoint dma-names at a CDMA or VDMA node, the RX
path would degrade gracefully (the wrapper returns NULL) but this line
would be a NULL function pointer call in the xmit path. Switching to the
wrapper would also make the extra dma_dev local unnecessary.
> +
> + 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);
[Severity: Medium]
What happens if dmaengine_submit() fails here? The cookie is discarded
without a dma_submit_error() check, and the ring slot plus the BQL byte
charge have already been committed above.
The provider this driver targets can fail the submit:
drivers/dma/xilinx/xilinx_dma.c:xilinx_dma_tx_submit() {
if (chan->err) {
err = xilinx_dma_chan_reset(chan);
if (err < 0)
return err;
}
...
}
xilinx_dma_reset() returns -ETIMEDOUT on reset timeout, and in that path
the descriptor is never appended to the pending list, so ep_dma_tx_cb()
never runs for it.
Because the completion retires strictly by ring_tail:
skbuf_dma = ep_get_desc(xchan,
xchan->ring_tail & (TX_BD_NUM_DEFAULT - 1));
one lost submit leaves a permanent one-slot offset. After that, every
completion unmaps the scatterlist and calls dev_consume_skb_any() on a
frame that may still be in flight in the engine, the buffers the engine
actually finished stay mapped, and the length handed to
netif_txq_completed_wake() belongs to a different frame than the one BQL
was charged for.
The bytes charged for the frame that was never queued also stay outstanding
in the DQL forever. Since ep_netdev_ops registers no .ndo_tx_timeout:
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,
};
__netdev_watchdog_up() never arms the watchdog, so a queue left stopped by
the skewed accounting is never detected or recovered while the interface
stays up.
Would it make sense to submit first and only advance ring_head and call
netdev_tx_sent_queue() once dma_submit_error() reports success, and to add
an .ndo_tx_timeout handler?
> +
> + 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;
[ ... ]
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 08/20] net: xilinx: tsn: deliver endpoint RX frames to DSA user ports
2026-08-07 10:44 ` [PATCH 08/20] net: xilinx: tsn: deliver endpoint RX frames to DSA user ports Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: xilinx: tsn: deliver endpoint RX frames to DSA user ports
The endpoint driver now allocates one METADATA_HW_PORT_MUX metadata_dst
per MAC port at probe time and attaches the matching entry to each RX
frame based on the TUSER port_id, so the DSA core can route frames to
the correct swpN user netdev.
> 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
[ ... ]
> @@ -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;
[Severity: Medium]
Should this attach be gated on the ep netdev actually being a DSA conduit?
The only in-tree receive-side consumer that interprets and then drops this
dst is dsa_switch_rcv() in net/dsa/tag.c:
if (md_dst && md_dst->type == METADATA_HW_PORT_MUX) {
unsigned int port = md_dst->u.port_info.port_id;
skb_dst_drop(skb);
if (!skb_has_extensions(skb))
skb->slow_gro = 0;
and that path is only reached for ETH_P_XDSA, which eth_type_trans() returns
only when netdev_uses_dsa(dev) is true.
NET_DSA_XILINX_TSN is a separate tristate from XILINX_TSN, so the ep netdev
can be up and receiving with no switch bound (switch driver not built, no
switch node in DT, switch probe failure, or the window between the ep
register_netdev() and dsa_register_switch()). In that configuration nothing
drops the metadata dst, and every received frame carries a dev-less
DST_METADATA into the generic stack: skb->slow_gro is forced on for all
traffic, IPv4 early demux is skipped because ip_rcv_finish_core() requires
!skb_dst(skb), and the stale port_id 1/2 stays readable through
skb_metadata_dst() by tc/BPF/netfilter ingress, AF_PACKET taps, bridging and
clones.
Both existing users of this mechanism gate the attach. mtk_poll_rx() in
drivers/net/ethernet/mediatek/mtk_eth_soc.c:
if (mtk_is_netsys_v1(eth) && (trxd.rxd2 & RX_DMA_VTAG) &&
netdev_uses_dsa(netdev)) {
...
skb_dst_set_noref(skb, ð->dsa_meta[port]->dst);
and airoha_qdma_rx_process() in drivers/net/ethernet/airoha/airoha_eth.c:
if (netdev_uses_dsa(netdev)) {
...
skb_dst_set_noref(q->skb,
&port->dsa_meta[sptag]->dst);
[Severity: Medium]
Is the noref dst still valid by the time it is dereferenced?
skb_dst_set_noref() stores an unreferenced pointer that must either be
consumed inside the same RCU read-side section or upgraded with
skb_dst_force(). Neither happens here: __netif_rx() does not process the
frame inline, netif_rx_internal() enqueues it on a softnet backlog queue
(possibly a remote CPU's under RPS) and returns, so the DMA completion
callback's BH section ends while the skb still holds the raw pointer:
ep_dma_rx_cb()
skb_dst_set_noref()
__netif_rx() -> netif_rx_internal() -> enqueue_to_backlog()
later NET_RX softirq:
process_backlog() -> __netif_receive_skb_core() -> dsa_switch_rcv()
skb_metadata_dst() / skb_dst()
The two existing users consume the metadata in the same BH section via
napi_gro_receive(). The one core call site that queues a dst-carrying skb
to the backlog, dev_loopback_xmit(), upgrades first:
DEBUG_NET_WARN_ON_ONCE(!skb_dst(skb));
skb_dst_force(skb);
netif_rx(skb);
Would napi_gro_receive()/netif_receive_skb(), or an skb_dst_force() before
the handoff, be more appropriate here?
> @@ -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;
> + }
> + }
> +}
[Severity: High]
Can this free a metadata_dst that skbs still point at?
metadata_dst_free() in net/core/dst.c is an unconditional kfree():
void metadata_dst_free(struct metadata_dst *md_dst)
{
...
kfree(md_dst);
}
It ignores dst->__rcuref (dst_init() starts it at 1) and skips the
call_rcu_hurry(&dst->rcu_head, dst_destroy_rcu) deferral that dst_release()
performs. Meanwhile ep_dma_rx_cb() published the raw pointer to the stack
with skb_dst_set_noref().
Two paths look reachable:
(a) xlnx_tsn_ep_remove() frees right after unregister_netdev() with no
synchronize_net() of its own. flush_all_backlogs() only purges skbs
still sitting on a backlog queue, not an skb already dequeued and in
flight in __netif_receive_skb_core() on another CPU, nor a clone that
already left the netdev-scoped backlog. Such an skb then reads
md_dst->type / md_dst->u.port_info.port_id out of freed memory in
net/dsa/tag.c or the flow dissector.
(b) If anything upgrades the noref dst with skb_dst_force() (for instance
__dev_queue_xmit() when the frame is bridged or forwarded to a device
without IFF_XMIT_DST_RELEASE, which is reachable in the ungated
non-DSA case above), dst_hold_safe() succeeds because rcuref starts at
1 and the skb owns a reference. The kfree() here then frees a
still-referenced object, and the skb's later skb_dst_drop() ->
dst_release() does a rcuref_put() write on freed memory and may queue
dst_destroy_rcu() on it.
Would dst_release(&ep->port_md[i]->dst) be the right teardown, matching
mtk_free_dev():
dst_release(ð->dsa_meta[i]->dst);
and the equivalent dst_release(&port->dsa_meta[i]->dst) in the airoha
driver?
[ ... ]
> @@ -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);
> }
[Severity: High]
Same concern as above, at the call site: the metadata_dst objects are
kfree()d here immediately after unregister_netdev(), with no RCU grace
period and no refcount check, while skbs or clones referencing them may
still be in flight in the receive stack or queued in a qdisc after an
skb_dst_force() upgrade.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 09/20] net: dsa: tag_xlnx_tsn: add skeleton tag protocol
2026-08-07 10:44 ` [PATCH 09/20] net: dsa: tag_xlnx_tsn: add skeleton tag protocol Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: tag_xlnx_tsn: add skeleton tag protocol
This adds DSA_TAG_PROTO_XLNX_TSN and a new tag driver for the AMD/Xilinx
TSN Endpoint Ethernet MAC, which carries port classification out of band
via a METADATA_HW_PORT_MUX metadata_dst instead of an in-band CPU tag.
The tagger is described as a no-op, with the PTP TX intercept deferred to
a later patch.
A few questions below.
> 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
[Severity: Low]
Should this symbol sit between NET_DSA_TAG_VSC73XX_8021Q and
NET_DSA_TAG_XRS700X instead of after NET_DSA_TAG_YT921X? The Makefile
change in this same patch does place it alphabetically:
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
so the two lists no longer correspond. This ordering is still the same at
the end of the series.
> + 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.
[Severity: Low]
The help text tells the user that "PTP TX frames are intercepted in the
xmit hook to be steered into the per-MAC PTP TX register window", but
xlnx_tsn_xmit() added by this same patch is a bare pass-through with no
ETH_P_1588 classification and no tagger_data->ptp_tx() call, and the file
does not include <linux/dsa/xlnx_tsn.h> yet.
Would it make sense to add the second sentence of the help text together
with the later patch that implements it? The commit message itself says
"A later patch adds the PTP TX intercept", so at this revision menuconfig
describes behaviour that is not present.
The mismatch does go away at the end of the series, where
xlnx_tsn_xmit() gains:
net/dsa/tag_xlnx_tsn.c:xlnx_tsn_xmit() {
...
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;
}
> +
> endif
[ ... ]
> 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 @@
[ ... ]
> +static struct sk_buff *xlnx_tsn_rcv(struct sk_buff *skb,
> + struct net_device *dev)
> +{
> + kfree_skb(skb);
> + return NULL;
> +}
[Severity: Low]
Could this drop be documented and made visible? The commit message and
the Kconfig text describe a "no-op tag driver" that "has no tag bytes to
add or strip", but xlnx_tsn_rcv() destroys every skb it is handed, with no
drop reason, no counter and no comment.
The hook does look mandatory for the design, since the conduit RX
interception only happens when .rcv is non-NULL:
include/net/dsa.h:netdev_uses_dsa() {
return dev->dsa_ptr && dev->dsa_ptr->rcv;
}
but that rationale is not stated anywhere in the file.
For frames that do carry the metadata dst attached by the EP MAC driver:
drivers/net/ethernet/xilinx/tsn/xilinx_tsn_ep.c:ep_dma_rx_cb() {
...
skb_dst_set_noref(skb, &ep->port_md[port_id]->dst);
...
}
dsa_switch_rcv() takes the METADATA_HW_PORT_MUX branch and never reaches
this function. Anything else falls into:
net/dsa/tag.c:dsa_switch_rcv() {
...
} else {
nskb = cpu_dp->rcv(skb, dev);
}
...
}
so a conduit frame whose metadata dst was stripped, for example by a tc
ingress action on the conduit, disappears without showing up in
ip -s link, dropwatch or drop-reason tracing. Would
kfree_skb_reason() plus a short comment explaining that the hook exists
only to make netdev_uses_dsa() true be preferable here? This body is
unchanged at the end of the series.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 11/20] net: dsa: xilinx: implement port_stp_state_set
2026-08-07 10:44 ` [PATCH 11/20] net: dsa: xilinx: implement port_stp_state_set Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: xilinx: implement port_stp_state_set
This adds a .port_stp_state_set callback that maps the five bridge STP
states onto the 3-bit per-port fields of the Switch Port State Control
register at +0x004C, using a self-clearing commit bit plus
readl_poll_timeout() for the acknowledge. setup() puts the CPU port in
FORWARDING and the user ports in DISABLED, and teardown() puts the ports
back to DISABLED.
A few questions on the new code below.
First, on the commit message itself:
> The bridge STP machine does not manage the CPU port. Its forwarding
> state does not change after setup().
[Severity: Low]
Is this accurate? The teardown() added by this same patch writes DISABLED
to the CPU port:
xlnx_tsn_set_port_state(sw, XLNX_TSN_CPU_PORT, TSN_PORT_STATE_DISABLED);
And because the driver now provides .port_stp_state_set, the DSA core also
drives the CPU port state on its own:
dsa_port_setup() [DSA_PORT_TYPE_CPU] -> dsa_port_enable(dp, NULL)
-> dsa_port_enable_rt() -> dsa_port_set_state_now(dp, BR_STATE_FORWARDING, false)
dsa_port_teardown() [DSA_PORT_TYPE_CPU] -> dsa_port_disable(dp)
-> dsa_port_disable_rt() -> dsa_port_set_state_now(dp, BR_STATE_DISABLED, false)
So the CPU port is written FORWARDING twice at bring-up (making the
explicit write in setup() redundant) and DISABLED from two paths at
unbind. Could the changelog be reworded to say the core drives the CPU
port state too, and that teardown disables it?
> 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
[ ... ]
> @@ -11,6 +15,86 @@
> +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;
> +}
[Severity: Medium]
Can this timeout path wedge the port permanently? When the 5 ms ack poll
expires, xlnx_tsn_set_port_state() returns -ETIMEDOUT with chg_bit still
set in TSN_PORT_STATE_CTRL, and it neither clears the bit nor re-drives
the transfer. Per the comment right above the precondition check, the bit
will not re-arm, so every later call for that port hits:
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;
}
That includes a subsequent probe attempt, since xlnx_tsn_setup()
propagates the error and aborts registration. Meanwhile the state field
and the 0->1 commit edge have already been written, so the hardware may
still latch the new encoding.
Would it make sense to write the commit bit back to 0 (or otherwise
re-drive it) on the timeout path, and to clear a stale commit bit once
before the first programming attempt in setup()?
The exact behaviour of the commit bit is not visible from the tree, so
this rests on the assumption stated in the comment.
> @@ -18,19 +102,83 @@ static enum dsa_tag_protocol xlnx_tsn_get_tag_protocol(struct dsa_switch *ds,
> +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);
> +}
[Severity: High]
Does this bridge MAC1 and MAC2 in hardware even when the two netdevs are
not bridged? The DSA core requests FORWARDING for any user port that is
not bridge-offloaded, on every ndo_open:
net/dsa/port.c:dsa_port_enable_rt() {
...
if (!dp->bridge)
dsa_port_set_state_now(dp, BR_STATE_FORWARDING, false);
...
}
That reaches the BR_STATE_FORWARDING case above and programs
TSN_PORT_STATE_FORWARDING on the MAC port. The ops table below adds no
port_bridge_join/leave and the driver programs no forwarding vector,
port-based VLAN, or any other isolation register (the only registers
touched are TSN_SW_STATUS_OFFSET and TSN_PORT_STATE_CTRL_OFFSET).
So after:
ip link set <mac1port> up
ip link set <mac2port> up
both switch ports sit in FORWARDING in the same flooding domain, and
frames received on one wire port are forwarded out the other in hardware
without reaching the host stack.
Documentation/networking/dsa/dsa.rst says of setup():
"This function is also expected to properly configure the switch to
separate all network interfaces from each other, that is, they should be
isolated by the switch hardware itself"
Should setup() program a per-port forwarding domain (for example a
port-based VLAN allowing only the CPU port and that port) so standalone
ports stay isolated?
[Severity: Medium]
The return value of xlnx_tsn_set_port_state() is discarded here. Since
.port_stp_state_set is a void callback, an -EBUSY or -ETIMEDOUT failure is
invisible to the bridge, which then records e.g. BLOCKING while the
hardware can still be FORWARDING. Would at least a dev_err() on failure
here help make that divergence visible?
> 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");
[Severity: Low]
This adds a probe-time topology requirement that isn't mentioned in the
changelog and isn't expressed in the binding. In
Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
the wire ports are described only as:
patternProperties:
"^port@[1-2]$":
with no "required" entry, so a device tree describing only port@0 and
port@1 validates against the schema but fails dsa_register_switch() with
-EINVAL, taking down the whole DSA tree for that conduit.
Should the binding mark port@1 and port@2 as required, or should the
driver tolerate a single wire port?
> +
> + /* CPU port stays in FORWARDING so host traffic always flows.
> + * User ports start in DISABLED and transition from there under
> + * bridge STP control.
> + */
[Severity: Low]
Is the second half of this comment correct? The DSA core replaces the
DISABLED encoding on the first ifup, with no bridge STP event involved:
net/dsa/port.c:dsa_port_enable_rt() {
if (!dp->bridge)
dsa_port_set_state_now(dp, BR_STATE_FORWARDING, false);
}
net/dsa/port.c:dsa_port_disable_rt() {
if (!dp->bridge)
dsa_port_set_state_now(dp, BR_STATE_DISABLED, false);
}
> + 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;
> }
[Severity: Low]
Should these error returns undo the CPU port write? The endpoint port has
already been committed to FORWARDING when the user-port loop runs, and if
xlnx_tsn_set_port_state() returns -EBUSY or -ETIMEDOUT for a user port the
fabric is left half-programmed.
The core does not call teardown() when setup() fails:
net/dsa/dsa.c:dsa_switch_setup() {
...
err = ds->ops->setup(ds);
if (err < 0)
goto unregister_notifier;
...
}
so probe fails, devm releases the register windows and frees struct
xlnx_tsn, but the endpoint port stays in FORWARDING with no driver bound.
Mirroring xlnx_tsn_teardown() on setup()'s error paths would keep this
symmetric.
> 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,
> };
[ ... ]
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 12/20] net: dsa: xilinx: register per-MAC MDIO buses
2026-08-07 10:44 ` [PATCH 12/20] net: dsa: xilinx: register per-MAC MDIO buses Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: xilinx: register per-MAC MDIO buses
This adds one MDIO bus per MAC, using the controller block at +0x500 inside
each MAC register window, registered under the mdio-mac1 / mdio-mac2 DT
children. A new struct xlnx_tsn_mac holds the per-MAC register window, the
mii_bus pointer and a back-pointer to the switch, and the MDC divisor is
derived from the wrapper node's s_axi clock rate.
A few questions below.
> 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
[ ... ]
> @@ -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);
> +}
> +
[ ... xlnx_tsn_mdio_read() / xlnx_tsn_mdio_write() snipped ... ]
> +/* 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;
> +}
[Severity: Medium]
Should the clamp path bring the bus up at all? For this block the rate is
f_MDC = f_HOST / ((1 + CLOCK_DIVIDE) * 2), so with div clamped to 0x3f the
resulting MDC is host_hz / 128, which is above 2.5 MHz for any s_axi rate
over 320 MHz:
320000000 / (2 * 2500000) - 1 = 63 = 0x3f
Past that point the divisor cannot satisfy the clause-22 MDC maximum that
DIV_ROUND_UP(host_hz, TSN_MDIO_MAX_FREQ_HZ * 2) - 1 exists to enforce, yet
xlnx_tsn_mdio_register_one() still OR's the clamped value with
TSN_MDIO_MC_MDIOEN and enables the bus. Nothing in the IP flags a failed
transaction: MCR.READY asserts and MRD is latched, so xlnx_tsn_mdio_read()
hands whatever the PHY drove at the over-clocked rate back to the PHY core
as a valid register value.
The driver for the same register block treats this as fatal instead:
drivers/net/ethernet/xilinx/xilinx_axienet_mdio.c:axienet_mdio_enable() {
...
if (clk_div & ~XAE_MDIO_MC_CLOCK_DIVIDE_MAX) {
netdev_warn(lp->ndev, "MDIO clock divisor overflow\n");
return -EOVERFLOW;
}
...
}
Would returning an error here rather than clamping match that behaviour?
The comment above the function only justifies the clamp as keeping the value
inside the field, and makes no claim that the resulting MDC rate is legal.
The same question applies to the !host_hz case, which also returns the max
divisor and enables the bus with an unknown MDC rate.
> +
> +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;
> + }
[Severity: Medium]
Can the devres allocation here be mixed with the manual registration below?
This is the pattern commit f53a2ce893b2c ("net: dsa: mv88e6xxx: don't use
devres for mdiobus") warns about: "either use devres for both the mdiobus
allocation and registration, or don't use devres at all".
devm_mdiobus_alloc() ties the free to driver unbind, but of_mdiobus_register()
and mdiobus_unregister() run on the DSA setup/teardown cycle, and there is no
matching mdiobus_free()/devm_mdiobus_free() anywhere.
Two consequences look possible. First, xlnx_tsn_shutdown() clears drvdata:
dsa_switch_shutdown(&sw->ds);
platform_set_drvdata(pdev, NULL);
so a later unbind hits the early return in xlnx_tsn_remove():
sw = platform_get_drvdata(pdev);
if (!sw)
return;
dsa_unregister_switch() is then never called, ds->ops->teardown() (the only
mdiobus_unregister() site) never runs, and devres_release_all() reaches
mdiobus_free() with the bus still registered:
drivers/net/phy/mdio_bus_provider.c:mdiobus_free() {
...
WARN(bus->state != MDIOBUS_UNREGISTERED,
"%s: not in UNREGISTERED state\n", bus->id);
...
}
At that point the bus is put_device()d while its child mdio devices, and the
"mac1"/"mac2" window its read/write callbacks touch, are still live.
Second, dsa_switch_teardown() resets ds->setup = false, so ds->ops->setup()
can run again on a still-bound device (a multi-switch tree torn down and
re-set-up). xlnx_tsn_mdio_register_one() then calls devm_mdiobus_alloc()
again, since m->mii_bus was set to NULL by the teardown, and the previous
allocation is only freed at unbind. Does this accumulate mii_bus objects
across setup cycles?
mv88e6xxx also registers its buses from ->setup and uses plain
mdiobus_alloc()/mdiobus_free() for exactly this reason.
> +
> + 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);
> + }
> +}
> +
[ ... ]
> + 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;
[Severity: Low]
Do the hardcoded "mdio-mac1" / "mdio-mac2" child names pass dtschema? The
binding added earlier in the series documents both nodes with
'$ref: /schemas/net/mdio.yaml#', and that schema constrains the node name it
is applied to:
Documentation/devicetree/bindings/net/mdio.yaml:
properties:
$nodename:
pattern: '^mdio(-(bus|external))?(@.+|-([0-9]+))?$'
After "mdio" only -bus, -external, @<addr> or -<digits> are accepted, so
"mdio-mac1" and "mdio-mac2" do not match, and the binding's own example uses
those names. The '-external' entry in that whitelist exists precisely
because mv88e6xxx needed a non-default name for a nested mdio node; every
other in-tree binding that $ref's mdio.yaml (qca8k.yaml, realtek.yaml,
renesas,rzn1-a5psw.yaml, renesas,etheravb.yaml and others) just names the
child "mdio".
The reverse also holds: a schema-conforming spelling such as mdio-1 / mdio-2
or mdio@<offset> makes the lookup in xlnx_tsn_mdio_register_one() fail and
the function return success with no bus registered:
mdio_np = of_get_child_by_name(sw->dev->of_node, child_name);
if (!mdio_np)
return 0;
Was dt_binding_check run on the binding example with these names?
[ ... ]
> 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
[ ... ]
> @@ -45,19 +46,66 @@ enum tsn_port_state {
[ ... register offset definitions snipped ... ]
> +/**
> + * struct xlnx_tsn_mac - per-MAC switch-side state
> + * @sw: back-pointer to the parent switch (for dev_* logging in
> + * bus callbacks)
[Severity: Low]
Is the "for dev_* logging in bus callbacks" part accurate? The bus callbacks
added here, xlnx_tsn_mdio_wait_ready(), xlnx_tsn_mdio_read() and
xlnx_tsn_mdio_write(), only touch m->regs through mac_ior()/mac_iow() and
emit no log messages at all. Every dev_err()/dev_warn() added by this patch
sits in xlnx_tsn_mdio_clk_div() and xlnx_tsn_mdio_register_one(), which are
handed a struct xlnx_tsn *sw explicitly.
The commit message makes the same claim:
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.
In this commit m->sw is only written, in xlnx_tsn_probe():
sw->mac[XLNX_TSN_PORT_MAC1].sw = sw;
sw->mac[XLNX_TSN_PORT_MAC2].sw = sw;
With the rest of the series applied the only reader is
xlnx_tsn_ptp_rx_isr() in drivers/net/dsa/xilinx/xilinx_tsn_ptp.c, where it
serves as the IRQ-handler back-pointer rather than for logging, and the
wording here is unchanged at the end of the series. Could this be reworded,
for example "back-pointer used by the per-MAC IRQ handlers"?
> + * @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;
> +};
[ ... ]
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 13/20] net: dsa: xilinx: wire up phylink for the switch ports
2026-08-07 10:44 ` [PATCH 13/20] net: dsa: xilinx: wire up phylink for the switch ports Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: xilinx: wire up phylink for the switch ports
This wires up the MAC-side phylink ops for the three switch ports:
phylink_get_caps advertises RGMII variants, SGMII and GMII plus MAC_100FD
and MAC_1000FD on the two external user ports and PHY_INTERFACE_MODE_INTERNAL
with MAC_1000FD on the CPU port, while mac_link_up/mac_link_down program the
speed-config field and the TX_EN / RX_EN bits.
> 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
[ ... ]
> @@ -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);
> +}
[Severity: Low]
Should the set of interface modes advertised here match the DT binding
added earlier in this series?
xlnx_tsn_phylink_get_caps() advertises six modes for ports 1 and 2:
phy_interface_set_rgmii() sets PHY_INTERFACE_MODE_RGMII, RGMII_ID,
RGMII_RXID and RGMII_TXID, and SGMII and GMII are set explicitly.
Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
allows a narrower set for the same ports:
phy-mode:
enum:
- gmii
- rgmii
- rgmii-id
and the top-level description repeats the narrower claim:
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.
If the MACs really can do SGMII and the rgmii-rxid / rgmii-txid delay
variants, does a board synthesised that way fail dtbs_check because the
schema rejects the phy-mode value the driver supports?
If instead the binding is the accurate description, does this advertise
interface modes the hardware does not implement?
> +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.
> + */
> +}
Since mac_config is empty and has no error path, phylink would resolve the
link for any of the advertised modes, so a mismatch would show up as a
silently dead port rather than a probe or configuration failure. Would it
make sense to either extend the binding enum or drop the extra
supported_interfaces bits so the two agree?
Both files are still in this state at the end of the series, so this does
not appear to be resolved by a later patch.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 14/20] net: dsa: xilinx: program MAC frame filter and per-port nibbles
2026-08-07 10:44 ` [PATCH 14/20] net: dsa: xilinx: program MAC frame filter and per-port nibbles Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: xilinx: program MAC frame filter and per-port nibbles
This programs the switch fabric's 48-bit unicast frame filter from the DSA
conduit MAC, wildcarding the low nibble of byte 5, and fills that nibble in
per-port fields of the Switch Port State Control register. A netdev notifier
drives the programming on NETDEV_REGISTER/NETDEV_CHANGEADDR, and
port_set_mac_address rejects addresses that break the prefix or nibble rules.
A few questions below on the notifier lifetime, the nibble uniqueness rule and
the register access protocol.
> 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.
[Severity: Low]
Is "to set its nibble" the whole story for the swpN NETDEV_REGISTER path?
xlnx_tsn_handle_user_register() also rewrites the user netdev's MAC with
dev_addr_mod() in two of its three branches (prefix mismatch, and address
equal to the conduit MAC).
The commit message also does not mention NETDEV_CHANGEADDR handling, yet
xlnx_tsn_netdev_event() handles it for both the conduit and the user ports,
and the conduit case unconditionally overwrites every user port's MAC with a
synthesized address - including an address that xlnx_tsn_port_set_mac_address()
previously validated and accepted from an administrator.
Could the message describe the MAC overriding and the CHANGEADDR handling?
> 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
[ ... ]
> @@ -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);
[Severity: Medium]
Should this read-modify-write of the Switch Port State Control register follow
the same protocol as the other accessor of the same address?
xlnx_tsn_set_port_state() gates every access on the readiness bit and refuses
to touch the word while a change-commit bit is still pending:
err = xlnx_tsn_switch_status_ready(sw);
if (err) { ... 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) { ... return -EBUSY; }
and the header itself says of TSN_SW_STATUS_READY: "Poll this before changing
port state."
xlnx_tsn_set_port_mac_nibble() does neither, and it writes the word back with
the EP/MAC1/MAC2 change-commit bits and STP status fields unmasked.
If an earlier xlnx_tsn_set_port_state() returned -ETIMEDOUT with a chg bit
still set, does this write-back re-assert that write-1-to-commit bit and
re-commit whatever STP state happens to be in the word?
Both callers, xlnx_tsn_handle_conduit_changeaddr() and the NETDEV_CHANGEADDR
branch of xlnx_tsn_netdev_event(), discard the return value, so if the write
is dropped the nibble field stays at its reset value while dev_addr claims a
different nibble. Would it be worth propagating the error here?
> 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);
[Severity: Low]
Does this discard a MAC address that the binding says is valid device tree?
In Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml,
"^switch@" is declared as:
$ref: /schemas/net/dsa/dsa.yaml#/$defs/ethernet-ports
and both port@0 and "^port@[1-2]$" are declared with
"additionalProperties: true", so the inherited dsa-port properties, including
local-mac-address and mac-address, are accepted on those nodes. DSA core reads
and applies them:
net/dsa/dsa.c:dsa_port_setup()
of_get_mac_address(dp->dn, dp->mac);
net/dsa/user.c:dsa_user_create()
eth_hw_addr_set(user_dev, port->mac);
A DT-supplied port address is then honoured only if it happens to share the
conduit's 44-bit prefix. Should the binding document that constraint, or should
the driver reject the DT instead of overriding it?
Also, dev_addr_mod() does not touch dev->perm_addr, so does ethtool -P swpN
(and IFLA_PERM_ADDRESS) keep reporting the discarded address while dev_addr
holds the synthesized one?
> + 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);
> +}
[Severity: High]
Can the two ports end up with the same nibble here?
The commit message says "The nibble is the only thing that tells ports apart
once the top 44 bits are shared", but the uniqueness check only exists in
xlnx_tsn_port_set_mac_address(). Neither of the paths that actually program the
hardware fields checks it:
- the final else branch above takes dev->dev_addr[5] & 0xf verbatim
- xlnx_tsn_synth_port_mac() computes (ep_nibble + port) & 0xf without looking
at nibbles already claimed by the other port
Consider a device tree where port@1 has a local-mac-address sharing the
conduit prefix with low nibble (ep_nibble + 2) & 0xf, and port@2 has no DT
address. dsa_user_create() gives port@2 the conduit address:
net/dsa/user.c:dsa_user_create()
if (!is_zero_ether_addr(port->mac))
eth_hw_addr_set(user_dev, port->mac);
else
eth_hw_addr_inherit(user_dev, conduit);
On NETDEV_REGISTER, port@1 takes the else branch and keeps
(ep_nibble + 2) & 0xf; port@2 takes the ether_addr_equal() branch and
xlnx_tsn_synth_port_mac() produces (ep_nibble + 2) & 0xf as well. Two
prefix-matching DT addresses with the same low nibble collide the same way.
MAC1_PORT_MAC_NIBBLE_MASK and MAC2_PORT_MAC_NIBBLE_MASK then hold the same
value, with no warning, and both callers discard the helper's return value.
With TSN_SW_MGMT_QUEUING_EP_SA_EGRESS now enabled, does the fabric still have
any way to tell the two wire ports apart?
Userspace attempting to reach this state through ip link would be refused with
-EADDRINUSE, so should the register and changeaddr paths enforce the same rule
that xlnx_tsn_port_set_mac_address() does?
> +
> +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);
[Severity: Medium]
Does changing the user port's address out of band leak an entry from the
conduit's unicast address list?
DSA keys the conduit uc entry and the standalone host FDB entry on the user
port's dev_addr:
net/dsa/user.c:dsa_user_open()
err = dsa_user_host_uc_install(dev, dev->dev_addr);
and the sanctioned way to change it installs the new address before removing
the old one:
net/dsa/user.c:dsa_user_set_mac_address()
err = dsa_user_host_uc_install(dev, addr->sa_data);
if (err)
return err;
dsa_user_host_uc_uninstall(dev);
...
eth_hw_addr_set(dev, addr->sa_data);
git grep NETDEV_CHANGEADDR net/dsa/ finds no handler, so nothing in DSA
re-syncs the conduit uc list from the notifier this driver emits. After
ip link set <conduit> address ... with swp1 up, the conduit list still holds
the old address and never learns the new one. On port close:
net/dsa/user.c:dsa_user_host_uc_uninstall()
if (!ether_addr_equal(dev->dev_addr, conduit->dev_addr))
dev_uc_del(conduit, dev->dev_addr);
dev_uc_del() is then called with the new address, which was never added, so
does the netdev_hw_addr entry for the old address leak and leave the conduit's
promiscuity/uc_promisc count unbalanced?
Would going through dsa_user_set_mac_address() (or at least calling
dsa_user_host_uc_install()/dsa_user_host_uc_uninstall() around this) be
preferable to dev_addr_mod()?
> + call_netdevice_notifiers(NETDEV_CHANGEADDR, dp->user);
[Severity: Low]
The two sites in this patch that rewrite a user port's dev_addr disagree on
whether the stack has to be told. This one follows dev_addr_mod(dp->user, ...)
with call_netdevice_notifiers(NETDEV_CHANGEADDR, dp->user), while
xlnx_tsn_handle_user_register() performs the identical mutation with no
notification at all.
Is one of the two wrong? Neither site updates dev->perm_addr or
dev->addr_assign_type, and neither goes through the driver's own
xlnx_tsn_port_set_mac_address() validation.
The register-time site also runs mid-walk of netdev_chain, from the
NETDEV_REGISTER replay inside register_netdevice_notifier(), so notifier blocks
already invoked for that device have recorded the pre-override address.
> + }
> +}
> +
> +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;
[ ... ]
> + 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;
[Severity: Medium]
dsa_user_set_mac_address() propagates this error verbatim:
net/dsa/user.c:dsa_user_set_mac_address()
if (ds->ops->port_set_mac_address) {
...
if (err)
return err;
}
so does that make some standard operations fail on swpN?
- ip link set swp1 address <externally assigned MAC> returns -EINVAL
- ip link set swp1 master bond0 fails, because bond_enslave() programs the
slave MAC to the bond MAC via dev_set_mac_address() and the bond address
does not share the conduit prefix
- ip link set swp1 address <conduit MAC>, i.e. the default that
dsa_user_create() installs itself, can never be restored once the driver has
overridden it, because of the -EADDRINUSE above
If this is an unavoidable property of the fabric (ports differ only in the low
nibble), could the commit message and the binding say so explicitly? No other
DSA driver imposes this restriction today.
> +
> + 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;
> +}
> +
[ ... ]
> @@ -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);
[Severity: Low]
Is TSN_SW_MGMT_QUEUING_EP_SA_EGRESS ever cleared again?
xlnx_tsn_teardown() unregisters the notifier, unregisters MDIO and moves the
ports to DISABLED, but leaves this bit set, leaves the 48-bit DA filter written
by xlnx_tsn_program_frame_filter() in place, and leaves the per-port
MAC-nibble fields programmed. The setup error paths below (port-state
programming, MDIO registration, notifier registration) also return with the bit
set.
After teardown the fabric is left with SA-nibble egress steering armed and a
stale 44-bit DA prefix filter that nobody updates any more, since the notifier
that tracked conduit NETDEV_CHANGEADDR is gone. Should teardown and the setup
unwind clear these?
> /* 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);
[Severity: High]
Can this notifier outlive the memory it lives in?
sw->nb is unregistered only from xlnx_tsn_teardown(), and
net/dsa/dsa.c:dsa_switch_shutdown() - reached from xlnx_tsn_shutdown() - never
calls ds->ops->teardown(), while it does drop DSA's conduit reference:
dsa_switch_for_each_cpu_port(dp, ds) {
dp->conduit->dsa_ptr = NULL;
netdev_put(dp->conduit, &dp->conduit_tracker);
}
so after shutdown the notifier is still on netdev_chain with sw->conduit
pointing at a netdev whose only reference was just released. The comment above
dsa_switch_shutdown() notes the conduit driver may unregister its own
net_device from its .shutdown. If the memory is reused, can
dev == sw->conduit match a foreign netdev and send
xlnx_tsn_handle_conduit_changeaddr() off to dereference it?
xlnx_tsn_shutdown() also does platform_set_drvdata(pdev, NULL), so a later
unbind takes the early return in xlnx_tsn_remove():
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);
}
dsa_unregister_switch() and hence xlnx_tsn_teardown() never run, so devres
frees the devm_kzalloc'd struct xlnx_tsn with its embedded notifier_block still
linked into netdev_chain. Would a devm action for the unregistration (and a
netdev_hold() on the conduit) close both cases?
> + 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);
[Severity: High]
Is this too late to protect the dp->user dereferences in
xlnx_tsn_handle_conduit_changeaddr()?
DSA destroys the user netdevs before it calls the driver's teardown:
net/dsa/dsa.c:dsa_tree_teardown()
dsa_tree_teardown_ports(dst); /* frees the swpN netdevs */
dsa_tree_teardown_switches(dst); /* calls ds->ops->teardown */
net/dsa/dsa.c:dsa_port_teardown()
case DSA_PORT_TYPE_USER:
if (dp->user) {
dsa_user_destroy(dp->user);
dp->user = NULL;
}
and dsa_user_destroy() frees the netdev with rtnl already dropped, before
dp->user is cleared:
net/dsa/user.c:dsa_user_destroy()
rtnl_lock();
netdev_upper_dev_unlink(conduit, user_dev);
unregister_netdevice(user_dev);
phylink_disconnect_phy(dp->pl);
rtnl_unlock();
...
free_netdev(user_dev);
For that whole window the notifier is still registered. If an
ip link set <conduit> address ... races driver unbind, does
xlnx_tsn_handle_conduit_changeaddr() then call
dev_addr_mod(dp->user, 0, want, ETH_ALEN) and
call_netdevice_notifiers(NETDEV_CHANGEADDR, dp->user) on a freed netdev? The
"if (!dp->user) continue;" check does not seem to help, since free_netdev()
happens before dp->user = NULL and neither is ordered against the rtnl-held
notifier read.
The same window appears to exist on the setup side: dsa_user_create() assigns
port->user before register_netdev() (its out_free path does
free_netdev(user_dev); port->user = NULL;), so a concurrent conduit
CHANGEADDR can rewrite the address of a not-yet-registered netdev.
> xlnx_tsn_mdio_unregister_all(sw);
>
> dsa_switch_for_each_user_port(dp, ds)
[ ... ]
> 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
[ ... ]
> @@ -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
[Severity: Low]
Could this kernel-doc list all four cases the notifier implements?
xlnx_tsn_netdev_event() handles conduit NETDEV_REGISTER, user NETDEV_REGISTER,
conduit NETDEV_CHANGEADDR and user NETDEV_CHANGEADDR.
The undocumented conduit NETDEV_REGISTER case is the only site that calls
xlnx_tsn_derive_prefix() and xlnx_tsn_program_frame_filter(), i.e. the frame
filter this patch is about, and it is served by a helper named
xlnx_tsn_handle_conduit_changeaddr() - the name says changeaddr while it also
services REGISTER. Would renaming it help?
The user-port NETDEV_CHANGEADDR path, which programs the nibble from an
address that has not been validated, is undocumented as well.
> * @mac: per-MAC state, indexed by user-port number (index 0 unused;
> * MAC1 at [1], MAC2 at [2])
> */
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 15/20] net: dsa: xilinx: register PHC backed by the RTC timer block
2026-08-07 10:44 ` [PATCH 15/20] net: dsa: xilinx: register PHC backed by the RTC timer block Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: xilinx: register PHC backed by the RTC timer block
This adds a new xilinx_tsn_ptp.c that brings up the IP-wide RTC timer block
living inside MAC1's register window and registers it as a PTP hardware
clock, with gettime/settime/adjtime/adjfine ops serialized by a new
per-switch spinlock. A timer ISR counts 128 hardware ticks and emits
PTP_CLOCK_PPS once per second.
A few questions on the commit message and the new code below.
The commit message says:
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().
[Severity: Low]
Is the "Map it in probe()" part accurate for this patch? The only changes
to drivers/net/dsa/xilinx/xilinx_tsn.c here are the xlnx_tsn_ptp_init()
call plus the err_nb label in xlnx_tsn_setup(), and the
xlnx_tsn_ptp_exit() call in xlnx_tsn_teardown().
The "mac1" window itself is mapped by pre-existing code in
xlnx_tsn_probe():
ret = xlnx_tsn_map_reg(pdev, "mac1",
&sw->mac[XLNX_TSN_PORT_MAC1].regs);
which matches the preceding sentence about the driver already owning that
window. Could the message just drop the probe() claim?
> 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
[ ... ]
> @@ -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
[Severity: Low]
Is this description of the interrupt right? xlnx_tsn_ptp_timer_isr()
treats the same line as a 128 Hz periodic tick and only emits an event
once every TSN_TIMER_PULSES_PER_PPS interrupts:
sw->countpulse++;
if (sw->countpulse >= TSN_TIMER_PULSES_PER_PPS) {
So it is neither a 1 PPS nor an RTC-overflow interrupt, and the
@countpulse line two lines further down ("reset to zero every
TSN_TIMER_PULSES_PER_PPS ticks") already says something different.
> + * @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
> */
[ ... ]
> 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 @@
[ ... ]
> +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_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);
> + }
[Severity: Medium]
What ties this synthesized pulse to the PHC second boundary?
sw->countpulse starts counting from whichever tick arrives first after
request_irq(), and the ISR never looks at TSN_TIMER_CURRENT_RTC_NS, so
the phase of the emitted PTP_CLOCK_PPS inside the PHC second looks
arbitrary, up to 1/128 s (about 7.8 ms) away from the true second
rollover.
The counter is also not re-phased when the RTC offset moves:
xlnx_tsn_ptp_settime()
xlnx_tsn_rtc_offset_write(sw, &delta);
xlnx_tsn_ptp_adjtime()
xlnx_tsn_rtc_offset_write(sw, &now);
including the large step done from xlnx_tsn_ptp_init(). Since
ptp_clock_info.pps = 1 registers a real PPS source, a consumer using
these events would be steered to a constant wrong phase. Would anchoring
countpulse to the nanoseconds register (or resetting it on
settime/adjtime and on enable) be preferable?
> +
> + 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);
[Severity: Low]
Should sw->pps_enable and sw->countpulse be reset here as well?
struct xlnx_tsn is devm_kzalloc()'d once in probe() and survives a DSA
teardown/setup cycle, while xlnx_tsn_ptp_exit() only clears
sw->ptp_clock. If PPS was enabled before the teardown, pps_enable stays
1, and after the next xlnx_tsn_ptp_init() the ISR condition
if (sw->ptp_clock && READ_ONCE(sw->pps_enable))
is true again on a freshly registered PHC that nobody asked for PPS on,
with a pulse phase carried over from the old countpulse value.
> +
> + 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 = 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");
> + }
[Severity: Medium]
Is it safe to publish the clock before the hardware and sw->rtc_value are
initialized?
ptp_clock_register() calls posix_clock_register() and creates /dev/ptpN
before returning, so from this point on all ops are reachable from
userspace, which is exactly the concurrency the commit message describes.
If clock_adjtime() lands in xlnx_tsn_ptp_adjfine() during the window
below, sw->rtc_value is still 0 (devm_kzalloc'd), and
incval = adjust_by_scaled_ppm(0, scaled_ppm); /* 0 */
...
mac_iow(..., TSN_TIMER_RTC_INCREMENT, (u32)incval);
programs an increment of 0, stopping the RTC.
Two related points on the same window: the mac_iow() to
TSN_TIMER_RTC_INCREMENT below is done without sw->reg_lock while
xlnx_tsn_ptp_adjfine() writes the same register under the lock, so a
userspace frequency adjustment can be clobbered; and sw->rtc_value is
written here with a plain u64 store and read in adjfine with a plain
unlocked load, which can be observed torn on 32-bit.
Would moving the rtc_value assignment, the increment write and the
initial settime ahead of ptp_clock_register() address all three?
> +
> + 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);
[Severity: Low]
This isn't a bug on today's boards, but should the RTC rate come from the
DT clock rather than TSN_TIMER_GTX_CLK_FREQ?
The binding documents the clock that drives this block:
Documentation/devicetree/bindings/net/xlnx,tsn-endpoint-ethernet-mac.yaml
clocks:
items:
- description: GTX clock for transmitter logic and RTC
...
clock-names:
items:
- const: gtx
and places no frequency constraint on it. The driver already reads a
wrapper-node clock rate elsewhere:
xlnx_tsn_mdio_register_all()
s_axi = clk_get(sw->dev->parent, "s_axi");
...
host_hz = clk_get_rate(s_axi);
so clk_get_rate() on "gtx" looks available here too. On an instance
synthesized with a different RTC clock, both the nominal tick period and
the adjfine base would be permanently wrong.
Separately, the shift is applied after the division, so fractional
nanoseconds are truncated before reaching the 20 fractional bits (for
example 156.25 MHz gives 6 ns instead of 6.4 ns). Would
div_u64((u64)NSEC_PER_SEC << TSN_TIMER_RTC_NS_SHIFT, rate)
be the intended form?
> +
> + ts = ktime_to_timespec64(ktime_get_real());
> + xlnx_tsn_ptp_settime(&sw->ptp_clock_info, &ts);
[Severity: Medium]
Can the dropped return value here hide a failure to program the RTC
offset? xlnx_tsn_ptp_settime() returns without touching the hardware when
the requested time is below the free-running counter:
delta = timespec64_sub(*ts, counter);
...
if (delta.tv_sec < 0)
return -ERANGE;
xlnx_tsn_rtc_offset_write(sw, &delta);
On a board with no battery-backed RTC where the PL keeps running across a
warm reboot, the counter can be hours ahead while ktime_get_real() starts
near the epoch, so delta.tv_sec is negative. In that case the offset
registers keep whatever the previous incarnation or the bootloader left
there, and neither the log nor xlnx_tsn_setup() sees anything. Should the
return value be checked and propagated?
> +
> + 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;
> +}
[ ... ]
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 16/20] net: dsa: xilinx: drive per-MAC PTP TX/RX hardware paths
2026-08-07 10:44 ` [PATCH 16/20] net: dsa: xilinx: drive per-MAC PTP TX/RX hardware paths Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: xilinx: drive per-MAC PTP TX/RX hardware paths
This adds the per-MAC PTP TX and RX hardware paths to the Xilinx TSN DSA
driver: the tag protocol diverts PTP frames into a per-MAC hardware TX
slot through a ds->tagger_data callback, and two new per-MAC interrupts
drive TX timestamp completion and direct RX delivery to the user port.
It also wires up port_hwtstamp_get/set and get_ts_info.
A few questions about the new paths below.
> 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
[ ... ]
> @@ -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);
[ ... ]
> 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
[ ... ]
> @@ -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_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_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)
[Severity: Medium]
Do these numbers add up? The comment says "a 252 B usable area and an 8 B
HW timestamp footer", but 252 + 8 is larger than the 256 B slot.
TSN_PTP_HW_TSTAMP_OFFSET is 256 - 8 = 248, so the footer sits at slot bytes
248..255, which leaves at most 248 usable bytes, not 252.
In xlnx_tsn_ptp_recv() both the copy length and the bound check use 252:
memcpy_fromio_32(m, frame_base, skb->data, TSN_PTP_RX_FRAME_SIZE);
...
if (msg_len + ETH_HLEN > TSN_PTP_RX_FRAME_SIZE) {
...
skb_put(skb, msg_len + ETH_HLEN);
so for a frame where msg_len + ETH_HLEN lands in (248, 252] the skb ends up
carrying up to 4 bytes of the timestamp footer as payload. Should
TSN_PTP_RX_FRAME_SIZE be 248?
The TX side has the mirror of this. TSN_PTP_TX_MAX_FRAME_SIZE is 248 and
frame data starts at slot offset 8, so a maximum-length frame writes slot
bytes 8..255 - including the address xlnx_tsn_tx_tstamp_work() then reads
back as the transmit timestamp:
ts_off = TSN_PTP_TX_BUFFER_OFFSET(index) +
TSN_PTP_HW_TSTAMP_OFFSET;
Should the accepted TX length be 240 for this layout? As a side note,
TSN_PTP_HW_TSTAMP_OFFSET is derived from TSN_PTP_RX_HWBUF_SIZE but is also
applied to TX slots; it only works because both happen to be 256.
[ ... ]
> 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
[ ... ]
> @@ -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;
> + }
[Severity: Medium]
Is it intentional that frame data is moved with the byte-swapping register
accessors? mac_iow()/mac_ior() in xilinx_tsn.h are iowrite32()/ioread32():
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);
}
Those apply cpu_to_le32()/le32_to_cpu(), which is right for control and
status registers but not for a frame byte stream. On a big-endian host
memcpy_toio_32() assembles a host-order u32 out of four consecutive frame
bytes and writes it byte-reversed into the slot, so the MAC transmits a
corrupted frame; memcpy_fromio_32() has the mirrored effect on receive, so
eth_type_trans(), the messageType byte at ETH_HLEN and get_unaligned_be16()
all parse swapped bytes.
The Kconfig entry only depends on OF && HAS_IOMEM && NET_DSA &&
PTP_1588_CLOCK, so big-endian hosts of this soft IP are not excluded. Would
raw/stream accessors (__raw_writel/__raw_readl, iowrite32_rep/ioread32_rep,
or memcpy_toio/memcpy_fromio) be more appropriate here?
[ ... ]
> +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);
[Severity: High]
Can this slot allocator hand out a slot that an skb still sitting in
m->ptp_txq owns?
free_index comes from the hardware "frames waiting to be transmitted"
bitmap, which clears when a frame departs, while ptp_txq holds frames that
have not yet been timestamped - the TX IRQ only calls schedule_work(). So:
frame A: free_index = fls(0) = 0, queued with cb[0] = 0, departs
HW clears bit 0, TX IRQ schedules tx_tstamp_work
frame B: reads frame_waiting == 0, free_index = fls(0) = 0 again,
queued with cb[0] = 0
xlnx_tsn_tx_tstamp_work() then reads tx_packet == 0 and drains both skbs,
handing frame B a timestamp read out of slot 0 that belongs to frame A.
The reverse ordering looks worse: if the queue head carries a high index
(say 5) and later frames get low indices, this test in
xlnx_tsn_tx_tstamp_work() never becomes true again:
if (index > tx_packet) {
__skb_queue_head(&m->ptp_txq, skb);
break;
}
so the head is requeued forever, no further TX timestamps are delivered for
that port, and skbs accumulate until the driver is unbound.
TSN_PTP_TX_PACKET_FIELD_MASK is GENMASK(18, 16), i.e. 3 bits, so the status
value wraps 0..7 - can it be used as a monotonic ordering key at all?
Would tracking slot ownership in software (a used-slot bitmap plus the
queued index) be more robust?
> + 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);
[Severity: High]
Can this read past the end of the linear part of the skb? memcpy_toio_32()
walks skb->len bytes linearly from skb->data, but nothing here checks
skb_headlen(skb) or calls skb_linearize(), so for a paged skb it reads
beyond the head buffer (into skb_shared_info and past the kmalloc'd area)
and pushes those bytes into the hardware slot, i.e. out onto the wire.
Non-linear skbs do not appear to be filtered out on the way in. This
tagger sets no needed_tailroom, so dsa_user_setup_tagger() does not strip
the features:
if (user->needed_tailroom)
user->features &= ~(NETIF_F_SG | NETIF_F_FRAGLIST);
and skb_ensure_writable_head_tail() only does pskb_expand_head(), it never
linearizes. An AF_PACKET send with PACKET_VNET_HDR (attacker-chosen
hdr_len picking a small linear area) or a bridge-forwarded frame whose
ingress driver put payload in fragments would reach this copy.
The same assumption exists one layer up in xlnx_tsn_xmit(), which
dereferences eth_hdr(skb)->h_proto with no guarantee that the 14 byte MAC
header is in the linear area.
> +
> + skb->cb[0] = free_index;
> + __skb_queue_tail(&m->ptp_txq, skb);
[Severity: Medium]
What releases these skbs when the port or the link goes down? The only
paths that dequeue ptp_txq are xlnx_tsn_tx_tstamp_work() (which requires the
completion match to succeed) and xlnx_tsn_port_ptp_exit() at unbind:
scoped_guard(spinlock_irqsave, &m->ptp_tx_lock)
while ((skb = __skb_dequeue(&m->ptp_txq)) != NULL)
dev_kfree_skb_any(skb);
TSN_TC_TX_EN is only set in xlnx_tsn_mac_link_up(), so frames written into a
slot while the link is down never depart, never complete, and hold their
skbs - and with them the owning socket's sk_wmem_alloc charge via
skb->sk/sock_wfree - until the driver is unbound. Stale skbs from a
previous link-up also stay queued while fls(frame_waiting) re-allocates the
same slot indices. Would a drain on link down / port disable, or a
timeout, be worth adding?
> +
> + if (skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP)
> + skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;
[Severity: Medium]
Is m->hwtstamp_tx_type ever consumed? xlnx_tsn_port_hwtstamp_set() stores
it and xlnx_tsn_port_hwtstamp_get() reports it back, but the TX path gates
only on the skb flag here, and xlnx_tsn_tx_tstamp_work() then gates only on
SKBTX_IN_PROGRESS:
if (skb_shinfo(skb)->tx_flags & SKBTX_IN_PROGRESS) {
xlnx_tsn_read_tstamp(m, &hwtstamps, ts_off);
skb_tstamp_tx(skb, &hwtstamps);
}
So a socket with SOF_TIMESTAMPING_TX_HARDWARE still gets hardware TX
timestamps on the error queue while the port is configured
HWTSTAMP_TX_OFF. The RX half of the same ABI does honour its stored state
(READ_ONCE(m->hwtstamp_rx_filter) gates xlnx_tsn_read_tstamp()), so the two
halves disagree.
The commit message says "TX timestamping accepts HWTSTAMP_TX_OFF and
HWTSTAMP_TX_ON" and the kernel-doc calls @hwtstamp_tx_type the "current
SO_TIMESTAMPING TX type for this port", which both suggest the value has an
effect.
Separately, SKBTX_HW_TSTAMP is (SKBTX_HW_TSTAMP_NOBPF | SKBTX_BPF), so this
test also fires for BPF-only timestamping requests; the DSA core uses
SKBTX_HW_TSTAMP_NOBPF in dsa_skb_tx_timestamp(). Was
SKBTX_HW_TSTAMP_NOBPF intended here?
> +
> + 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;
[Severity: High]
Can this dereference a freed net_device during teardown? dp->user is read
here from hardirq context (xlnx_tsn_ptp_rx_isr() calls this directly) with
no RCU section, lock, or netdev reference, while the PTP RX interrupt is
only released much later, in xlnx_tsn_port_ptp_exit() from ds->ops->
teardown().
The DSA teardown order is ports first, switches second:
net/dsa/dsa.c:dsa_tree_teardown()
dsa_tree_teardown_ports(dst); -> dsa_port_teardown()
dsa_tree_teardown_switches(dst); -> ds->ops->teardown()
and dsa_port_teardown() frees the netdev before clearing the pointer:
case DSA_PORT_TYPE_USER:
if (dp->user) {
dsa_user_destroy(dp->user);
dp->user = NULL;
}
So between dsa_user_destroy() and the dp->user = NULL store, a PTP RX
interrupt reads a non-NULL dangling pointer and runs netif_running(),
netdev_alloc_skb(), eth_type_trans(), dev_sw_netstats_rx_add() and
netif_rx() on it. Should the PTP RX capture be masked in hardware and the
IRQ freed before the user netdevs go away (or the RX work moved out of
hardirq and synchronised against teardown)?
> +
> + while ((m->ptp_rx_hw_pointer & 0xf) != (m->ptp_rx_sw_pointer & 0xf)) {
[ ... ]
> + 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);
[Severity: High]
Does this leak stale slot contents to the network stack? msg_len is the PTP
messageLength field taken from the wire, not a hardware-reported receive
length, and the only validation is the upper bound above.
memcpy_fromio_32() always copies the full slot, so if a peer sends a short
0x88f7 frame that declares a large messageLength, skb_put() hands up to
~238 bytes of whatever was previously in that per-MAC RX slot (earlier
frames from other hosts, or uninitialised FPGA buffer content - the slot is
never zeroed) to netif_rx(), and from there to any listener or bridge port.
There is also no lower bound: msg_len may be 0 or shorter than the PTP
common header, and nothing cross-checks it against the number of bytes the
hardware actually received. Is there a hardware length field for the slot
that could be used instead?
> + skb->protocol = eth_type_trans(skb, user);
> + skb->ip_summed = CHECKSUM_UNNECESSARY;
[ ... ]
> + dev_sw_netstats_rx_add(user, skb->len);
[Severity: Medium]
Should this add ETH_HLEN back? eth_type_trans() has already pulled the 14
byte MAC header, while the core DSA receive path feeds the same per-CPU
tstats counter on the same netdev in the other unit:
net/dsa/tag.c:dsa_switch_rcv()
dev_sw_netstats_rx_add(skb->dev, skb->len + ETH_HLEN);
As it stands, rx_bytes reported through rtnl_link_stats64 under-counts PTP
frames by 14 bytes relative to every other frame on the same interface.
[Severity: Medium]
Is the non-irqsave u64_stats writer safe here? This runs in hardirq
context (request_irq() with flags 0 and no threaded handler), and
dev_sw_netstats_rx_add() uses plain u64_stats_update_begin()/
u64_stats_update_end() on the DSA user device's per-CPU syncp, which is
also written from the transmit path:
net/dsa/user.c:dsa_user_xmit()
dev_sw_netstats_tx_add(dev, 1, skb->len);
On 32-bit SMP those are real seqcount operations, so:
dsa_user_xmit() -> u64_stats_update_begin() (seq odd)
PTP RX hardirq on the same CPU
dev_sw_netstats_rx_add() -> begin()/end() (seq even again)
reader in dev_get_tstats64() sees an even seq mid-update
There is no _irqsave flavour of dev_sw_netstats_rx_add(), so would a
threaded IRQ / NAPI context for the RX path, or open-coded _irqsave
counter updates, be needed?
> + netif_rx(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;
> +}
[Severity: Medium]
How are these two interrupts acknowledged? This handler reads
TSN_PTP_TX_CONTROL_OFFSET and throws the value away, and
xlnx_tsn_ptp_rx_isr() only samples the RX packet pointer without ever
writing TSN_PTP_RX_PACKET_CLEAR, while the PPS handler in the same file
does an explicit write to clear:
mac_iow(&sw->mac[XLNX_TSN_PORT_MAC1], TSN_TIMER_INTERRUPT,
TSN_TIMER_INT_CLEAR);
If the discarded read is a read-to-clear acknowledge, then the
TSN_PTP_TX_PACKET_FIELD_MASK status has already been consumed by the time
xlnx_tsn_tx_tstamp_work() re-reads the register for tx_packet. If it is
not an acknowledge, does the line stay asserted until genirq's spurious
interrupt detector disables it? Either way, a bare register read with no
comment is hard to follow.
> +
> +int xlnx_tsn_port_ptp_init(struct xlnx_tsn *sw, int port,
> + const char *rx_name, const char *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);
[Severity: Medium]
Why is the RX pointer reset here but the TX block left untouched? The
first transmit consumes TSN_PTP_TX_FRAME_WAITING_MASK and the first
completion pass consumes TSN_PTP_TX_PACKET_FIELD_MASK exactly as the
hardware left them.
After a rebind, or a warm reset, or a boot stage that already used the PTP
TX block, those fields can be non-zero:
- a stale bit 7 makes xlnx_tsn_ptp_tx() take the "buffers full" branch and
free every PTP frame from then on
- other stale bits make fls(frame_waiting) pick a slot while ptp_txq is
empty
- a stale TX_PACKET makes the first xlnx_tsn_tx_tstamp_work() pass read a
timestamp footer out of a slot that never transmitted and report it to
userspace
xlnx_tsn_port_ptp_exit() also frees the IRQs and drains ptp_txq but leaves
the PTP TX/RX paths armed and any waiting bit set, so the next bind starts
from that state. Should init clear the TX control fields (and exit quiesce
the hardware)? Also, both this write and mac_iow(m,
TSN_PTP_TX_CONTROL_OFFSET, BIT(free_index)) are full-register writes to
registers that carry other fields - is a read-modify-write needed?
[ ... ]
> 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.
> */
>
[ ... ]
> 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);
[Severity: Medium]
This matches on the ethertype only, so every non-VLAN 0x88f7 frame is
diverted into the 248 byte hardware slot, not just the PTP event frames the
commit message describes ("PTP event frames bypass the switch fabric").
Announce, signalling and management messages carrying TLVs can exceed 248
bytes.
xlnx_tsn_ptp_tx() then drops such a frame with no accounting:
if (unlikely(skb->len > TSN_PTP_TX_MAX_FRAME_SIZE)) {
dev_kfree_skb_any(skb);
return;
}
...
if (frame_waiting & TSN_PTP_TX_BUFFERS_FULL_MASK) {
dev_kfree_skb_any(skb);
return;
}
Before this patch xlnx_tsn_xmit() returned the skb and it egressed through
the conduit, so this turns those frames into a silent black hole.
dsa_user_xmit() has already counted the frame as transmitted
(dev_sw_netstats_tx_add(dev, 1, skb->len)) before calling the tagger, so
nothing shows up in tx_dropped or tx_errors either. Should oversized
frames fall back to the normal path, and should the drops bump a counter?
> +
> + return NULL;
> }
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 17/20] net: dsa: xilinx: opt into TX forwarding offload on bridge join
2026-08-07 10:44 ` [PATCH 17/20] net: dsa: xilinx: opt into TX forwarding offload on bridge join Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: xilinx: opt into TX forwarding offload on bridge join
The patch adds xlnx_tsn_port_bridge_join() which unconditionally sets
*tx_fwd_offload = true, so the bridge sends a single copy and relies on the
switch fabric for per-port replication. It also sets ds->max_num_bridges
to 1 in xlnx_tsn_probe().
A few questions below.
> 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;
[Severity: High]
Does this drop PTP frames on all user ports but one?
The comment's premise does not seem to hold for the frame class the tagger
diverts. In net/dsa/tag_xlnx_tsn.c, xlnx_tsn_xmit() takes untagged
ETH_P_1588 frames out of the fabric path entirely:
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;
That handler, xlnx_tsn_ptp_tx() in
drivers/net/dsa/xilinx/xilinx_tsn_ptp.c, writes into the TX buffer of one
specific MAC:
m = &sw->mac[dp->index];
so the frame goes into the per-port PTP TX FIFO of the single egress MAC
and never reaches the fabric that would replicate it.
Before this patch the bridge cloned a flooded frame once per member port,
so each clone landed in its own MAC's PTP FIFO. With
BR_TX_FWD_OFFLOAD requested, net/bridge/br_switchdev.c suppresses the
remaining copies for the same hwdom:
if (nbp_switchdev_can_offload_tx_fwd(p, skb))
set_bit(p->hwdom, &BR_INPUT_SKB_CB(skb)->fwd_hwdoms);
...
return !test_bit(p->hwdom, &cb->fwd_hwdoms) &&
(!skb->offload_fwd_mark || cb->src_hwdom != p->hwdom);
With br0 containing both swp1 and swp2, an untagged PTP frame flooded from
another bridge port then loads only one MAC's PTP FIFO and the other user
port transmits nothing, with no counter or log for the loss. Should PTP
traffic be excluded from the offload, or should the tagger encode a flood
so the second copy is regenerated?
> +
> + return 0;
> +}
> +
[ ... ]
> @@ -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;
[Severity: Medium]
The commit message says:
"Set ds->max_num_bridges to 1 so the DSA core rejects a second bridge
instead of silently merging the ports."
Does the core actually reject it? In net/dsa/port.c,
dsa_port_bridge_create() only returns -EOPNOTSUPP when the bridge number
allocation comes back empty:
bridge->num = dsa_bridge_num_get(br, ds->max_num_bridges);
if (ds->max_num_bridges && !bridge->num) {
NL_SET_ERR_MSG_MOD(extack,
"Range of offloadable bridges exceeded");
kfree(bridge);
return -EOPNOTSUPP;
}
and dsa_user_changeupper() in net/dsa/user.c turns that into success:
if (err == -EOPNOTSUPP) {
NL_SET_ERR_MSG_WEAK_MOD(extack,
"Offloading not supported");
err = 0;
}
So "ip link set swp2 master br1" succeeds and the port joins br1
unoffloaded with dp->bridge rolled back to NULL. Since the driver
programs no hardware forwarding domain (the new
xlnx_tsn_port_bridge_join() touches no register and ignores its bridge
argument), and dsa_port_enable_rt() puts a non-bridged port into hardware
forwarding:
if (!dp->bridge)
dsa_port_set_state_now(dp, BR_STATE_FORWARDING, false);
both MACs stay forwarding and the fabric keeps bridging them. Does this
mean the two L2 domains are merged in hardware anyway, which is the case
the commit message claims is prevented? If rejection is the intent,
would the driver need to track the offloaded bridge in port_bridge_join
and return something other than -EOPNOTSUPP (for example -EBUSY) with an
extack?
[Severity: Medium]
Is max_num_bridges = 1 workable given the bridge number allocator is
system-global rather than per-switch? In net/dsa/dsa.c,
dsa_bridge_num_get() picks the lowest free bit of the file-scope
dsa_fwd_offloading_bridges bitmap and compares that global index against
this driver's max:
bridge_num = find_next_zero_bit(&dsa_fwd_offloading_bridges,
DSA_MAX_NUM_OFFLOADING_BRIDGES,
1);
if (bridge_num > max)
return 0;
With max == 1 the only acceptable global index is 1. If any other DSA
switch, or a second instance of this platform driver, already holds global
bridge number 1, then the very first bridge on this switch gets index 2,
2 > 1, and dsa_port_bridge_create() returns -EOPNOTSUPP, so
xlnx_tsn_port_bridge_join() is never called and *tx_fwd_offload is never
set. dsa_user_changeupper() rewrites the error to 0, so the join looks
successful while the bridge silently resumes clone-and-send and the wire
sees duplicated frames again.
Other drivers size max_num_bridges from a hardware resource count (for
example DSA_TAG_8021Q_MAX_NUM_BRIDGES or ds->num_ports). Would that be
more appropriate here?
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 18/20] net: dsa: xilinx: offload the bridge FDB to the switch CAM
2026-08-07 10:44 ` [PATCH 18/20] net: dsa: xilinx: offload the bridge FDB to the switch CAM Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: xilinx: offload the bridge FDB to the switch CAM
This backs the bridge FDB with the switch's stream-destination lookup CAM,
adding indirect CAM access helpers plus a mutex, the port_fdb_add /
port_fdb_del / port_fdb_dump / port_fast_age ops, and enabling the hardware
learning engine on bridge join. A few questions below about the global
switch settings, locking and the dump path.
> 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);
> +}
[Severity: Medium]
If the restore write here fails, what is left programmed in the port's
field of TSN_PORT_STATE_CTRL?
xlnx_tsn_set_port_state() can fail in three places: the
xlnx_tsn_switch_status_ready() check, the -EBUSY path when the
change-commit bit is still set, and the -ETIMEDOUT ack poll. By the time
the restore leg runs, TSN_PORT_STATE_FLUSH has already been committed, and
the header comment describes it as "Not an STP state".
The only caller in this patch is xlnx_tsn_port_fast_age(), a void DSA op
that only dev_err()s, so nothing re-asserts the STP state afterwards while
DSA and the bridge still consider the port forwarding.
Since xlnx_tsn_get_port_state() returns the raw field with no validation,
would a later cycle on that port then read back the flush value as "saved"
and faithfully reprogram that non-STP value?
>
> 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);
> +
[Severity: High]
Can this leak frames between a bridged port and a standalone port?
The comment says "Standalone ports do not learn", but
xlnx_tsn_set_global_learning() clears one switch-wide
TSN_SW_ADDR_LEARN_DISABLE bit, and there is no per-port qualifier. Source
MACs seen on a standalone user port are then learnt into the same shared
CAM, and xlnx_tsn_setup() pins the switch-wide unknown-unicast miss action
to TSN_SW_CTRL_UCAST_MISS_FLOOD, so a CAM miss is replicated to ports that
are not bridge members.
DSA leaves standalone user ports at BR_STATE_FORWARDING via
dsa_port_enable() -> dsa_port_set_state(), so both wire ports are
forwarding in the fabric.
With swp0 in br0 and swp1 left standalone (or moved to another netns):
- unicast from the bridge domain misses the CAM and is flooded out swp1
- once swp1's neighbour is learnt into the shared CAM, br0 traffic is
unicast-forwarded to swp1
xlnx_tsn_port_bridge_leave() below only disables learning once the last
user port leaves any bridge, so a port that has already left keeps being
learnt and learning while the other port is bridged, and the entries
already installed in the CAM stay programmed. Is a port-isolation or
port-matrix register available to restrict the flood domain to bridge
members?
> /* 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);
> +}
> +
[ ... ]
> @@ -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);
> +}
> +
[ ... ]
> +/* 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);
> +}
[Severity: Medium]
Is the self-clearing of TSN_CAM_OP_ENABLE enough to conclude that
TSN_CAM_OP_ADD stored the entry?
The CAM is a finite resource (the read-key address space alone is 4096
slots), so an add can fail for lack of a free slot. The READ path does
consult TSN_CAM_FOUND in the same control register, but nothing is checked
after an ADD.
xlnx_tsn_port_fdb_add() returns this result unchanged, so DSA calls
dsa_fdb_offload_notify() and the bridge marks the entry offloaded even
though no hardware entry exists. Does the block expose a full or
add-failure indication that could be checked here?
> +
> +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);
> +}
[Severity: Medium]
Should this take sw->indirect_lock?
Writing TSN_PORT_STATE_FLUSH mutates the CAM (per the enum comment it
"flushes the port's dynamic learnt entries"), yet every other new CAM
accessor documents "Caller holds indirect_lock" and this one takes no lock.
The two contexts are not otherwise mutually exclusive: .port_fdb_add and
.port_fdb_del run from dsa_user_switchdev_event_work(), which does not take
rtnl_lock, while .port_fast_age is reached from rtnl-held paths
dsa_port_set_state() -> dsa_port_fast_age() and dsa_port_bridge_flags().
CPU0 in xlnx_tsn_port_fdb_add() holds indirect_lock and has finished
xlnx_tsn_cam_read_portlist() but not yet xlnx_tsn_cam_write(); CPU1 issues
the flush; CPU0 then writes back a port list computed from pre-flush state,
resurrecting a flushed entry. The TSN_CAM_STATUS_READY handshake only
covers the driver's own operations, so an ADD can also be started while the
hardware flush walk is in progress. The same window applies to the 2048
slot scan in xlnx_tsn_port_fdb_dump().
> +
> +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;
[Severity: Medium]
Can folding vid 0 onto TSN_SW_DEFAULT_VID make two software FDB entries
share one hardware entry?
nbp_vlan_init() adds the bridge's default_pvid (1) to every port's VLAN
group regardless of VLAN filtering, and br_fdb_add() then installs both a
vid 0 entry and one entry per configured VLAN:
net/bridge/br_fdb.c:br_fdb_add() {
...
err = __br_fdb_add(ndm, br, p, addr, nlh_flags, 0, nfea_tb, notified, extack);
if (err || !vg || !vg->num_vlans)
goto out;
list_for_each_entry(v, &vg->vlan_list, vlist) {
...
}
So a plain "bridge fdb add <mac> dev swp1 master" reaches the driver twice
and both calls program key (mac, 1); DSA does not refcount FDB entries for
user ports. A later "bridge fdb del <mac> dev swp1 master vlan 1" then
makes xlnx_tsn_port_fdb_del() clear the port bit and issue
TSN_CAM_OP_DELETE, destroying hardware state that the surviving vid 0
software entry still needs, while the bridge keeps it flagged offloaded.
The reverse mapping in xlnx_tsn_port_fdb_dump() also reports a genuine
vid 1 entry as vid 0, which hides the alias from the operator.
> +
> + 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;
> +}
[Severity: Medium]
Can this read-modify-write lose an update made by the learning engine?
After xlnx_tsn_port_bridge_join() clears TSN_SW_ADDR_LEARN_DISABLE, the
fabric writes the same (MAC, VID) entries autonomously, and
sw->indirect_lock only serialises the software writers:
xlnx_tsn_cam_read_portlist() /* snapshot */
<hardware learns the same key on the other port, sets its bit>
portlist |= TSN_PORT_BIT(port);
xlnx_tsn_cam_write() /* writes back the stale snapshot */
The bit set by hardware is then cleared, so the entry no longer forwards to
the port where the address is actually reachable. In xlnx_tsn_port_fdb_del()
the mirror case makes a port list that hardware repopulated after the read
look empty, so TSN_CAM_OP_DELETE is issued on a live entry.
No lock can order software against the learning engine. Is there an atomic
set/clear opcode, or could learning be masked around the sequence, or the
entry re-read and validated after the write?
> +
> +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;
[Severity: Medium]
How long does this hold rtnl_lock?
PF_BRIDGE / RTM_GETNEIGH is registered without RTNL_FLAG_DUMP_UNLOCKED:
net/core/rtnetlink.c:
{.protocol = PF_BRIDGE, .msgtype = RTM_GETNEIGH, .doit = rtnl_fdb_get,
.dumpit = rtnl_fdb_dump},
so rtnl_lock is held across the whole callback, and a netlink dump needs no
capability. Each of the TSN_CAM_READ_KEY_COUNT (2048) iterations runs two
sleeping readl_poll_timeout() loops with a 10 us delay and a 5000 us
timeout, so a healthy pass sleeps at least once per slot and a slow CAM can
stretch the pass towards seconds. A netlink dump also re-invokes the
callback per skb, repeating the full hardware scan for each chunk.
Would it be reasonable to cache the scan result, or bound the region
scanned, so that "bridge fdb show" cannot block the networking
configuration path for that long?
> +
> + 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;
> +}
[Severity: Medium]
Should this filter on the entry's port list?
The only filter in the loop is the TSN_CAM_FOUND test; TSN_CAM_PORT_ACT_OFFSET
and TSN_CAM_PORT_LIST, the bitmap that xlnx_tsn_port_fdb_add() and
xlnx_tsn_port_fdb_del() maintain, are never read here.
xlnx_tsn_cam_write() selects a slot purely from the (MAC, VID) key, with no
port index, so can a slot range encode ownership by port at all? Two
consequences follow:
- host entries written through .port_fdb_add for the CPU port (DSA calls
it from dsa_switch_host_fdb_add() -> dsa_port_do_fdb_add(cpu_dp), and
the driver programs TSN_PORT_BIT(0)) must land inside one of the two
windows, and get reported as learnt on that user port
- a (MAC, VID) reachable on both ports occupies a single slot, since the
read-modify-write logic assumes one entry per key, so it can appear in
at most one port's dump and is missing from the other's
The commit message says:
A (MAC, VID) reachable on several
ports then carries a correct multi-port list.
which holds for the port list in hardware, but the dump does not express
it.
Separately, every entry is passed to cb() with is_static = false, so
entries installed through .port_fdb_add come back as dynamic
(net/dsa/user.c maps is_static to ndm_state, NUD_REACHABLE versus
NUD_NOARP). Since "bridge fdb show" is an unprivileged dump, addresses
belonging to the other port's domain are also visible there.
> +
> 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);
> +
[Severity: Medium]
This overrides the hardware reset default for unknown-unicast forwarding
unconditionally at probe time. TSN_SW_CTRL_UCAST_MISS_MASK is GENMASK(1, 0)
of the switch-wide Switch Control Register, no per-port flood domain is
programmed anywhere in the driver, and neither port_bridge_join nor
port_bridge_leave revisits it, so the policy also applies to frames
ingressing standalone user ports. The commit message describes the CAM
offload, the learning engine and the new ops, but not this global
forwarding change.
The peer field TSN_SW_CTRL_MCAST_MISS_MASK and TSN_SW_CTRL_MCAST_MISS_FLOOD
are added in the same header block but never written here, so unknown
unicast and unknown multicast end up under different policies, one
programmed and one left at the reset default. Was the multicast write meant
to be here too?
> /* 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,
[Severity: Medium]
Since hardware learning is now switched on, should .port_pre_bridge_flags
and .port_bridge_flags be implemented as well?
That path is the only way the bridge port's BR_LEARNING, BR_FLOOD,
BR_MCAST_FLOOD and BR_PORT_LOCKED settings reach a driver:
net/dsa/port.c:dsa_port_bridge_flags() {
if (!ds->ops->port_bridge_flags)
return -EOPNOTSUPP;
...
}
and dsa_port_inherit_brport_flags() swallows -EOPNOTSUPP. So
"bridge link set dev swpN learning off" is accepted while the CAM learning
engine enabled in xlnx_tsn_port_bridge_join() keeps installing entries, and
dsa_port_can_configure_learning() keeps dp->learning at 0 although hardware
is learning.
> .port_hwtstamp_get = xlnx_tsn_port_hwtstamp_get,
> .port_hwtstamp_set = xlnx_tsn_port_hwtstamp_set,
> .get_ts_info = xlnx_tsn_get_ts_info,
[ ... ]
> 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 @@
[ ... ]
> 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,
> };
[Severity: Medium]
What makes an entry static from the hardware's point of view here?
xlnx_tsn_cam_write() writes TSN_CAM_TV1_OFFSET and TSN_CAM_TV2_OFFSET as 0
and uses the plain TSN_CAM_OP_ADD opcode, i.e. the same record format the
now-enabled learning engine produces, so nothing distinguishes a
bridge-programmed entry from a learnt one. Can this comment's guarantee be
relied on?
Two consequences follow if it cannot:
- xlnx_tsn_port_fast_age(), which DSA invokes on FORWARDING/LEARNING to
DISABLED/BLOCKING/LISTENING transitions via dsa_port_set_state() ->
dsa_port_fast_age(), can purge entries installed through
.port_fdb_add
- a spoofed source MAC can make the learning engine overwrite a
bridge-programmed (MAC, VID) port list with the port the frame arrived
on
Because xlnx_tsn_port_fdb_dump() reports every entry with is_static =
false, the bridge cannot observe either the loss or the override, so it
still believes its entries are offloaded and never reprograms them.
[ ... ]
Cross-instance finding from sashiko-gemini (96445ad11157158b46d97d25327595ed58d8d6812a15918739e8e9b5f82cb3e3):
[Severity: Medium]
Moving a static unicast FDB entry causes traffic duplication and information leak due to bitwise-ORing the port list.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 19/20] net: dsa: xilinx: offload bridge VLAN filtering to the switch
2026-08-07 10:44 ` [PATCH 19/20] net: dsa: xilinx: offload bridge VLAN filtering to the switch Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: xilinx: offload bridge VLAN filtering to the switch
This wires bridge VLAN filtering into the switch's per-VID port-membership
table and the per-port native-VID registers, and declares VLAN filtering
switch-wide. It also sets the per-MAC VLAN-enable bits so tagged frames are
not treated as oversize.
A few questions about the new VLAN paths below.
> 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
[ ... ]
> @@ -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);
> +}
[Severity: Medium]
Should xlnx_tsn_vlan_read() and xlnx_tsn_vlan_write() confirm the engine is
idle (TSN_VLAN_EN clear) before writing TSN_VLAN_EN again?
If xlnx_tsn_vlan_wait_done() times out, the error is propagated but
TSN_VLAN_EN is left set and nothing clears or re-checks it. The next
operation then writes EN=1 while EN is already 1. The driver assumes the
enable is edge-triggered for the sibling blocks, xlnx_tsn_cam_wait_ready()
polls CAM readiness before every CAM access and xlnx_tsn_set_port_state()
carries:
/* 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) {
...
return -EBUSY;
}
Can this then report a dropped access as a successful one?
readx_poll_timeout() evaluates its condition before the first sleep, so if
the previously stuck access clears EN just after the CTRL write,
xlnx_tsn_vlan_wait_done() returns 0 immediately and xlnx_tsn_vlan_read()
hands back the TSN_VLAN_DATA contents of the previous VID as the membership
word of the requested VID. The callers then write that value back:
xlnx_tsn_port_vlan_add() ORs the port bit into the wrong port list,
xlnx_tsn_port_vlan_del() clears a bit from the wrong entry, and
xlnx_tsn_port_vlan_filtering() copies one VID's port list onto another.
[ ... ]
> +/* 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);
> +}
[ ... ]
> +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;
[Severity: Medium]
What state is the switch left in when this function fails part way through?
A -ETIMEDOUT out of xlnx_tsn_vlan_wait_done() inside this loop returns
immediately, so the VIDs already visited have Port-List-Valid flipped while
the remaining ones keep the old value. DSA does not update
ds->vlan_filtering when the driver op fails (dsa_port_vlan_filtering()
returns the error before assigning it) and the bridge reverts its own
BROPT_VLAN_ENABLED, so those already-flipped VIDs enforce membership
together with the switch-wide TSN_SW_CTRL_MEMBER_VIOL_DISCARD pinned in
xlnx_tsn_setup(), while the bridge believes it is VLAN-unaware.
The same applies to everything written after sw->vlan_aware is committed
here: the miss policy, the learning qualifier, the native-VID/untag
registers and the port-state cycling. A failure in
xlnx_tsn_port_state_cycle() (-EBUSY "previous state change still pending"
or -ETIMEDOUT from xlnx_tsn_set_port_state()) leaves the hardware fully
VLAN-aware, with unknown unicast and multicast discarded rather than
flooded, while DSA and the driver shadow both think filtering is off.
Also, xlnx_tsn_port_state_cycle() sets the requested state and then
restores the saved one, so if the restoring write fails the user port stays
in TSN_PORT_STATE_BLOCKING or in TSN_PORT_STATE_FLUSH, which is not an STP
state at all, and nothing puts it back.
The last patch of the series ("net: dsa: xilinx: trap link-local control
frames to the CPU port") adds a restore: label here, but it only assigns
sw->vlan_aware back to its old value; every register written in between
keeps its new value, so this question still stands at the end of the
series.
> +
> + /* 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);
[Severity: Medium]
Is the VLAN-unaware state here the same as the VLAN-unaware state at boot?
TSN_SW_CTRL_MCAST_MISS_MASK is only ever programmed from this function.
xlnx_tsn_setup() masks and programs TSN_SW_CTRL_UCAST_MISS_MASK and
TSN_SW_CTRL_MEMBER_VIOL_MASK only, so at probe the multicast miss field
keeps whatever the register already holds:
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);
The header documents the encoding as flood / to processor / to MAC /
discard, with FLOOD == 0x1, so a reset value of 0x0 is a different action.
Unless the reset value happens to be FLOOD, unknown multicast and broadcast
handling in a VLAN-unaware bridge would depend on whether vlan_filtering
has ever been toggled once. Could xlnx_tsn_setup() program this field too?
The changelog also does not mention that the patch starts driving this
switch-wide field.
[ ... ]
> +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;
> + }
[Severity: Medium]
Can this restriction be bypassed by command ordering? It is validated
against the mutable sw->pvid[] shadow and never re-validated when the PVID
moves:
bridge vlan add dev swp1 vid 10 pvid untagged
bridge vlan add dev swp1 vid 20 pvid untagged
The second call has pvid == true, so the check above is skipped and the
if (pvid) branch below overwrites sw->pvid[1] with 20 and reprograms the
single native-VID/untag pair. The bridge keeps BRIDGE_VLAN_INFO_UNTAGGED
on VLAN 10 (__vlan_add_pvid() only moves vg->pvid, VLAN 10 is not
re-notified), so the bridge still expects VLAN 10 to egress untagged on
swp1 while the hardware now tags it, with no error reported.
Deleting VID 20 afterwards gives the same divergence through
xlnx_tsn_port_vlan_del(), which resets the native VID and untag setting to
the defaults while VLAN 10 is still configured untagged.
> +
> + 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;
> +}
[Severity: High]
What clears sw->pvid[port] when the bridge withdraws
BRIDGE_VLAN_INFO_PVID from a VLAN that stays configured?
bridge vlan add dev swp1 vid 10 pvid untagged
bridge vlan add dev swp1 vid 10 untagged # pvid flag dropped
nbp_vlan_add() detects the flag change via __vlan_flags_would_change() and
re-notifies switchdev with flags lacking PVID, so this function is called
with pvid == false. The extack check above is skipped because
vlan->vid == sw->pvid[port], the if (pvid) block is not taken, and the last
block only updates sw->pvid_untagged[]. sw->pvid[1] stays 10, so
xlnx_tsn_apply_pvid() keeps VID 10 in the native-VID register and untagged
frames from the wire are still admitted into VLAN 10 and forwarded to its
other members, although the bridge would drop them.
Other DSA drivers handle this notification explicitly.
mv88e6xxx_port_vlan_add():
} else if (vlan->vid && p->bridge_pvid.vid == vlan->vid) {
/* The old pvid was reinstalled as a non-pvid VLAN */
p->bridge_pvid.valid = false;
...
}
and ocelot_vlan_add() calls ocelot_port_set_pvid(ocelot, port, NULL) in the
same situation.
The mirror case is in xlnx_tsn_port_vlan_del() below:
if (sw->pvid[port] == vlan->vid) {
sw->pvid[port] = TSN_SW_DEFAULT_VID;
Rather than disabling untagged admission, this re-points untagged ingress
at VID 1. When VLAN 1 is a configured VLAN whose member list includes the
port (the bridge default_pvid VLAN, kept as a tagged member), does
TSN_SW_CTRL_MEMBER_VIOL_DISCARD still fire, or does untagged traffic leak
into VLAN 1 once the real PVID VLAN is deleted?
[ ... ]
> @@ -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);
> + }
> +
[Severity: Medium]
Should the per-VID VLAN-membership memory be flushed here as well?
The membership words at TSN_VLAN_CTRL_OFFSET / TSN_VLAN_DATA_OFFSET now
gate forwarding, and MEMBER_VIOL is pinned to DISCARD just above, but the
driver never initializes that memory at setup and never clears it at
teardown. The software shadows (sw->cfg_vids, sw->pvid[], sw->vlan_aware)
always start zeroed from devm_kzalloc(), so anything left in the table
takes part in forwarding without the driver knowing about it. This is
reachable after unbind/rebind or module reload where the entries were not
torn down, for example a failing xlnx_tsn_vlan_write() during bridge
teardown, a shutdown/kexec path (dsa_switch_shutdown() does no VLAN
cleanup), or a pre-programmed fabric.
Three consequences to consider:
A stale entry with Port-List-Valid set makes the pinned MEMBER_VIOL=DISCARD
action fire for tagged frames arriving on a port absent from the stale port
list, so traffic a VLAN-unaware bridge must forward is dropped. The new
comment "The membership check only bites once VLANs exist" would then not
hold.
xlnx_tsn_port_vlan_add() does a read-modify-write:
ret = xlnx_tsn_vlan_read(sw, vlan->vid, &data);
...
data |= TSN_PORT_BIT(port);
so a newly added VLAN inherits stale member ports and forwards that VID to
ports the bridge never added.
The Port-List-Valid flip in xlnx_tsn_port_vlan_filtering() walks only
sw->cfg_vids, so untracked VIDs are never made inert or repaired, and
last_member in xlnx_tsn_port_vlan_del() is computed from a port list that
can contain bits the driver never set.
For reference, mv88e6xxx flushes the whole VTU at setup via
mv88e6xxx_vtu_setup() -> mv88e6xxx_g1_vtu_flush().
[ ... ]
> 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
[ ... ]
> @@ -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
[Severity: Low]
This isn't a bug, but does the @pvid description match what the code does
for the CPU port?
pvid[XLNX_TSN_CPU_PORT] can never change: xlnx_tsn_port_vlan_add() returns
early at
/* The CPU port needs membership only. */
if (port == XLNX_TSN_CPU_PORT)
return 0;
before any pvid bookkeeping, and xlnx_tsn_port_vlan_del() only ever writes
TSN_SW_DEFAULT_VID back into it. So the endpoint native VID stays at VID 1
for the lifetime of the driver even though TSN_EP_NATIVE_VLAN_OFFSET and
xlnx_tsn_set_native_vid() handle XLNX_TSN_CPU_PORT.
The two apply loops also disagree on coverage: xlnx_tsn_setup() uses
for (port = XLNX_TSN_CPU_PORT; port < XLNX_TSN_NUM_PORTS; port++) while
xlnx_tsn_port_vlan_filtering() re-applies with
dsa_switch_for_each_user_port(), which excludes the CPU port. Could the
documentation say the CPU-port slots are unused, or the loops be made
consistent?
> + * @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
[ ... ]
Cross-instance finding from sashiko-gemini (4f56bb35ca00c92afa83ce956f0bfab329dc0d5a7ae6d461bc954faf0f898d34):
[Severity: Medium]
Mixing goto-based error handling with scope-based cleanup helpers (`guard()`, `scoped_guard()`) in the same function.
Cross-instance finding from sashiko-gemini (e8525d575b62bcd573cc21461acce156f144090732b49f8b9225c62ef9d9e0ba):
[Severity: High]
Leaking internal default VLAN tags to wire ports when VLAN filtering is disabled.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 20/20] net: dsa: xilinx: trap link-local control frames to the CPU port
2026-08-07 10:44 ` [PATCH 20/20] net: dsa: xilinx: trap link-local control frames to the CPU port Nagadheeraj Rottela
@ 2026-08-08 19:48 ` Jakub Kicinski
0 siblings, 0 replies; 45+ messages in thread
From: Jakub Kicinski @ 2026-08-08 19:48 UTC (permalink / raw)
To: nagadheeraj.rottela
Cc: Jakub Kicinski, srinivas.neeli, andrew+netdev, davem, edumazet,
pabeni, robh, krzk+dt, conor+dt, richardcochran, michal.simek,
andrew, olteanv, horms, linux, netdev, devicetree, linux-kernel,
linux-arm-kernel, git-dev
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: dsa: xilinx: trap link-local control frames to the CPU port
This adds CAM entries for the two bridge-group destination MACs in active
use (STP at 01:80:c2:00:00:00 and LLDP at 01:80:c2:00:00:0e) with a port
list containing only the CPU port and the endpoint management-queue bit
set. The entries are kept in sync with each wire port's native VID via a
new shadow list in struct xlnx_tsn.
> 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
[ ... ]
> @@ -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;
> +
[Severity: Medium]
Do the three FDB ops now disagree about these two addresses?
xlnx_tsn_port_fdb_add() and xlnx_tsn_port_fdb_del() return 0 without
touching the CAM, so a request like:
bridge fdb add 01:80:c2:00:00:00 dev swp1 master static
is accepted by br_fdb_add() (it only rejects the all-zero address),
notified to the driver through dsa_port_do_fdb_add() ->
ds->ops->port_fdb_add(), and reported as successfully offloaded although
nothing was programmed. The corresponding delete likewise returns success
while the trap entry stays in the CAM, so user space has no way to remove
it and gets no error.
xlnx_tsn_port_fdb_dump() has no matching filter - it walks the per-MAC
read-key region and reports every slot with TSN_CAM_FOUND set using
is_static = false:
ctrl = sw_ior(sw, TSN_CAM_CTRL_OFFSET);
if (!(ctrl & TSN_CAM_FOUND))
continue;
...
ret = cb(addr, vid, false, data);
If the trap entries land in a wire port's read-key region (the driver
already relies on that region for its own OP_ADD entries), they would be
dumped back as dynamic entries learnt on swp1/swp2, which contradicts what
add and del report.
Would it be better to either apply the same filter in the dump, or make
add/del return an error for these addresses instead of reporting success?
> if (!vid)
> vid = TSN_SW_DEFAULT_VID;
>
[ ... ]
> @@ -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;
>
[ ... ]
> @@ -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);
[Severity: Medium]
Can this port list also catch the host's own transmitted control frames?
The host bridge sends BPDUs and LLDPDUs with exactly these destination
MACs. Those frames enter the fabric from the endpoint port and, when
untagged, are looked up under the CPU port's native VID. While the bridge
is VLAN-unaware xlnx_tsn_apply_pvid() keeps that at TSN_SW_DEFAULT_VID,
which is also the trap key installed here, so the frame matches the same
CAM entry - whose port list contains only TSN_PORT_BIT(XLNX_TSN_CPU_PORT)
and therefore neither wire port.
Whether the frame still egresses depends on the SA-egress selection
programmed in xlnx_tsn_setup():
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);
taking precedence over a CAM hit. Is that precedence guaranteed by the IP?
Nothing in the driver or the register definitions states it. If the CAM
hit governs instead, the host's own BPDUs and LLDPDUs would be forwarded
only back to the ingress (CPU) port and dropped, which would disable STP
transmission entirely. Could this be confirmed against the switch IP
documentation, or the trap port list adjusted for endpoint ingress?
> + 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;
> +}
> +
[ ... ]
> +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;
> +}
[Severity: Medium]
Can the shadow list get permanently out of step with the CAM here?
xlnx_tsn_set_ctrl_traps() writes two entries and returns on the first
xlnx_tsn_cam_write() failure without unwinding the one it already
programmed, but this function treats it as all-or-nothing.
Removal direction: if the STP delete succeeds and the LLDP delete returns
-ETIMEDOUT from xlnx_tsn_cam_wait_ready()/xlnx_tsn_cam_wait_done(), the
early return happens before:
sw->ctrl_trap_vid[i] = sw->ctrl_trap_vid[--sw->ctrl_trap_count];
so the VID stays recorded as installed although one of its entries is
gone. If that VID becomes a wanted native VID again, the removal loop
keeps it and the add loop skips it, because
xlnx_tsn_vid_in(sw->ctrl_trap_vid, ...) is true - the deleted STP trap is
never reinstalled and BPDUs at that VID are relayed to the peer wire port
again, with no error reported anywhere.
Add direction: if the STP add succeeds and the LLDP add fails, the return
happens before:
sw->ctrl_trap_vid[sw->ctrl_trap_count++] = want[i];
so the installed entry is untracked, and xlnx_tsn_remove_ctrl_traps()
iterates only ctrl_trap_vid[0..ctrl_trap_count), so it can never delete
it.
Should xlnx_tsn_set_ctrl_traps() roll back the entries it already
programmed, or should the bookkeeping be updated per entry rather than per
VID?
> 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;
>
[ ... ]
> @@ -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;
> }
[Severity: Medium]
Does this rollback leave the shadow flag disagreeing with the hardware?
Only sw->vlan_aware is reverted, while every register write already made
for the new setting stays in place:
- TSN_VLAN_PORT_LIST_VALID flipped on each VID in sw->cfg_vids
- the unicast/multicast CAM-miss policy in TSN_SW_CTRL
- the VLAN-only-learning bit via xlnx_tsn_set_vlan_only_learning()
- the per-port native VID and untag bits via xlnx_tsn_apply_pvid()
xlnx_tsn_port_state_cycle() can return -EBUSY (previous state-change bit
still pending) or -ETIMEDOUT (ack timeout), and xlnx_tsn_sync_ctrl_traps()
can return -ETIMEDOUT from the CAM polling.
When enabling filtering fails that way, the hardware keeps discarding
unknown unicast/multicast and enforcing VLAN membership while
sw->vlan_aware is false and DSA and the bridge both believe filtering is
off, so traffic is dropped with no indication of why.
It also affects later operations: xlnx_tsn_port_vlan_add() takes the
data &= ~TSN_VLAN_PORT_LIST_VALID branch, xlnx_tsn_apply_pvid() reprograms
the native VID to TSN_SW_DEFAULT_VID, and xlnx_tsn_sync_ctrl_traps()
recomputes wanted trap VIDs from the reverted flag:
vid = sw->vlan_aware ? sw->pvid[dp->index] : TSN_SW_DEFAULT_VID;
so the CAM trap keys can end up permanently mismatched against the native
VIDs actually programmed. Before this patch the function returned the
error with the flag matching the writes already made. Would undoing the
hardware writes too, or dropping the flag revert, be more consistent?
[ ... ]
> @@ -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.
> + */
[Severity: Medium]
Can trapped frames actually reach the host bridge in this tree?
The tag protocol receive hook for this switch drops everything coming from
the conduit:
net/dsa/tag_xlnx_tsn.c:xlnx_tsn_rcv() {
kfree_skb(skb);
return NULL;
}
That is the only rcv path in the tagger, so no frame the switch delivers on
the CPU port ever reaches a DSA user netdev or the software bridge, and
this patch adds no management-queue consumer. With the CAM port list set to
TSN_PORT_BIT(XLNX_TSN_CPU_PORT) only, the observable change today looks
like 01:80:c2:00:00:00 and 01:80:c2:00:00:0e are dropped rather than
consumed locally.
The commit message says the switch "then delivers trapped frames on the
management queue" - is there a receive path for that queue, or does that
part depend on a follow-up patch?
Separately, this comment says the frame "would be flooded out the other
instead of reaching the host bridge", while the commit message says a CAM
miss is "flooded to all ports except the ingress port", which includes the
CPU port. Could the two descriptions be reconciled?
> + scoped_guard(mutex, &sw->indirect_lock) {
> + ret = xlnx_tsn_sync_ctrl_traps(sw);
> + if (ret)
> + return ret;
> + }
[Severity: Medium]
Should this error path use the err_traps label added below instead of
returning directly?
xlnx_tsn_sync_ctrl_traps() installs entries incrementally and records each
wanted VID as it goes, so a failure on the second VID - or on the second
destination MAC of the first VID - leaves CAM entries programmed and
ctrl_trap_count possibly non-zero.
Nothing removes them afterwards: DSA does not call ds->ops->teardown() when
ds->ops->setup() fails,
net/dsa/dsa.c:dsa_switch_setup() {
err = ds->ops->setup(ds);
if (err < 0)
goto unregister_notifier;
}
and the shadow state goes away with the failed probe, so a re-probe starts
with ctrl_trap_count = 0 and never accounts for the stale entries. The
mutex itself is released correctly by the scoped_guard cleanup; it is only
the xlnx_tsn_remove_ctrl_traps() call that is skipped.
> +
> 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;
> }
[ ... ]
Cross-instance finding from sashiko-gemini (f0bf43e93137481b1a742a87803d2ec1a8a9f5833a1ae7688d64415f986a1ea2):
[Severity: Medium]
Mixing goto-based error handling with scoped guard cleanups in the same function.
^ permalink raw reply [flat|nested] 45+ messages in thread
end of thread, other threads:[~2026-08-08 19:48 UTC | newest]
Thread overview: 45+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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-08 19:48 ` Jakub Kicinski
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
2026-08-08 12:27 ` Neeli, Srinivas
2026-08-08 19:48 ` Jakub Kicinski
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
2026-08-08 12:28 ` Neeli, Srinivas
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 04/20] net: xilinx: tsn: parse endpoint DMA channel configuration Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 05/20] net: xilinx: tsn: bring up the endpoint MCDMA channels Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 06/20] net: xilinx: tsn: add the endpoint RX data path Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 07/20] net: xilinx: tsn: add the endpoint TX " Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 08/20] net: xilinx: tsn: deliver endpoint RX frames to DSA user ports Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 09/20] net: dsa: tag_xlnx_tsn: add skeleton tag protocol Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 10/20] net: dsa: xilinx: add skeleton driver for TSN switch Nagadheeraj Rottela
2026-08-07 10:44 ` [PATCH 11/20] net: dsa: xilinx: implement port_stp_state_set Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 12/20] net: dsa: xilinx: register per-MAC MDIO buses Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 13/20] net: dsa: xilinx: wire up phylink for the switch ports Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 14/20] net: dsa: xilinx: program MAC frame filter and per-port nibbles Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 15/20] net: dsa: xilinx: register PHC backed by the RTC timer block Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 16/20] net: dsa: xilinx: drive per-MAC PTP TX/RX hardware paths Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 17/20] net: dsa: xilinx: opt into TX forwarding offload on bridge join Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 18/20] net: dsa: xilinx: offload the bridge FDB to the switch CAM Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 19/20] net: dsa: xilinx: offload bridge VLAN filtering to the switch Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 10:44 ` [PATCH 20/20] net: dsa: xilinx: trap link-local control frames to the CPU port Nagadheeraj Rottela
2026-08-08 19:48 ` Jakub Kicinski
2026-08-07 22:28 ` [PATCH 00/20] xilinx: tsn: Add TSN Endpoint Ethernet MAC driver support Jakub Kicinski
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).